Detailed changes
@@ -12214,7 +12214,6 @@ dependencies = [
"serde",
"serde_json",
"ui",
- "util",
"workspace",
"workspace-hack",
]
@@ -17632,6 +17631,7 @@ dependencies = [
"icons",
"itertools 0.14.0",
"menu",
+ "schemars",
"serde",
"settings",
"smallvec",
@@ -5,15 +5,15 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
- App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
+ App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list,
};
use std::{fmt::Display, ops::Range};
use text::Bias;
use time::{OffsetDateTime, UtcOffset};
use ui::{
- HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
- Tooltip, prelude::*,
+ HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar,
+ prelude::*,
};
pub struct AcpThreadHistory {
@@ -26,8 +26,6 @@ pub struct AcpThreadHistory {
visible_items: Vec<ListItemType>,
- scrollbar_visibility: bool,
- scrollbar_state: ScrollbarState,
local_timezone: UtcOffset,
_update_task: Task<()>,
@@ -90,7 +88,6 @@ impl AcpThreadHistory {
});
let scroll_handle = UniformListScrollHandle::default();
- let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
history_store,
@@ -99,8 +96,6 @@ impl AcpThreadHistory {
hovered_index: None,
visible_items: Default::default(),
search_editor,
- scrollbar_visibility: true,
- scrollbar_state,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
@@ -339,43 +334,6 @@ impl AcpThreadHistory {
task.detach_and_log_err(cx);
}
- fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
- return None;
- }
-
- Some(
- div()
- .occlude()
- .id("thread-history-scroll")
- .h_full()
- .bg(cx.theme().colors().panel_background.opacity(0.8))
- .border_l_1()
- .border_color(cx.theme().colors().border_variant)
- .absolute()
- .right_1()
- .top_0()
- .bottom_0()
- .w_4()
- .pl_1()
- .cursor_default()
- .on_mouse_move(cx.listener(|_, _, _window, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
- cx.notify();
- }))
- .children(Scrollbar::vertical(self.scrollbar_state.clone())),
- )
- }
-
fn render_list_items(
&mut self,
range: Range<usize>,
@@ -491,7 +449,7 @@ impl Focusable for AcpThreadHistory {
}
impl Render for AcpThreadHistory {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("ThreadHistory")
.size_full()
@@ -555,9 +513,7 @@ impl Render for AcpThreadHistory {
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
)
- .when_some(self.render_scrollbar(cx), |div, scrollbar| {
- div.child(scrollbar)
- })
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
}
})
}
@@ -24,10 +24,9 @@ use futures::FutureExt as _;
use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
- ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
- Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
- WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*,
- pulsating_between,
+ ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task,
+ TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
+ ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, pulsating_between,
};
use language::Buffer;
@@ -47,7 +46,7 @@ use text::Anchor;
use theme::{AgentFontSize, ThemeSettings};
use ui::{
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
- PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
+ PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -281,7 +280,6 @@ pub struct AcpThreadView {
thread_error: Option<ThreadError>,
thread_feedback: ThreadFeedbackState,
list_state: ListState,
- scrollbar_state: ScrollbarState,
auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
@@ -405,8 +403,7 @@ impl AcpThreadView {
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
- list_state: list_state.clone(),
- scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
+ list_state: list_state,
thread_retry_status: None,
thread_error: None,
thread_feedback: Default::default(),
@@ -4764,39 +4761,6 @@ impl AcpThreadView {
cx.notify();
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
- div()
- .id("acp-thread-scrollbar")
- .occlude()
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
- }
-
fn render_token_limit_callout(
&self,
line_height: Pixels,
@@ -5370,23 +5334,27 @@ impl Render for AcpThreadView {
configuration_view,
pending_auth_method,
..
- } => self.render_auth_required_state(
- connection,
- description.as_ref(),
- configuration_view.as_ref(),
- pending_auth_method.as_ref(),
- window,
- cx,
- ),
+ } => self
+ .render_auth_required_state(
+ connection,
+ description.as_ref(),
+ configuration_view.as_ref(),
+ pending_auth_method.as_ref(),
+ window,
+ cx,
+ )
+ .into_any(),
ThreadState::Loading { .. } => v_flex()
.flex_1()
- .child(self.render_recent_history(window, cx)),
+ .child(self.render_recent_history(window, cx))
+ .into_any(),
ThreadState::LoadError(e) => v_flex()
.flex_1()
.size_full()
.items_center()
.justify_end()
- .child(self.render_load_error(e, window, cx)),
+ .child(self.render_load_error(e, window, cx))
+ .into_any(),
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
if has_messages {
this.child(
@@ -5406,9 +5374,11 @@ impl Render for AcpThreadView {
.flex_grow()
.into_any(),
)
- .child(self.render_vertical_scrollbar(cx))
+ .vertical_scrollbar_for(self.list_state.clone(), window, cx)
+ .into_any()
} else {
this.child(self.render_recent_history(window, cx))
+ .into_any()
}
}),
})
@@ -35,8 +35,7 @@ use project::{
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
- Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
- prelude::*,
+ Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -64,7 +63,6 @@ pub struct AgentConfiguration {
tools: Entity<ToolWorkingSet>,
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
- scrollbar_state: ScrollbarState,
_check_for_gemini: Task<()>,
}
@@ -101,9 +99,6 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach();
- let scroll_handle = ScrollHandle::new();
- let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
-
let mut this = Self {
fs,
language_registry,
@@ -116,8 +111,7 @@ impl AgentConfiguration {
expanded_provider_configurations: HashMap::default(),
tools,
_registry_subscription: registry_subscription,
- scroll_handle,
- scrollbar_state,
+ scroll_handle: ScrollHandle::new(),
_check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
@@ -1157,42 +1151,21 @@ impl Render for AgentConfiguration {
.size_full()
.pb_8()
.bg(cx.theme().colors().panel_background)
- .child(
- v_flex()
- .id("assistant-configuration-content")
- .track_scroll(&self.scroll_handle)
- .size_full()
- .overflow_y_scroll()
- .child(self.render_general_settings_section(cx))
- .child(self.render_agent_servers_section(cx))
- .child(self.render_context_servers_section(window, cx))
- .child(self.render_provider_configuration_section(cx)),
- )
.child(
div()
- .id("assistant-configuration-scrollbar")
- .occlude()
- .absolute()
- .right(px(3.))
- .top_0()
- .bottom_0()
- .pb_6()
- .w(px(12.))
- .cursor_default()
- .on_mouse_move(cx.listener(|_, _, _window, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
- cx.notify();
- }))
- .children(Scrollbar::vertical(self.scrollbar_state.clone())),
+ .size_full()
+ .child(
+ v_flex()
+ .id("assistant-configuration-content")
+ .track_scroll(&self.scroll_handle)
+ .size_full()
+ .overflow_y_scroll()
+ .child(self.render_general_settings_section(cx))
+ .child(self.render_agent_servers_section(cx))
+ .child(self.render_context_servers_section(window, cx))
+ .child(self.render_provider_configuration_section(cx)),
+ )
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
)
}
}
@@ -10,7 +10,7 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use gpui::{
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
- Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
+ Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
};
use language::Point;
use project::{
@@ -23,8 +23,8 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
- Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, Scrollbar,
- ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*,
+ Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
+ StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
};
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@@ -49,7 +49,6 @@ pub(crate) struct BreakpointList {
breakpoint_store: Entity<BreakpointStore>,
dap_store: Entity<DapStore>,
worktree_store: Entity<WorktreeStore>,
- scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>,
session: Option<Entity<Session>>,
focus_handle: FocusHandle,
@@ -87,7 +86,6 @@ impl BreakpointList {
let dap_store = project.dap_store();
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
- let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
cx.new(|cx| {
@@ -95,7 +93,6 @@ impl BreakpointList {
breakpoint_store,
dap_store,
worktree_store,
- scrollbar_state,
breakpoints: Default::default(),
workspace,
session,
@@ -576,39 +573,6 @@ impl BreakpointList {
.flex_1()
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
- div()
- .occlude()
- .id("breakpoint-list-vertical-scrollbar")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
- }
-
pub(crate) fn render_control_strip(&self) -> AnyElement {
let selection_kind = self.selection_kind();
let focus_handle = self.focus_handle.clone();
@@ -789,7 +753,7 @@ impl Render for BreakpointList {
.size_full()
.pt_1()
.child(self.render_list(cx))
- .child(self.render_vertical_scrollbar(cx))
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal().color(DividerColor::Border))
.child(
@@ -1,17 +1,17 @@
use std::{
- cell::LazyCell,
+ cell::{LazyCell, RefCell, RefMut},
fmt::Write,
ops::RangeInclusive,
+ rc::Rc,
sync::{Arc, LazyLock},
time::Duration,
};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
- Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable,
- MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, TextStyle,
- UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point,
- uniform_list,
+ Action, Along, AppContext, Axis, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle,
+ Focusable, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle,
+ UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
@@ -19,7 +19,7 @@ use settings::Settings;
use theme::ThemeSettings;
use ui::{
ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render,
- Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*,
+ ScrollableHandle, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
};
use workspace::Workspace;
@@ -29,11 +29,9 @@ actions!(debugger, [GoToSelectedAddress]);
pub(crate) struct MemoryView {
workspace: WeakEntity<Workspace>,
- scroll_handle: UniformListScrollHandle,
- scroll_state: ScrollbarState,
stack_frame_list: WeakEntity<StackFrameList>,
focus_handle: FocusHandle,
- view_state: ViewState,
+ view_state_handle: ViewStateHandle,
query_editor: Entity<Editor>,
session: Entity<Session>,
width_picker_handle: PopoverMenuHandle<ContextMenu>,
@@ -90,18 +88,29 @@ impl SelectedMemoryRange {
}
}
+#[derive(Clone)]
+struct ViewStateHandle(Rc<RefCell<ViewState>>);
+
+impl ViewStateHandle {
+ fn new(base_row: u64, line_width: ViewWidth) -> Self {
+ Self(Rc::new(RefCell::new(ViewState::new(base_row, line_width))))
+ }
+}
+
#[derive(Clone)]
struct ViewState {
/// Uppermost row index
base_row: u64,
/// How many cells per row do we have?
line_width: ViewWidth,
+ scroll_handle: UniformListScrollHandle,
selection: Option<SelectedMemoryRange>,
}
impl ViewState {
fn new(base_row: u64, line_width: ViewWidth) -> Self {
Self {
+ scroll_handle: UniformListScrollHandle::new(),
base_row,
line_width,
selection: None,
@@ -119,13 +128,39 @@ impl ViewState {
fn schedule_scroll_up(&mut self) {
self.base_row = self.base_row.saturating_sub(1);
}
+
+ fn set_offset(&mut self, point: Point<Pixels>) {
+ if point.y >= -Pixels::ZERO {
+ self.schedule_scroll_up();
+ } else if point.y <= -self.scroll_handle.max_offset().height {
+ self.schedule_scroll_down();
+ }
+ self.scroll_handle.set_offset(point);
+ }
}
-struct ScrollbarDragging;
+impl ScrollableHandle for ViewStateHandle {
+ fn max_offset(&self) -> gpui::Size<Pixels> {
+ self.0.borrow().scroll_handle.max_offset()
+ }
+
+ fn set_offset(&self, point: Point<Pixels>) {
+ self.0.borrow_mut().set_offset(point);
+ }
+
+ fn offset(&self) -> Point<Pixels> {
+ self.0.borrow().scroll_handle.offset()
+ }
+
+ fn viewport(&self) -> gpui::Bounds<Pixels> {
+ self.0.borrow().scroll_handle.viewport()
+ }
+}
static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
+
impl MemoryView {
pub(crate) fn new(
session: Entity<Session>,
@@ -134,19 +169,15 @@ impl MemoryView {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
- let view_state = ViewState::new(0, WIDTHS[4].clone());
- let scroll_handle = UniformListScrollHandle::default();
+ let view_state_handle = ViewStateHandle::new(0, WIDTHS[4].clone());
let query_editor = cx.new(|cx| Editor::single_line(window, cx));
- let scroll_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
workspace,
- scroll_state,
- scroll_handle,
stack_frame_list,
focus_handle: cx.focus_handle(),
- view_state,
+ view_state_handle,
query_editor,
session,
width_picker_handle: Default::default(),
@@ -162,50 +193,17 @@ impl MemoryView {
this
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
- div()
- .occlude()
- .id("memory-view-vertical-scrollbar")
- .on_drag_move(cx.listener(|this, evt, _, cx| {
- let did_handle = this.handle_scroll_drag(evt);
- cx.notify();
- if did_handle {
- cx.stop_propagation()
- }
- }))
- .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx)))
+ fn view_state(&self) -> RefMut<'_, ViewState> {
+ self.view_state_handle.0.borrow_mut()
}
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
let weak = cx.weak_entity();
let session = self.session.clone();
- let view_state = self.view_state.clone();
+ let view_state = self.view_state_handle.0.borrow().clone();
uniform_list(
"debugger-memory-view",
- self.view_state.row_count() as usize,
+ view_state.row_count() as usize,
move |range, _, cx| {
let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize);
let memory_start =
@@ -230,22 +228,13 @@ impl MemoryView {
rows
},
)
- .track_scroll(self.scroll_handle.clone())
+ .track_scroll(view_state.scroll_handle)
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
+ let mut view_state = this.view_state();
let delta = evt.delta.pixel_delta(window.line_height());
- let scroll_handle = this.scroll_state.scroll_handle();
- let size = scroll_handle.content_size();
- let viewport = scroll_handle.viewport();
- let current_offset = scroll_handle.offset();
- let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32;
- let last_entry_offset_boundary = size.height - first_entry_offset_boundary;
- if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() {
- // The topmost entry is visible, hence if we're scrolling up, we need to load extra lines.
- this.view_state.schedule_scroll_up();
- } else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height {
- this.view_state.schedule_scroll_down();
- }
- scroll_handle.set_offset(current_offset + point(px(0.), delta.y));
+ let current_offset = view_state.scroll_handle.offset();
+ view_state
+ .set_offset(current_offset.apply_along(Axis::Vertical, |offset| offset + delta.y));
}))
}
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
@@ -275,7 +264,7 @@ impl MemoryView {
cx.spawn(async move |this, cx| {
let access_size = access_size.await.unwrap_or(1);
this.update(cx, |this, cx| {
- this.view_state.selection = Some(SelectedMemoryRange::DragComplete(Drag {
+ this.view_state().selection = Some(SelectedMemoryRange::DragComplete(Drag {
start_address: as_address,
end_address: as_address + access_size - 1,
}));
@@ -287,43 +276,23 @@ impl MemoryView {
}
fn handle_memory_drag(&mut self, evt: &DragMoveEvent<Drag>) {
- if !self
- .view_state
+ let mut view_state = self.view_state();
+ if !view_state
.selection
.as_ref()
.is_some_and(|selection| selection.is_dragging())
{
return;
}
- let row_count = self.view_state.row_count();
- debug_assert!(row_count > 1);
- let scroll_handle = self.scroll_state.scroll_handle();
- let viewport = scroll_handle.viewport();
-
- if viewport.bottom() < evt.event.position.y {
- self.view_state.schedule_scroll_down();
- } else if viewport.top() > evt.event.position.y {
- self.view_state.schedule_scroll_up();
- }
- }
-
- fn handle_scroll_drag(&mut self, evt: &DragMoveEvent<ScrollbarDragging>) -> bool {
- if !self.scroll_state.is_dragging() {
- return false;
- }
- let row_count = self.view_state.row_count();
+ let row_count = view_state.row_count();
debug_assert!(row_count > 1);
- let scroll_handle = self.scroll_state.scroll_handle();
+ let scroll_handle = &view_state.scroll_handle;
let viewport = scroll_handle.viewport();
if viewport.bottom() < evt.event.position.y {
- self.view_state.schedule_scroll_down();
- true
+ view_state.schedule_scroll_down();
} else if viewport.top() > evt.event.position.y {
- self.view_state.schedule_scroll_up();
- true
- } else {
- false
+ view_state.schedule_scroll_up();
}
}
@@ -354,7 +323,7 @@ impl MemoryView {
fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu {
let weak = cx.weak_entity();
- let selected_width = self.view_state.line_width.clone();
+ let selected_width = self.view_state().line_width.clone();
DropdownMenu::new(
"memory-view-width-picker",
selected_width.label.clone(),
@@ -364,24 +333,25 @@ impl MemoryView {
let width = width.clone();
this = this.entry(width.label.clone(), None, move |_, cx| {
_ = weak.update(cx, |this, _| {
+ let mut view_state = this.view_state();
// Convert base ix between 2 line widths to keep the shown memory address roughly the same.
// All widths are powers of 2, so the conversion should be lossless.
- match this.view_state.line_width.width.cmp(&width.width) {
+ match view_state.line_width.width.cmp(&width.width) {
std::cmp::Ordering::Less => {
// We're converting up.
let shift = width.width.trailing_zeros()
- - this.view_state.line_width.width.trailing_zeros();
- this.view_state.base_row >>= shift;
+ - view_state.line_width.width.trailing_zeros();
+ view_state.base_row >>= shift;
}
std::cmp::Ordering::Greater => {
// We're converting down.
- let shift = this.view_state.line_width.width.trailing_zeros()
+ let shift = view_state.line_width.width.trailing_zeros()
- width.width.trailing_zeros();
- this.view_state.base_row <<= shift;
+ view_state.base_row <<= shift;
}
_ => {}
}
- this.view_state.line_width = width.clone();
+ view_state.line_width = width.clone();
});
});
}
@@ -400,18 +370,18 @@ impl MemoryView {
}
fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
- self.view_state.base_row = self
- .view_state
+ let mut view_state = self.view_state();
+ view_state.base_row = view_state
.base_row
- .overflowing_add(self.view_state.row_count())
+ .overflowing_add(view_state.row_count())
.0;
cx.notify();
}
fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
- self.view_state.base_row = self
- .view_state
+ let mut view_state = self.view_state();
+ view_state.base_row = view_state
.base_row
- .overflowing_sub(self.view_state.row_count())
+ .overflowing_sub(view_state.row_count())
.0;
cx.notify();
}
@@ -447,7 +417,8 @@ impl MemoryView {
_: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(SelectedMemoryRange::DragComplete(selection)) = self.view_state.selection.clone()
+ let Some(SelectedMemoryRange::DragComplete(selection)) =
+ self.view_state().selection.clone()
else {
return;
};
@@ -484,7 +455,8 @@ impl MemoryView {
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
- if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection {
+ let selection = self.view_state().selection.clone();
+ if let Some(SelectedMemoryRange::DragComplete(drag)) = selection {
// Go into memory writing mode.
if !self.is_writing_memory {
let should_return = self.session.update(cx, |session, cx| {
@@ -558,9 +530,11 @@ impl MemoryView {
}
fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) {
- self.view_state.base_row = (address & !0xfff) / self.view_state.line_width.width as u64;
- let line_ix = (address & 0xfff) / self.view_state.line_width.width as u64;
- self.scroll_handle
+ let mut view_state = self.view_state();
+ view_state.base_row = (address & !0xfff) / view_state.line_width.width as u64;
+ let line_ix = (address & 0xfff) / view_state.line_width.width as u64;
+ view_state
+ .scroll_handle
.scroll_to_item(line_ix as usize, ScrollStrategy::Center);
cx.notify();
}
@@ -595,7 +569,7 @@ impl MemoryView {
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
- self.view_state.selection = None;
+ self.view_state().selection = None;
cx.notify();
}
@@ -606,7 +580,7 @@ impl MemoryView {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone()
+ let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state().selection.clone()
else {
return;
};
@@ -718,7 +692,7 @@ fn render_single_memory_view_line(
weak: gpui::WeakEntity<MemoryView>,
cx: &mut App,
) -> AnyElement {
- let Ok(view_state) = weak.update(cx, |this, _| this.view_state.clone()) else {
+ let Ok(view_state) = weak.update(cx, |this, _| this.view_state().clone()) else {
return div().into_any();
};
let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64;
@@ -799,7 +773,7 @@ fn render_single_memory_view_line(
let weak = weak.clone();
move |drag, _, _, cx| {
_ = weak.update(cx, |this, _| {
- this.view_state.selection =
+ this.view_state().selection =
Some(SelectedMemoryRange::DragUnderway(drag.clone()));
});
@@ -811,7 +785,7 @@ fn render_single_memory_view_line(
let weak = weak.clone();
move |drag: &Drag, _, cx| {
_ = weak.update(cx, |this, _| {
- this.view_state.selection =
+ this.view_state().selection =
Some(SelectedMemoryRange::DragComplete(Drag {
start_address: drag.start_address,
end_address: base_address + cell_ix as u64,
@@ -821,7 +795,7 @@ fn render_single_memory_view_line(
})
.drag_over(move |style, drag: &Drag, _, cx| {
_ = weak.update(cx, |this, _| {
- this.view_state.selection =
+ this.view_state().selection =
Some(SelectedMemoryRange::DragUnderway(Drag {
start_address: drag.start_address,
end_address: base_address + cell_ix as u64,
@@ -943,7 +917,7 @@ impl Render for MemoryView {
)
.with_priority(1)
}))
- .child(self.render_vertical_scrollbar(cx)),
+ .vertical_scrollbar_for(self.view_state_handle.clone(), window, cx),
)
}
}
@@ -1,15 +1,15 @@
use anyhow::anyhow;
use dap::Module;
use gpui::{
- AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
- Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
+ AnyElement, Entity, FocusHandle, Focusable, ScrollStrategy, Subscription, Task,
+ UniformListScrollHandle, WeakEntity, uniform_list,
};
use project::{
ProjectItem as _, ProjectPath,
debugger::session::{Session, SessionEvent},
};
use std::{ops::Range, path::Path, sync::Arc};
-use ui::{Scrollbar, ScrollbarState, prelude::*};
+use ui::{WithScrollbar, prelude::*};
use workspace::Workspace;
pub struct ModuleList {
@@ -18,7 +18,6 @@ pub struct ModuleList {
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
- scrollbar_state: ScrollbarState,
entries: Vec<Module>,
_rebuild_task: Option<Task<()>>,
_subscription: Subscription,
@@ -44,7 +43,6 @@ impl ModuleList {
let scroll_handle = UniformListScrollHandle::new();
Self {
- scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
session,
workspace,
@@ -167,38 +165,6 @@ impl ModuleList {
self.session
.update(cx, |session, cx| session.modules(cx).to_vec())
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
- div()
- .occlude()
- .id("module-list-vertical-scrollbar")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()))
- }
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else { return };
@@ -313,6 +279,6 @@ impl Render for ModuleList {
.size_full()
.p_1()
.child(self.render_list(window, cx))
- .child(self.render_vertical_scrollbar(cx))
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
}
}
@@ -7,7 +7,7 @@ use dap::StackFrameId;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
- MouseButton, Stateful, Subscription, Task, WeakEntity, list,
+ Subscription, Task, WeakEntity, list,
};
use util::debug_panic;
@@ -16,7 +16,7 @@ use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
use project::{ProjectItem, ProjectPath};
-use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
+use ui::{Tooltip, WithScrollbar, prelude::*};
use workspace::{ItemHandle, Workspace};
use super::RunningState;
@@ -64,7 +64,6 @@ pub struct StackFrameList {
workspace: WeakEntity<Workspace>,
selected_ix: Option<usize>,
opened_stack_frame_id: Option<StackFrameId>,
- scrollbar_state: ScrollbarState,
list_state: ListState,
list_filter: StackFrameFilter,
filter_entries_indices: Vec<usize>,
@@ -102,7 +101,6 @@ impl StackFrameList {
});
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
- let scrollbar_state = ScrollbarState::new(list_state.clone());
let list_filter = KEY_VALUE_STORE
.read_kvp(&format!(
@@ -127,7 +125,6 @@ impl StackFrameList {
opened_stack_frame_id: None,
list_filter,
list_state,
- scrollbar_state,
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
@@ -697,39 +694,6 @@ impl StackFrameList {
}
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
- div()
- .occlude()
- .id("stack-frame-list-vertical-scrollbar")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()))
- }
-
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
cx.notify();
@@ -941,7 +905,7 @@ impl Render for StackFrameList {
)
})
.child(self.render_list(window, cx))
- .child(self.render_vertical_scrollbar(cx))
+ .vertical_scrollbar_for(self.list_state.clone(), window, cx)
}
}
@@ -8,9 +8,8 @@ use dap::{
use editor::Editor;
use gpui::{
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
- FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
- TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
- uniform_list,
+ FocusHandle, Focusable, Hsla, MouseDownEvent, Point, Subscription, TextStyleRefinement,
+ UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::{
@@ -18,7 +17,7 @@ use project::debugger::{
session::{Session, SessionEvent, Watcher},
};
use std::{collections::HashMap, ops::Range, sync::Arc};
-use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
+use ui::{ContextMenu, ListItem, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
use util::{debug_panic, maybe};
actions!(
@@ -189,7 +188,6 @@ pub struct VariableList {
entry_states: HashMap<EntryPath, EntryState>,
selected_stack_frame_id: Option<StackFrameId>,
list_handle: UniformListScrollHandle,
- scrollbar_state: ScrollbarState,
session: Entity<Session>,
selection: Option<EntryPath>,
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
@@ -235,7 +233,6 @@ impl VariableList {
let list_state = UniformListScrollHandle::default();
Self {
- scrollbar_state: ScrollbarState::new(list_state.clone()),
list_handle: list_state,
session,
focus_handle,
@@ -1500,39 +1497,6 @@ impl VariableList {
)
.into_any()
}
-
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
- div()
- .occlude()
- .id("variable-list-vertical-scrollbar")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()))
- }
}
impl Focusable for VariableList {
@@ -1542,7 +1506,7 @@ impl Focusable for VariableList {
}
impl Render for VariableList {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.key_context("VariableList")
@@ -1587,7 +1551,7 @@ impl Render for VariableList {
)
.with_priority(1)
}))
- .child(self.render_vertical_scrollbar(cx))
+ .vertical_scrollbar_for(self.list_handle.clone(), window, cx)
}
}
@@ -55,7 +55,7 @@ pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPla
pub use edit_prediction::Direction;
pub use editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
- ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar,
+ ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
};
pub use editor_settings_controls::*;
pub use element::{
@@ -166,7 +166,7 @@ use project::{
};
use rand::seq::SliceRandom;
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
-use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
+use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
use selections_collection::{
MutableSelectionsCollection, SelectionsCollection, resolve_selections,
};
@@ -196,7 +196,7 @@ use theme::{
};
use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
- IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
+ IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, scrollbars::ScrollbarAutoHide,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
@@ -7,6 +7,7 @@ use project::project_settings::DiagnosticSeverity;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings};
+use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
use util::serde::default_true;
/// Imports from the VSCode settings at
@@ -205,23 +206,6 @@ pub struct Gutter {
pub folds: bool,
}
-/// When to show the scrollbar in the editor.
-///
-/// Default: auto
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowScrollbar {
- /// Show the scrollbar if there's important information or
- /// follow the system's configured behavior.
- Auto,
- /// Match the system's configured behavior.
- System,
- /// Always show the scrollbar.
- Always,
- /// Never show the scrollbar.
- Never,
-}
-
/// When to show the minimap in the editor.
///
/// Default: never
@@ -778,6 +762,12 @@ impl EditorSettings {
}
}
+impl ScrollbarVisibility for EditorSettings {
+ fn visibility(&self, _cx: &App) -> ShowScrollbar {
+ self.scrollbar.show
+ }
+}
+
impl Settings for EditorSettings {
type FileContent = EditorSettingsContent;
@@ -18,7 +18,7 @@ use crate::{
editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes,
- ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
+ ScrollbarDiagnostics, ShowMinimap,
},
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
hover_popover::{
@@ -85,7 +85,7 @@ use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::utils::ensure_minimum_contrast;
use ui::{
ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
- right_click_menu,
+ right_click_menu, scrollbars::ShowScrollbar,
};
use unicode_segmentation::UnicodeSegmentation;
use util::post_inc;
@@ -9,8 +9,8 @@ use anyhow::Context as _;
use gpui::{
AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla,
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
- Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
- TextStyleRefinement, Window, div, px,
+ StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement,
+ Window, div, px,
};
use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
@@ -23,7 +23,7 @@ use std::{borrow::Cow, cell::RefCell};
use std::{ops::Range, sync::Arc, time::Duration};
use std::{path::PathBuf, rc::Rc};
use theme::ThemeSettings;
-use ui::{Scrollbar, ScrollbarState, prelude::*, theme_is_transparent};
+use ui::{Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
use url::Url;
use util::TryFutureExt;
use workspace::{OpenOptions, OpenVisible, Workspace};
@@ -184,7 +184,6 @@ pub fn hover_at_inlay(
let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content,
- scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(false)),
anchor: None,
@@ -387,7 +386,6 @@ fn show_hover(
local_diagnostic,
markdown,
border_color,
- scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
background_color,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
@@ -457,7 +455,6 @@ fn show_hover(
info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
- scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
@@ -507,7 +504,6 @@ fn show_hover(
info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
- scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
@@ -846,7 +842,6 @@ pub struct InfoPopover {
pub symbol_range: RangeInEditor,
pub parsed_content: Option<Entity<Markdown>>,
pub scroll_handle: ScrollHandle,
- pub scrollbar_state: ScrollbarState,
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>,
_subscription: Option<Subscription>,
@@ -891,7 +886,12 @@ impl InfoPopover {
.on_url_click(open_markdown_url),
),
)
- .child(self.render_vertical_scrollbar(cx))
+ .custom_scrollbars(
+ Scrollbars::for_settings::<EditorSettings>()
+ .tracked_scroll_handle(self.scroll_handle.clone()),
+ window,
+ cx,
+ )
})
.into_any_element()
}
@@ -905,39 +905,6 @@ impl InfoPopover {
cx.notify();
self.scroll_handle.set_offset(current);
}
-
- fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
- div()
- .occlude()
- .id("info-popover-vertical-scroll")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()))
- }
}
pub struct DiagnosticPopover {
@@ -949,7 +916,6 @@ pub struct DiagnosticPopover {
pub anchor: Anchor,
_subscription: Subscription,
pub scroll_handle: ScrollHandle,
- pub scrollbar_state: ScrollbarState,
}
impl DiagnosticPopover {
@@ -1013,43 +979,15 @@ impl DiagnosticPopover {
),
),
)
- .child(self.render_vertical_scrollbar(cx)),
+ .custom_scrollbars(
+ Scrollbars::for_settings::<EditorSettings>()
+ .tracked_scroll_handle(self.scroll_handle.clone()),
+ window,
+ cx,
+ ),
)
.into_any_element()
}
-
- fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
- div()
- .occlude()
- .id("diagnostic-popover-vertical-scroll")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|_, _, _, cx| {
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()))
- }
}
#[cfg(test)]
@@ -12,7 +12,7 @@ use crate::{
};
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use core::fmt::Debug;
-use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px};
+use gpui::{Along, App, Axis, Context, Pixels, Task, Window, point, px};
use language::language_settings::{AllLanguageSettings, SoftWrap};
use language::{Bias, Point};
pub use scroll_amount::ScrollAmount;
@@ -21,6 +21,7 @@ use std::{
cmp::Ordering,
time::{Duration, Instant},
};
+use ui::scrollbars::ScrollbarAutoHide;
use util::ResultExt;
use workspace::{ItemId, WorkspaceId};
@@ -29,11 +30,6 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
pub struct WasScrolled(pub(crate) bool);
-#[derive(Default)]
-pub struct ScrollbarAutoHide(pub bool);
-
-impl Global for ScrollbarAutoHide {}
-
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor {
pub offset: gpui::Point<f32>,
@@ -327,7 +323,7 @@ impl ScrollManager {
cx.notify();
}
- if cx.default_global::<ScrollbarAutoHide>().0 {
+ if cx.default_global::<ScrollbarAutoHide>().should_hide() {
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
@@ -2,8 +2,8 @@ use crate::actions::ShowSignatureHelp;
use crate::hover_popover::open_markdown_url;
use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
use gpui::{
- App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful,
- StyledText, Task, TextStyle, Window, combine_highlights,
+ App, Context, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, StyledText, Task,
+ TextStyle, Window, combine_highlights,
};
use language::BufferSnapshot;
use markdown::{Markdown, MarkdownElement};
@@ -15,8 +15,8 @@ use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
- LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString,
- StatefulInteractiveElement, Styled, StyledExt, div, px, relative,
+ LabelSize, ParentElement, Pixels, SharedString, StatefulInteractiveElement, Styled, StyledExt,
+ WithScrollbar, div, relative,
};
// Language-specific settings may define quotes as "brackets", so filter them out separately.
@@ -243,7 +243,6 @@ impl Editor {
.min(signatures.len().saturating_sub(1));
let signature_help_popover = SignatureHelpPopover {
- scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
style,
signatures,
current_signature,
@@ -330,7 +329,6 @@ pub struct SignatureHelpPopover {
pub signatures: Vec<SignatureHelp>,
pub current_signature: usize,
scroll_handle: ScrollHandle,
- scrollbar_state: ScrollbarState,
}
impl SignatureHelpPopover {
@@ -391,7 +389,8 @@ impl SignatureHelpPopover {
)
}),
)
- .child(self.render_vertical_scrollbar(cx));
+ .vertical_scrollbar(window, cx);
+
let controls = if self.signatures.len() > 1 {
let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
.shape(IconButtonShape::Square)
@@ -460,26 +459,4 @@ impl SignatureHelpPopover {
.child(main_content)
.into_any_element()
}
-
- fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
- div()
- .occlude()
- .id("signature_help_scrollbar")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| cx.stop_propagation())
- .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
- .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation())
- .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify()))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_1()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.scrollbar_state.clone()))
- }
}
@@ -24,8 +24,8 @@ use settings::Settings;
use strum::IntoEnumIterator as _;
use theme::ThemeSettings;
use ui::{
- CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,
- ToggleButton, Tooltip, prelude::*,
+ CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, ToggleButton, Tooltip,
+ WithScrollbar, prelude::*,
};
use vim_mode_setting::VimModeSetting;
use workspace::{
@@ -290,7 +290,6 @@ pub struct ExtensionsPage {
_subscriptions: [gpui::Subscription; 2],
extension_fetch_task: Option<Task<()>>,
upsells: BTreeSet<Feature>,
- scrollbar_state: ScrollbarState,
}
impl ExtensionsPage {
@@ -339,7 +338,7 @@ impl ExtensionsPage {
let mut this = Self {
workspace: workspace.weak_handle(),
- list: scroll_handle.clone(),
+ list: scroll_handle,
is_fetching_extensions: false,
filter: ExtensionFilter::All,
dev_extension_entries: Vec::new(),
@@ -351,7 +350,6 @@ impl ExtensionsPage {
_subscriptions: subscriptions,
query_editor,
upsells: BTreeSet::default(),
- scrollbar_state: ScrollbarState::new(scroll_handle),
};
this.fetch_extensions(
this.search_query(cx),
@@ -1375,7 +1373,7 @@ impl ExtensionsPage {
}
impl Render for ExtensionsPage {
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.size_full()
.bg(cx.theme().colors().editor_background)
@@ -1520,25 +1518,24 @@ impl Render for ExtensionsPage {
}
if count == 0 {
- return this.py_4().child(self.render_empty_state(cx));
- }
-
- let scroll_handle = self.list.clone();
- this.child(
- uniform_list("entries", count, cx.processor(Self::render_extensions))
+ this.py_4()
+ .child(self.render_empty_state(cx))
+ .into_any_element()
+ } else {
+ let scroll_handle = self.list.clone();
+ this.child(
+ uniform_list(
+ "entries",
+ count,
+ cx.processor(Self::render_extensions),
+ )
.flex_grow()
.pb_4()
- .track_scroll(scroll_handle),
- )
- .child(
- div()
- .absolute()
- .right_1()
- .top_0()
- .bottom_0()
- .w(px(12.))
- .children(Scrollbar::vertical(self.scrollbar_state.clone())),
- )
+ .track_scroll(scroll_handle.clone()),
+ )
+ .vertical_scrollbar_for(scroll_handle, window, cx)
+ .into_any_element()
+ }
}),
)
}
@@ -13,10 +13,7 @@ use agent_settings::AgentSettings;
use anyhow::Context as _;
use askpass::AskPassDelegate;
use db::kvp::KEY_VALUE_STORE;
-use editor::{
- Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
- scroll::ScrollbarAutoHide,
-};
+use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
use futures::StreamExt as _;
use git::blame::ParsedCommitMessage;
use git::repository::{
@@ -32,11 +29,10 @@ use git::{
TrashUntrackedFiles, UnstageAll,
};
use gpui::{
- Action, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity,
- EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
- ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
- Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
- uniform_list,
+ Action, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity, EventEmitter,
+ FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
+ MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
+ UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
};
use itertools::Itertools;
use language::{Buffer, File};
@@ -64,7 +60,7 @@ use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize,
- PopoverMenu, Scrollbar, ScrollbarState, SplitButton, Tooltip, prelude::*,
+ PopoverMenu, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::SERIALIZATION_THROTTLE_TIME;
@@ -291,61 +287,6 @@ struct PendingOperation {
op_id: usize,
}
-// computed state related to how to render scrollbars
-// one per axis
-// on render we just read this off the panel
-// we update it when
-// - settings change
-// - on focus in, on focus out, on hover, etc.
-#[derive(Debug)]
-struct ScrollbarProperties {
- axis: Axis,
- show_scrollbar: bool,
- show_track: bool,
- auto_hide: bool,
- hide_task: Option<Task<()>>,
- state: ScrollbarState,
-}
-
-impl ScrollbarProperties {
- // Shows the scrollbar and cancels any pending hide task
- fn show(&mut self, cx: &mut Context<GitPanel>) {
- if !self.auto_hide {
- return;
- }
- self.show_scrollbar = true;
- self.hide_task.take();
- cx.notify();
- }
-
- fn hide(&mut self, window: &mut Window, cx: &mut Context<GitPanel>) {
- const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
-
- if !self.auto_hide {
- return;
- }
-
- let axis = self.axis;
- self.hide_task = Some(cx.spawn_in(window, async move |panel, cx| {
- cx.background_executor()
- .timer(SCROLLBAR_SHOW_INTERVAL)
- .await;
-
- if let Some(panel) = panel.upgrade() {
- panel
- .update(cx, |panel, cx| {
- match axis {
- Axis::Vertical => panel.vertical_scrollbar.show_scrollbar = false,
- Axis::Horizontal => panel.horizontal_scrollbar.show_scrollbar = false,
- }
- cx.notify();
- })
- .log_err();
- }
- }));
- }
-}
-
pub struct GitPanel {
pub(crate) active_repository: Option<Entity<Repository>>,
pub(crate) commit_editor: Entity<Editor>,
@@ -358,8 +299,6 @@ pub struct GitPanel {
single_tracked_entry: Option<GitStatusEntry>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
- horizontal_scrollbar: ScrollbarProperties,
- vertical_scrollbar: ScrollbarProperties,
new_count: usize,
entry_count: usize,
new_staged_count: usize,
@@ -443,10 +382,6 @@ impl GitPanel {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
- cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
- this.hide_scrollbars(window, cx);
- })
- .detach();
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
@@ -471,24 +406,6 @@ impl GitPanel {
let scroll_handle = UniformListScrollHandle::new();
- let vertical_scrollbar = ScrollbarProperties {
- axis: Axis::Vertical,
- state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
- show_scrollbar: false,
- show_track: false,
- auto_hide: false,
- hide_task: None,
- };
-
- let horizontal_scrollbar = ScrollbarProperties {
- axis: Axis::Horizontal,
- state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
- show_scrollbar: false,
- show_track: false,
- auto_hide: false,
- hide_task: None,
- };
-
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
@@ -570,8 +487,6 @@ impl GitPanel {
workspace: workspace.weak_handle(),
modal_open: false,
entry_count: 0,
- horizontal_scrollbar,
- vertical_scrollbar,
bulk_staging: None,
stash_entries: Default::default(),
_settings_subscription,
@@ -582,86 +497,6 @@ impl GitPanel {
})
}
- fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.horizontal_scrollbar.hide(window, cx);
- self.vertical_scrollbar.hide(window, cx);
- }
-
- fn update_scrollbar_properties(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- // TODO: This PR should have defined Editor's `scrollbar.axis`
- // as an Option<ScrollbarAxis>, not a ScrollbarAxes as it would allow you to
- // `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`.
- //
- // Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis`
- // so we can show each axis based on the settings.
- //
- // We should fix this. PR: https://github.com/zed-industries/zed/pull/19495
-
- let show_setting = GitPanelSettings::get_global(cx)
- .scrollbar
- .show
- .unwrap_or(EditorSettings::get_global(cx).scrollbar.show);
-
- let scroll_handle = self.scroll_handle.0.borrow();
-
- let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
- ShowScrollbar::Auto => true,
- ShowScrollbar::System => cx
- .try_global::<ScrollbarAutoHide>()
- .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
- ShowScrollbar::Always => false,
- ShowScrollbar::Never => false,
- };
-
- let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
- (size.contents.width > size.item.width).then_some(size.contents.width)
- });
-
- // is there an item long enough that we should show a horizontal scrollbar?
- let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
- longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
- } else {
- true
- };
-
- let show_horizontal = match (show_setting, item_wider_than_container) {
- (_, false) => false,
- (ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always, true) => true,
- (ShowScrollbar::Never, true) => false,
- };
-
- let show_vertical = match show_setting {
- ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
- ShowScrollbar::Never => false,
- };
-
- let show_horizontal_track =
- show_horizontal && matches!(show_setting, ShowScrollbar::Always);
-
- // TODO: we probably should hide the scroll track when the list doesn't need to scroll
- let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
-
- self.vertical_scrollbar = ScrollbarProperties {
- axis: self.vertical_scrollbar.axis,
- state: self.vertical_scrollbar.state.clone(),
- show_scrollbar: show_vertical,
- show_track: show_vertical_track,
- auto_hide: autohide(show_setting, cx),
- hide_task: None,
- };
-
- self.horizontal_scrollbar = ScrollbarProperties {
- axis: self.horizontal_scrollbar.axis,
- state: self.horizontal_scrollbar.state.clone(),
- show_scrollbar: show_horizontal,
- show_track: show_horizontal_track,
- auto_hide: autohide(show_setting, cx),
- hide_task: None,
- };
-
- cx.notify();
- }
-
pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option<usize> {
if GitPanelSettings::get_global(cx).sort_by_path {
return self
@@ -2676,7 +2511,6 @@ impl GitPanel {
git_panel.clear_pending();
}
git_panel.update_visible_entries(window, cx);
- git_panel.update_scrollbar_properties(window, cx);
})
.ok();
}
@@ -3799,110 +3633,6 @@ impl GitPanel {
)
}
- fn render_vertical_scrollbar(
- &self,
- show_horizontal_scrollbar_container: bool,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
- div()
- .id("git-panel-vertical-scroll")
- .occlude()
- .flex_none()
- .h_full()
- .cursor_default()
- .absolute()
- .right_0()
- .top_0()
- .bottom_0()
- .w(px(12.))
- .when(show_horizontal_scrollbar_container, |this| {
- this.pb_neg_3p5()
- })
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|this, _, window, cx| {
- if !this.vertical_scrollbar.state.is_dragging()
- && !this.focus_handle.contains_focused(window, cx)
- {
- this.vertical_scrollbar.hide(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .children(Scrollbar::vertical(
- // percentage as f32..end_offset as f32,
- self.vertical_scrollbar.state.clone(),
- ))
- }
-
- /// Renders the horizontal scrollbar.
- ///
- /// The right offset is used to determine how far to the right the
- /// scrollbar should extend to, useful for ensuring it doesn't collide
- /// with the vertical scrollbar when visible.
- fn render_horizontal_scrollbar(
- &self,
- right_offset: Pixels,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
- div()
- .id("git-panel-horizontal-scroll")
- .occlude()
- .flex_none()
- .w_full()
- .cursor_default()
- .absolute()
- .bottom_neg_px()
- .left_0()
- .right_0()
- .pr(right_offset)
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|this, _, window, cx| {
- if !this.horizontal_scrollbar.state.is_dragging()
- && !this.focus_handle.contains_focused(window, cx)
- {
- this.horizontal_scrollbar.hide(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .children(Scrollbar::horizontal(
- // percentage as f32..end_offset as f32,
- self.horizontal_scrollbar.state.clone(),
- ))
- }
-
fn render_buffer_header_controls(
&self,
entity: &Entity<Self>,
@@ -3950,33 +3680,16 @@ impl GitPanel {
fn render_entries(
&self,
has_write_access: bool,
- _: &Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let entry_count = self.entries.len();
- let scroll_track_size = px(16.);
-
- let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar {
- // magic number
- px(3.)
- } else {
- px(0.)
- };
-
v_flex()
.flex_1()
.size_full()
.overflow_hidden()
.relative()
- // Show a border on the top and bottom of the container when
- // the vertical scrollbar container is visible so we don't have a
- // floating left border in the panel.
- .when(self.vertical_scrollbar.show_track, |this| {
- this.border_t_1()
- .border_b_1()
- .border_color(cx.theme().colors().border)
- })
.child(
h_flex()
.flex_1()
@@ -4017,15 +3730,6 @@ impl GitPanel {
items
}),
)
- .when(
- !self.horizontal_scrollbar.show_track
- && self.horizontal_scrollbar.show_scrollbar,
- |this| {
- // when not showing the horizontal scrollbar track, make sure we don't
- // obscure the last entry
- this.pb(scroll_track_size)
- },
- )
.size_full()
.flex_grow()
.with_sizing_behavior(ListSizingBehavior::Auto)
@@ -4041,72 +3745,14 @@ impl GitPanel {
this.deploy_panel_context_menu(event.position, window, cx)
}),
)
- .when(self.vertical_scrollbar.show_track, |this| {
- this.child(
- v_flex()
- .h_full()
- .flex_none()
- .w(scroll_track_size)
- .bg(cx.theme().colors().panel_background)
- .child(
- div()
- .size_full()
- .flex_1()
- .border_l_1()
- .border_color(cx.theme().colors().border),
- ),
- )
- })
- .when(self.vertical_scrollbar.show_scrollbar, |this| {
- this.child(
- self.render_vertical_scrollbar(
- self.horizontal_scrollbar.show_track,
- cx,
- ),
- )
- }),
+ .custom_scrollbars(
+ Scrollbars::for_settings::<GitPanelSettings>()
+ .tracked_scroll_handle(self.scroll_handle.clone())
+ .with_track_along(ScrollAxes::Horizontal),
+ window,
+ cx,
+ ),
)
- .when(self.horizontal_scrollbar.show_track, |this| {
- this.child(
- h_flex()
- .w_full()
- .h(scroll_track_size)
- .flex_none()
- .relative()
- .child(
- div()
- .w_full()
- .flex_1()
- // for some reason the horizontal scrollbar is 1px
- // taller than the vertical scrollbar??
- .h(scroll_track_size - px(1.))
- .bg(cx.theme().colors().panel_background)
- .border_t_1()
- .border_color(cx.theme().colors().border),
- )
- .when(self.vertical_scrollbar.show_track, |this| {
- this.child(
- div()
- .flex_none()
- // -1px prevents a missing pixel between the two container borders
- .w(scroll_track_size - px(1.))
- .h_full(),
- )
- .child(
- // HACK: Fill the missing 1px 🥲
- div()
- .absolute()
- .right(scroll_track_size - px(1.))
- .bottom(scroll_track_size - px(1.))
- .size_px()
- .bg(cx.theme().colors().border),
- )
- }),
- )
- })
- .when(self.horizontal_scrollbar.show_scrollbar, |this| {
- this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx))
- })
}
fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
@@ -4620,15 +4266,6 @@ impl Render for GitPanel {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
.on_action(cx.listener(Self::toggle_sort_by_path))
- .on_hover(cx.listener(move |this, hovered, window, cx| {
- if *hovered {
- this.horizontal_scrollbar.show(cx);
- this.vertical_scrollbar.show(cx);
- cx.notify();
- } else if !this.focus_handle.contains_focused(window, cx) {
- this.hide_scrollbars(window, cx);
- }
- }))
.size_full()
.overflow_hidden()
.bg(cx.theme().colors().panel_background)
@@ -1,8 +1,9 @@
-use editor::ShowScrollbar;
+use editor::EditorSettings;
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
use workspace::dock::DockPosition;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -90,6 +91,22 @@ pub struct GitPanelSettings {
pub collapse_untracked_diff: bool,
}
+impl ScrollbarVisibility for GitPanelSettings {
+ fn visibility(&self, cx: &ui::App) -> ShowScrollbar {
+ // TODO: This PR should have defined Editor's `scrollbar.axis`
+ // as an Option<ScrollbarAxis>, not a ScrollbarAxes as it would allow you to
+ // `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`.
+ //
+ // Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis`
+ // so we can show each axis based on the settings.
+ //
+ // We should fix this. PR: https://github.com/zed-industries/zed/pull/19495
+ self.scrollbar
+ .show
+ .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
+ }
+}
+
impl Settings for GitPanelSettings {
type FileContent = GitPanelSettingsContent;
@@ -16,10 +16,10 @@
//! constructed by combining these two systems into an all-in-one element.
use crate::{
- Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase,
- Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior,
- HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent,
- KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton,
+ AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent,
+ DispatchPhase, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox,
+ HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent,
+ KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton,
MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels,
Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task,
TooltipId, Visibility, Window, WindowControlArea, point, px, size,
@@ -1036,6 +1036,15 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self
}
+ /// Set the space to be reserved for rendering the scrollbar.
+ ///
+ /// This will only affect the layout of the element when overflow for this element is set to
+ /// `Overflow::Scroll`.
+ fn scrollbar_width(mut self, width: impl Into<AbsoluteLength>) -> Self {
+ self.interactivity().base_style.scrollbar_width = Some(width.into());
+ self
+ }
+
/// Track the scroll state of this element with the given handle.
fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone());
@@ -5,10 +5,10 @@
//! elements with uniform height.
use crate::{
- AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
- Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
- ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
- Window, point, size,
+ AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity,
+ GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
+ IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size,
+ StyleRefinement, Styled, Window, point, size,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -71,7 +71,7 @@ pub struct UniformList {
/// Frame state used by the [UniformList].
pub struct UniformListFrameState {
items: SmallVec<[AnyElement; 32]>,
- decorations: SmallVec<[AnyElement; 1]>,
+ decorations: SmallVec<[AnyElement; 2]>,
}
/// A handle for controlling the scroll position of a uniform list.
@@ -529,6 +529,31 @@ pub trait UniformListDecoration {
) -> AnyElement;
}
+impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
+ fn compute(
+ &self,
+ visible_range: Range<usize>,
+ bounds: Bounds<Pixels>,
+ scroll_offset: Point<Pixels>,
+ item_height: Pixels,
+ item_count: usize,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyElement {
+ self.update(cx, |inner, cx| {
+ inner.compute(
+ visible_range,
+ bounds,
+ scroll_offset,
+ item_height,
+ item_count,
+ window,
+ cx,
+ )
+ })
+ }
+}
+
impl UniformList {
/// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
@@ -153,7 +153,7 @@ pub struct Style {
#[refineable]
pub overflow: Point<Overflow>,
/// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
- pub scrollbar_width: f32,
+ pub scrollbar_width: AbsoluteLength,
/// Whether both x and y axis should be scrollable at the same time.
pub allow_concurrent_scroll: bool,
/// Whether scrolling should be restricted to the axis indicated by the mouse wheel.
@@ -745,7 +745,7 @@ impl Default for Style {
},
allow_concurrent_scroll: false,
restrict_scroll_to_axis: false,
- scrollbar_width: 0.0,
+ scrollbar_width: AbsoluteLength::default(),
position: Position::Relative,
inset: Edges::auto(),
margin: Edges::<Length>::zero(),
@@ -277,7 +277,7 @@ impl ToTaffy<taffy::style::Style> for Style {
taffy::style::Style {
display: self.display.into(),
overflow: self.overflow.into(),
- scrollbar_width: self.scrollbar_width,
+ scrollbar_width: self.scrollbar_width.to_taffy(rem_size),
position: self.position.into(),
inset: self.inset.to_taffy(rem_size),
size: self.size.to_taffy(rem_size),
@@ -314,6 +314,15 @@ impl ToTaffy<taffy::style::Style> for Style {
}
}
+impl ToTaffy<f32> for AbsoluteLength {
+ fn to_taffy(&self, rem_size: Pixels) -> f32 {
+ match self {
+ AbsoluteLength::Pixels(pixels) => pixels.into(),
+ AbsoluteLength::Rems(rems) => (*rems * rem_size).into(),
+ }
+ }
+}
+
impl ToTaffy<taffy::style::LengthPercentageAuto> for Length {
fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto {
match self {
@@ -4960,6 +4960,12 @@ impl<T: Into<SharedString>> From<(ElementId, T)> for ElementId {
}
}
+impl From<&'static core::panic::Location<'static>> for ElementId {
+ fn from(location: &'static core::panic::Location<'static>) -> Self {
+ ElementId::CodeLocation(*location)
+ }
+}
+
/// A rectangle to be rendered in the window at the given position and size.
/// Passed as an argument [`Window::paint_quad`].
#[derive(Clone)]
@@ -13,8 +13,8 @@ use editor::{CompletionProvider, Editor, EditorEvent};
use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
- EventEmitter, FocusHandle, Focusable, Global, IsZero,
+ Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
+ FocusHandle, Focusable, Global, IsZero,
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy,
ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
@@ -425,7 +425,7 @@ impl KeymapEditor {
fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let _keymap_subscription =
cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed);
- let table_interaction_state = TableInteractionState::new(window, cx);
+ let table_interaction_state = TableInteractionState::new(cx);
let keystroke_editor = cx.new(|cx| {
let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
@@ -781,9 +781,8 @@ impl KeymapEditor {
match previous_edit {
// should remove scroll from process_query
PreviousEdit::ScrollBarOffset(offset) => {
- this.table_interaction_state.update(cx, |table, _| {
- table.set_scrollbar_offset(Axis::Vertical, offset)
- })
+ this.table_interaction_state
+ .update(cx, |table, _| table.set_scroll_offset(offset))
// set selected index and scroll
}
PreviousEdit::Keybinding {
@@ -812,9 +811,8 @@ impl KeymapEditor {
cx,
);
} else {
- this.table_interaction_state.update(cx, |table, _| {
- table.set_scrollbar_offset(Axis::Vertical, fallback)
- });
+ this.table_interaction_state
+ .update(cx, |table, _| table.set_scroll_offset(fallback));
}
cx.notify();
}
@@ -1199,9 +1197,7 @@ impl KeymapEditor {
};
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
- self.table_interaction_state
- .read(cx)
- .get_scrollbar_offset(Axis::Vertical),
+ self.table_interaction_state.read(cx).scroll_offset(),
));
let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |_, _| {
@@ -2374,10 +2370,7 @@ impl KeybindingEditorModal {
keymap.previous_edit = Some(PreviousEdit::Keybinding {
action_mapping,
action_name,
- fallback: keymap
- .table_interaction_state
- .read(cx)
- .get_scrollbar_offset(Axis::Vertical),
+ fallback: keymap.table_interaction_state.read(cx).scroll_offset(),
});
let status_toast = StatusToast::new(
format!("Saved edits to the {} action.", humanized_action_name),
@@ -1,20 +1,20 @@
-use std::{ops::Range, rc::Rc, time::Duration};
+use std::{ops::Range, rc::Rc};
-use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
+use editor::EditorSettings;
use gpui::{
- AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, EntityId,
- FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point,
- Stateful, Task, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
+ AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId,
+ FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful,
+ UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
};
use itertools::intersperse_with;
-use settings::Settings as _;
use ui::{
ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
- Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, StyledExt as _,
- StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
+ ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _,
+ StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px,
+ single_example, v_flex,
};
const RESIZE_COLUMN_WIDTH: f32 = 8.0;
@@ -56,136 +56,22 @@ impl<const COLS: usize> TableContents<COLS> {
pub struct TableInteractionState {
pub focus_handle: FocusHandle,
pub scroll_handle: UniformListScrollHandle,
- pub horizontal_scrollbar: ScrollbarProperties,
- pub vertical_scrollbar: ScrollbarProperties,
}
impl TableInteractionState {
- pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
- cx.new(|cx| {
- let focus_handle = cx.focus_handle();
-
- cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
- this.hide_scrollbars(window, cx);
- })
- .detach();
-
- let scroll_handle = UniformListScrollHandle::new();
- let vertical_scrollbar = ScrollbarProperties {
- axis: Axis::Vertical,
- state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
- show_scrollbar: false,
- show_track: false,
- auto_hide: false,
- hide_task: None,
- };
-
- let horizontal_scrollbar = ScrollbarProperties {
- axis: Axis::Horizontal,
- state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
- show_scrollbar: false,
- show_track: false,
- auto_hide: false,
- hide_task: None,
- };
-
- let mut this = Self {
- focus_handle,
- scroll_handle,
- horizontal_scrollbar,
- vertical_scrollbar,
- };
-
- this.update_scrollbar_visibility(cx);
- this
+ pub fn new(cx: &mut App) -> Entity<Self> {
+ cx.new(|cx| Self {
+ focus_handle: cx.focus_handle(),
+ scroll_handle: UniformListScrollHandle::new(),
})
}
- pub fn get_scrollbar_offset(&self, axis: Axis) -> Point<Pixels> {
- match axis {
- Axis::Vertical => self.vertical_scrollbar.state.scroll_handle().offset(),
- Axis::Horizontal => self.horizontal_scrollbar.state.scroll_handle().offset(),
- }
- }
-
- pub fn set_scrollbar_offset(&self, axis: Axis, offset: Point<Pixels>) {
- match axis {
- Axis::Vertical => self
- .vertical_scrollbar
- .state
- .scroll_handle()
- .set_offset(offset),
- Axis::Horizontal => self
- .horizontal_scrollbar
- .state
- .scroll_handle()
- .set_offset(offset),
- }
- }
-
- fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
- let show_setting = EditorSettings::get_global(cx).scrollbar.show;
-
- let scroll_handle = self.scroll_handle.0.borrow();
-
- let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
- ShowScrollbar::Auto => true,
- ShowScrollbar::System => cx
- .try_global::<ScrollbarAutoHide>()
- .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
- ShowScrollbar::Always => false,
- ShowScrollbar::Never => false,
- };
-
- let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
- (size.contents.width > size.item.width).then_some(size.contents.width)
- });
-
- // is there an item long enough that we should show a horizontal scrollbar?
- let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
- longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
- } else {
- true
- };
-
- let show_scrollbar = match show_setting {
- ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
- ShowScrollbar::Never => false,
- };
- let show_vertical = show_scrollbar;
-
- let show_horizontal = item_wider_than_container && show_scrollbar;
-
- let show_horizontal_track =
- show_horizontal && matches!(show_setting, ShowScrollbar::Always);
-
- // TODO: we probably should hide the scroll track when the list doesn't need to scroll
- let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
-
- self.vertical_scrollbar = ScrollbarProperties {
- axis: self.vertical_scrollbar.axis,
- state: self.vertical_scrollbar.state.clone(),
- show_scrollbar: show_vertical,
- show_track: show_vertical_track,
- auto_hide: autohide(show_setting, cx),
- hide_task: None,
- };
-
- self.horizontal_scrollbar = ScrollbarProperties {
- axis: self.horizontal_scrollbar.axis,
- state: self.horizontal_scrollbar.state.clone(),
- show_scrollbar: show_horizontal,
- show_track: show_horizontal_track,
- auto_hide: autohide(show_setting, cx),
- hide_task: None,
- };
-
- cx.notify();
+ pub fn scroll_offset(&self) -> Point<Pixels> {
+ self.scroll_handle.offset()
}
- fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.horizontal_scrollbar.hide(window, cx);
- self.vertical_scrollbar.hide(window, cx);
+ pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
+ self.scroll_handle.set_offset(offset);
}
pub fn listener<E: ?Sized>(
@@ -280,183 +166,6 @@ impl TableInteractionState {
.children(dividers)
.into_any_element()
}
-
- fn render_vertical_scrollbar_track(
- this: &Entity<Self>,
- parent: Div,
- scroll_track_size: Pixels,
- cx: &mut App,
- ) -> Div {
- if !this.read(cx).vertical_scrollbar.show_track {
- return parent;
- }
- let child = v_flex()
- .h_full()
- .flex_none()
- .w(scroll_track_size)
- .bg(cx.theme().colors().background)
- .child(
- div()
- .size_full()
- .flex_1()
- .border_l_1()
- .border_color(cx.theme().colors().border),
- );
- parent.child(child)
- }
-
- fn render_vertical_scrollbar(this: &Entity<Self>, parent: Div, cx: &mut App) -> Div {
- if !this.read(cx).vertical_scrollbar.show_scrollbar {
- return parent;
- }
- let child = div()
- .id(("table-vertical-scrollbar", this.entity_id()))
- .occlude()
- .flex_none()
- .h_full()
- .cursor_default()
- .absolute()
- .right_0()
- .top_0()
- .bottom_0()
- .w(px(12.))
- .on_mouse_move(Self::listener(this, |_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- Self::listener(this, |this, _, window, cx| {
- if !this.vertical_scrollbar.state.is_dragging()
- && !this.focus_handle.contains_focused(window, cx)
- {
- this.vertical_scrollbar.hide(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
- cx.notify();
- }))
- .children(Scrollbar::vertical(
- this.read(cx).vertical_scrollbar.state.clone(),
- ));
- parent.child(child)
- }
-
- /// Renders the horizontal scrollbar.
- ///
- /// The right offset is used to determine how far to the right the
- /// scrollbar should extend to, useful for ensuring it doesn't collide
- /// with the vertical scrollbar when visible.
- fn render_horizontal_scrollbar(
- this: &Entity<Self>,
- parent: Div,
- right_offset: Pixels,
- cx: &mut App,
- ) -> Div {
- if !this.read(cx).horizontal_scrollbar.show_scrollbar {
- return parent;
- }
- let child = div()
- .id(("table-horizontal-scrollbar", this.entity_id()))
- .occlude()
- .flex_none()
- .w_full()
- .cursor_default()
- .absolute()
- .bottom_neg_px()
- .left_0()
- .right_0()
- .pr(right_offset)
- .on_mouse_move(Self::listener(this, |_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- Self::listener(this, |this, _, window, cx| {
- if !this.horizontal_scrollbar.state.is_dragging()
- && !this.focus_handle.contains_focused(window, cx)
- {
- this.horizontal_scrollbar.hide(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
- cx.notify();
- }))
- .children(Scrollbar::horizontal(
- // percentage as f32..end_offset as f32,
- this.read(cx).horizontal_scrollbar.state.clone(),
- ));
- parent.child(child)
- }
-
- fn render_horizontal_scrollbar_track(
- this: &Entity<Self>,
- parent: Div,
- scroll_track_size: Pixels,
- cx: &mut App,
- ) -> Div {
- if !this.read(cx).horizontal_scrollbar.show_track {
- return parent;
- }
- let child = h_flex()
- .w_full()
- .h(scroll_track_size)
- .flex_none()
- .relative()
- .child(
- div()
- .w_full()
- .flex_1()
- // for some reason the horizontal scrollbar is 1px
- // taller than the vertical scrollbar??
- .h(scroll_track_size - px(1.))
- .bg(cx.theme().colors().background)
- .border_t_1()
- .border_color(cx.theme().colors().border),
- )
- .when(this.read(cx).vertical_scrollbar.show_track, |parent| {
- parent
- .child(
- div()
- .flex_none()
- // -1px prevents a missing pixel between the two container borders
- .w(scroll_track_size - px(1.))
- .h_full(),
- )
- .child(
- // HACK: Fill the missing 1px 🥲
- div()
- .absolute()
- .right(scroll_track_size - px(1.))
- .bottom(scroll_track_size - px(1.))
- .size_px()
- .bg(cx.theme().colors().border),
- )
- });
-
- parent.child(child)
- }
}
#[derive(Debug, Copy, Clone, PartialEq)]
@@ -1054,17 +763,6 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
.and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial)))
.map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial));
- let scroll_track_size = px(16.);
- let h_scroll_offset = if interaction_state
- .as_ref()
- .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
- {
- // magic number
- px(3.)
- } else {
- px(0.)
- };
-
let width = self.width;
let no_rows_rendered = self.rows.is_empty();
@@ -1115,8 +813,8 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
})
}
})
- .child(
- div()
+ .child({
+ let content = div()
.flex_grow()
.w_full()
.relative()
@@ -1187,25 +885,21 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
)
}))
},
- )
- .when_some(interaction_state.as_ref(), |this, interaction_state| {
- this.map(|this| {
- TableInteractionState::render_vertical_scrollbar_track(
- interaction_state,
- this,
- scroll_track_size,
- cx,
- )
- })
- .map(|this| {
- TableInteractionState::render_vertical_scrollbar(
- interaction_state,
- this,
- cx,
- )
- })
- }),
- )
+ );
+
+ if let Some(state) = interaction_state.as_ref() {
+ content
+ .custom_scrollbars(
+ Scrollbars::for_settings::<EditorSettings>()
+ .tracked_scroll_handle(state.read(cx).scroll_handle.clone()),
+ window,
+ cx,
+ )
+ .into_any_element()
+ } else {
+ content.into_any_element()
+ }
+ })
.when_some(
no_rows_rendered
.then_some(self.empty_table_callback)
@@ -1220,52 +914,12 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
.child(callback(window, cx)),
)
},
- )
- .when_some(
- width.and(interaction_state.as_ref()),
- |this, interaction_state| {
- this.map(|this| {
- TableInteractionState::render_horizontal_scrollbar_track(
- interaction_state,
- this,
- scroll_track_size,
- cx,
- )
- })
- .map(|this| {
- TableInteractionState::render_horizontal_scrollbar(
- interaction_state,
- this,
- h_scroll_offset,
- cx,
- )
- })
- },
);
if let Some(interaction_state) = interaction_state.as_ref() {
table
.track_focus(&interaction_state.read(cx).focus_handle)
.id(("table", interaction_state.entity_id()))
- .on_hover({
- let interaction_state = interaction_state.downgrade();
- move |hovered, window, cx| {
- interaction_state
- .update(cx, |interaction_state, cx| {
- if *hovered {
- interaction_state.horizontal_scrollbar.show(cx);
- interaction_state.vertical_scrollbar.show(cx);
- cx.notify();
- } else if !interaction_state
- .focus_handle
- .contains_focused(window, cx)
- {
- interaction_state.hide_scrollbars(window, cx);
- }
- })
- .ok();
- }
- })
.into_any_element()
} else {
table.into_any_element()
@@ -1273,65 +927,6 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
}
}
-// computed state related to how to render scrollbars
-// one per axis
-// on render we just read this off the keymap editor
-// we update it when
-// - settings change
-// - on focus in, on focus out, on hover, etc.
-#[derive(Debug)]
-pub struct ScrollbarProperties {
- axis: Axis,
- show_scrollbar: bool,
- show_track: bool,
- auto_hide: bool,
- hide_task: Option<Task<()>>,
- state: ScrollbarState,
-}
-
-impl ScrollbarProperties {
- // Shows the scrollbar and cancels any pending hide task
- fn show(&mut self, cx: &mut Context<TableInteractionState>) {
- if !self.auto_hide {
- return;
- }
- self.show_scrollbar = true;
- self.hide_task.take();
- cx.notify();
- }
-
- fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
- const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
-
- if !self.auto_hide {
- return;
- }
-
- let axis = self.axis;
- self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
- cx.background_executor()
- .timer(SCROLLBAR_SHOW_INTERVAL)
- .await;
-
- if let Some(keymap_editor) = keymap_editor.upgrade() {
- keymap_editor
- .update(cx, |keymap_editor, cx| {
- match axis {
- Axis::Vertical => {
- keymap_editor.vertical_scrollbar.show_scrollbar = false
- }
- Axis::Horizontal => {
- keymap_editor.horizontal_scrollbar.show_scrollbar = false
- }
- }
- cx.notify();
- })
- .ok();
- }
- }));
- }
-}
-
impl Component for Table<3> {
fn scope() -> ComponentScope {
ComponentScope::Layout
@@ -4,11 +4,11 @@ use anyhow::Context as _;
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use db::kvp::KEY_VALUE_STORE;
use editor::{
- AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId,
- ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, ShowScrollbar,
+ AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange,
+ MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects,
display_map::ToDisplayPoint,
items::{entry_git_aware_label_color, entry_label_color},
- scroll::{Autoscroll, ScrollAnchor, ScrollbarAutoHide},
+ scroll::{Autoscroll, ScrollAnchor},
};
use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
@@ -45,19 +45,18 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use smol::channel;
use theme::{SyntaxTheme, ThemeSettings};
-use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout};
+use ui::{
+ ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, DynamicSpacing, FluentBuilder,
+ HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, IndentGuideColors,
+ IndentGuideLayout, Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt,
+ StyledTypography, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex,
+};
use util::{RangeExt, ResultExt, TryFutureExt, debug_panic};
use workspace::{
OpenInTerminal, WeakItemHandle, Workspace,
dock::{DockPosition, Panel, PanelEvent},
item::ItemHandle,
searchable::{SearchEvent, SearchableItem},
- ui::{
- ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel,
- Icon, IconButton, IconButtonShape, IconName, IconSize, Label, LabelCommon, ListItem,
- Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, h_flex,
- v_flex,
- },
};
use worktree::{Entry, ProjectEntryId, WorktreeId};
@@ -125,10 +124,6 @@ pub struct OutlinePanel {
cached_entries: Vec<CachedEntry>,
filter_editor: Entity<Editor>,
mode: ItemsDisplayMode,
- show_scrollbar: bool,
- vertical_scrollbar_state: ScrollbarState,
- horizontal_scrollbar_state: ScrollbarState,
- hide_scrollbar_task: Option<Task<()>>,
max_width_item_index: Option<usize>,
preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
pending_default_expansion_depth: Option<usize>,
@@ -752,10 +747,6 @@ impl OutlinePanel {
let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in);
- let focus_out_subscription =
- cx.on_focus_out(&focus_handle, window, |outline_panel, _, window, cx| {
- outline_panel.hide_scrollbar(window, cx);
- });
let workspace_subscription = cx.subscribe_in(
&workspace
.weak_handle()
@@ -868,12 +859,6 @@ impl OutlinePanel {
workspace: workspace_handle,
project,
fs: workspace.app_state().fs.clone(),
- show_scrollbar: !Self::should_autohide_scrollbar(cx),
- hide_scrollbar_task: None,
- vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
- .parent_entity(&cx.entity()),
- horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
- .parent_entity(&cx.entity()),
max_width_item_index: None,
scroll_handle,
focus_handle,
@@ -903,7 +888,6 @@ impl OutlinePanel {
settings_subscription,
icons_subscription,
focus_subscription,
- focus_out_subscription,
workspace_subscription,
filter_update_subscription,
],
@@ -4489,150 +4473,6 @@ impl OutlinePanel {
cx.notify();
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !Self::should_show_scrollbar(cx)
- || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
- {
- return None;
- }
- Some(
- div()
- .occlude()
- .id("project-panel-vertical-scroll")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|outline_panel, _, window, cx| {
- if !outline_panel.vertical_scrollbar_state.is_dragging()
- && !outline_panel.focus_handle.contains_focused(window, cx)
- {
- outline_panel.hide_scrollbar(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
- )
- }
-
- fn render_horizontal_scrollbar(
- &self,
- _: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<Stateful<Div>> {
- if !Self::should_show_scrollbar(cx)
- || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
- {
- return None;
- }
- Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
- div()
- .occlude()
- .id("project-panel-horizontal-scroll")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|outline_panel, _, window, cx| {
- if !outline_panel.horizontal_scrollbar_state.is_dragging()
- && !outline_panel.focus_handle.contains_focused(window, cx)
- {
- outline_panel.hide_scrollbar(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .w_full()
- .absolute()
- .right_1()
- .left_1()
- .bottom_0()
- .h(px(12.))
- .cursor_default()
- .child(scrollbar)
- })
- }
-
- fn should_show_scrollbar(cx: &App) -> bool {
- let show = OutlinePanelSettings::get_global(cx)
- .scrollbar
- .show
- .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
- match show {
- ShowScrollbar::Auto => true,
- ShowScrollbar::System => true,
- ShowScrollbar::Always => true,
- ShowScrollbar::Never => false,
- }
- }
-
- fn should_autohide_scrollbar(cx: &App) -> bool {
- let show = OutlinePanelSettings::get_global(cx)
- .scrollbar
- .show
- .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
- match show {
- ShowScrollbar::Auto => true,
- ShowScrollbar::System => cx
- .try_global::<ScrollbarAutoHide>()
- .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
- ShowScrollbar::Always => false,
- ShowScrollbar::Never => true,
- }
- }
-
- fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
- if !Self::should_autohide_scrollbar(cx) {
- return;
- }
- self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
- cx.background_executor()
- .timer(SCROLLBAR_SHOW_INTERVAL)
- .await;
- panel
- .update(cx, |panel, cx| {
- panel.show_scrollbar = false;
- cx.notify();
- })
- .log_err();
- }))
- }
-
fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
let item_text_chars = match entry {
PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
@@ -4688,7 +4528,7 @@ impl OutlinePanel {
indent_size: f32,
window: &mut Window,
cx: &mut Context<Self>,
- ) -> Div {
+ ) -> impl IntoElement {
let contents = if self.cached_entries.is_empty() {
let header = if self.updating_fs_entries || self.updating_cached_entries {
None
@@ -4699,6 +4539,7 @@ impl OutlinePanel {
};
v_flex()
+ .id("empty-outline-state")
.flex_1()
.justify_center()
.size_full()
@@ -4848,10 +4689,13 @@ impl OutlinePanel {
.flex_shrink()
.size_full()
.child(list_contents.size_full().flex_shrink())
- .children(self.render_vertical_scrollbar(cx))
- .when_some(
- self.render_horizontal_scrollbar(window, cx),
- |this, scrollbar| this.pb_4().child(scrollbar),
+ .custom_scrollbars(
+ Scrollbars::for_settings::<OutlinePanelSettings>()
+ .tracked_scroll_handle(self.scroll_handle.clone())
+ .with_track_along(ScrollAxes::Horizontal)
+ .tracked_entity(cx.entity_id()),
+ window,
+ cx,
)
}
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
@@ -5119,15 +4963,6 @@ impl Render for OutlinePanel {
.size_full()
.overflow_hidden()
.relative()
- .on_hover(cx.listener(|this, hovered, window, cx| {
- if *hovered {
- this.show_scrollbar = true;
- this.hide_scrollbar_task.take();
- cx.notify();
- } else if !this.focus_handle.contains_focused(window, cx) {
- this.hide_scrollbar(window, cx);
- }
- }))
.key_context(self.dispatch_context(window, cx))
.on_action(cx.listener(Self::open_selected_entry))
.on_action(cx.listener(Self::cancel))
@@ -1,8 +1,9 @@
-use editor::ShowScrollbar;
-use gpui::Pixels;
+use editor::EditorSettings;
+use gpui::{App, Pixels};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
@@ -116,6 +117,14 @@ pub struct OutlinePanelSettingsContent {
pub expand_outlines_with_depth: Option<usize>,
}
+impl ScrollbarVisibility for OutlinePanelSettings {
+ fn visibility(&self, cx: &App) -> ShowScrollbar {
+ self.scrollbar
+ .show
+ .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
+ }
+}
+
impl Settings for OutlinePanelSettings {
type FileContent = OutlinePanelSettingsContent;
@@ -23,7 +23,6 @@ menu.workspace = true
schemars.workspace = true
serde.workspace = true
ui.workspace = true
-util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
@@ -11,17 +11,17 @@ use editor::{
use gpui::{
Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render,
- ScrollStrategy, Stateful, Task, UniformListScrollHandle, Window, actions, div, list,
- prelude::*, uniform_list,
+ ScrollStrategy, Task, UniformListScrollHandle, Window, actions, div, list, prelude::*,
+ uniform_list,
};
use head::Head;
use schemars::JsonSchema;
use serde::Deserialize;
use std::{ops::Range, sync::Arc, time::Duration};
use ui::{
- Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex,
+ Color, Divider, Label, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar,
+ prelude::*, v_flex,
};
-use util::ResultExt;
use workspace::ModalView;
enum ElementContainer {
@@ -65,13 +65,8 @@ pub struct Picker<D: PickerDelegate> {
width: Option<Length>,
widest_item: Option<usize>,
max_height: Option<Length>,
- focus_handle: FocusHandle,
/// An external control to display a scrollbar in the `Picker`.
show_scrollbar: bool,
- /// An internal state that controls whether to show the scrollbar based on the user's focus.
- scrollbar_visibility: bool,
- scrollbar_state: ScrollbarState,
- hide_scrollbar_task: Option<Task<()>>,
/// Whether the `Picker` is rendered as a self-contained modal.
///
/// Set this to `false` when rendering the `Picker` as part of a larger modal.
@@ -293,13 +288,6 @@ impl<D: PickerDelegate> Picker<D> {
cx: &mut Context<Self>,
) -> Self {
let element_container = Self::create_element_container(container);
- let scrollbar_state = match &element_container {
- ElementContainer::UniformList(scroll_handle) => {
- ScrollbarState::new(scroll_handle.clone())
- }
- ElementContainer::List(state) => ScrollbarState::new(state.clone()),
- };
- let focus_handle = cx.focus_handle();
let mut this = Self {
delegate,
head,
@@ -309,12 +297,8 @@ impl<D: PickerDelegate> Picker<D> {
width: None,
widest_item: None,
max_height: Some(rems(18.).into()),
- focus_handle,
show_scrollbar: false,
- scrollbar_visibility: true,
- scrollbar_state,
is_modal: true,
- hide_scrollbar_task: None,
};
this.update_matches("".to_string(), window, cx);
// give the delegate 4ms to render the first set of suggestions.
@@ -790,67 +774,6 @@ impl<D: PickerDelegate> Picker<D> {
}
}
}
-
- fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
- const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
- self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
- cx.background_executor()
- .timer(SCROLLBAR_SHOW_INTERVAL)
- .await;
- panel
- .update(cx, |panel, cx| {
- panel.scrollbar_visibility = false;
- cx.notify();
- })
- .log_err();
- }))
- }
-
- fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !self.show_scrollbar
- || !(self.scrollbar_visibility || self.scrollbar_state.is_dragging())
- {
- return None;
- }
- Some(
- div()
- .occlude()
- .id("picker-scroll")
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_0()
- .w(px(12.))
- .cursor_default()
- .on_mouse_move(cx.listener(|_, _, _window, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|picker, _, window, cx| {
- if !picker.scrollbar_state.is_dragging()
- && !picker.focus_handle.contains_focused(window, cx)
- {
- picker.hide_scrollbar(cx);
- cx.notify();
- }
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
- cx.notify();
- }))
- .children(Scrollbar::vertical(self.scrollbar_state.clone())),
- )
- }
}
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
@@ -900,17 +823,22 @@ impl<D: PickerDelegate> Render for Picker<D> {
.overflow_hidden()
.children(self.delegate.render_header(window, cx))
.child(self.render_element_container(cx))
- .on_hover(cx.listener(|this, hovered, window, cx| {
- if *hovered {
- this.scrollbar_visibility = true;
- this.hide_scrollbar_task.take();
- cx.notify();
- } else if !this.focus_handle.contains_focused(window, cx) {
- this.hide_scrollbar(cx);
- }
- }))
- .when_some(self.render_scrollbar(cx), |div, scrollbar| {
- div.child(scrollbar)
+ .when(self.show_scrollbar, |this| {
+ let base_scrollbar_config =
+ Scrollbars::new(ScrollAxes::Vertical).width_sm();
+
+ this.map(|this| match &self.element_container {
+ ElementContainer::List(state) => this.custom_scrollbars(
+ base_scrollbar_config.tracked_scroll_handle(state.clone()),
+ window,
+ cx,
+ ),
+ ElementContainer::UniformList(state) => this.custom_scrollbars(
+ base_scrollbar_config.tracked_scroll_handle(state.clone()),
+ window,
+ cx,
+ ),
+ })
}),
)
})
@@ -7,12 +7,11 @@ use collections::{BTreeSet, HashMap, hash_map};
use command_palette_hooks::CommandPaletteFilter;
use db::kvp::KEY_VALUE_STORE;
use editor::{
- Editor, EditorEvent, EditorSettings, ShowScrollbar,
+ Editor, EditorEvent,
items::{
entry_diagnostic_aware_icon_decoration_and_color,
entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
},
- scroll::ScrollbarAutoHide,
};
use file_icons::FileIcons;
use git::status::GitSummary;
@@ -59,7 +58,8 @@ use theme::ThemeSettings;
use ui::{
Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind,
IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing,
- ScrollableHandle, Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex,
+ ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*,
+ v_flex,
};
use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
use workspace::{
@@ -109,10 +109,6 @@ pub struct ProjectPanel {
workspace: WeakEntity<Workspace>,
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
- show_scrollbar: bool,
- vertical_scrollbar_state: ScrollbarState,
- horizontal_scrollbar_state: ScrollbarState,
- hide_scrollbar_task: Option<Task<()>>,
diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
max_width_item_index: Option<usize>,
diagnostic_summary_update: Task<()>,
@@ -461,7 +457,6 @@ impl ProjectPanel {
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.focus_out(window, cx);
- this.hide_scrollbar(window, cx);
})
.detach();
@@ -652,12 +647,6 @@ impl ProjectPanel {
workspace: workspace.weak_handle(),
width: None,
pending_serialization: Task::ready(None),
- show_scrollbar: !Self::should_autohide_scrollbar(cx),
- hide_scrollbar_task: None,
- vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
- .parent_entity(&cx.entity()),
- horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
- .parent_entity(&cx.entity()),
max_width_item_index: None,
diagnostics: Default::default(),
diagnostic_summary_update: Task::ready(()),
@@ -4847,103 +4836,6 @@ impl ProjectPanel {
}
}
- fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !Self::should_show_scrollbar(cx)
- || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
- {
- return None;
- }
- Some(
- div()
- .occlude()
- .id("project-panel-vertical-scroll")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|this, _, window, cx| {
- if !this.vertical_scrollbar_state.is_dragging()
- && !this.focus_handle.contains_focused(window, cx)
- {
- this.hide_scrollbar(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .h_full()
- .absolute()
- .right_1()
- .top_1()
- .bottom_1()
- .w(px(12.))
- .cursor_default()
- .children(Scrollbar::vertical(
- // percentage as f32..end_offset as f32,
- self.vertical_scrollbar_state.clone(),
- )),
- )
- }
-
- fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !Self::should_show_scrollbar(cx)
- || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
- {
- return None;
- }
- Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
- div()
- .occlude()
- .id("project-panel-horizontal-scroll")
- .on_mouse_move(cx.listener(|_, _, _, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|this, _, window, cx| {
- if !this.horizontal_scrollbar_state.is_dragging()
- && !this.focus_handle.contains_focused(window, cx)
- {
- this.hide_scrollbar(window, cx);
- cx.notify();
- }
-
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _, cx| {
- cx.notify();
- }))
- .w_full()
- .absolute()
- .right_1()
- .left_1()
- .bottom_1()
- .h(px(12.))
- .cursor_default()
- .child(scrollbar)
- })
- }
-
fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("ProjectPanel");
@@ -4959,52 +4851,6 @@ impl ProjectPanel {
dispatch_context
}
- fn should_show_scrollbar(cx: &App) -> bool {
- let show = ProjectPanelSettings::get_global(cx)
- .scrollbar
- .show
- .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
- match show {
- ShowScrollbar::Auto => true,
- ShowScrollbar::System => true,
- ShowScrollbar::Always => true,
- ShowScrollbar::Never => false,
- }
- }
-
- fn should_autohide_scrollbar(cx: &App) -> bool {
- let show = ProjectPanelSettings::get_global(cx)
- .scrollbar
- .show
- .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
- match show {
- ShowScrollbar::Auto => true,
- ShowScrollbar::System => cx
- .try_global::<ScrollbarAutoHide>()
- .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
- ShowScrollbar::Always => false,
- ShowScrollbar::Never => true,
- }
- }
-
- fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
- if !Self::should_autohide_scrollbar(cx) {
- return;
- }
- self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
- cx.background_executor()
- .timer(SCROLLBAR_SHOW_INTERVAL)
- .await;
- panel
- .update(cx, |panel, cx| {
- panel.show_scrollbar = false;
- cx.notify();
- })
- .log_err();
- }))
- }
-
fn reveal_entry(
&mut self,
project: Entity<Project>,
@@ -5357,15 +5203,6 @@ impl Render for ProjectPanel {
this.refresh_drag_cursor_style(&event.modifiers, window, cx);
},
))
- .on_hover(cx.listener(|this, hovered, window, cx| {
- if *hovered {
- this.show_scrollbar = true;
- this.hide_scrollbar_task.take();
- cx.notify();
- } else if !this.focus_handle.contains_focused(window, cx) {
- this.hide_scrollbar(window, cx);
- }
- }))
.on_click(cx.listener(|this, event, _, cx| {
if matches!(event, gpui::ClickEvent::Keyboard(_)) {
return;
@@ -5755,10 +5592,14 @@ impl Render for ProjectPanel {
)
.size_full(),
)
- .children(self.render_vertical_scrollbar(cx))
- .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
- this.pb_4().child(scrollbar)
- })
+ .custom_scrollbars(
+ Scrollbars::for_settings::<ProjectPanelSettings>()
+ .tracked_scroll_handle(self.scroll_handle.clone())
+ .with_track_along(ScrollAxes::Horizontal)
+ .notify_content(),
+ window,
+ cx,
+ )
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()
@@ -1,8 +1,9 @@
-use editor::ShowScrollbar;
+use editor::EditorSettings;
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
@@ -168,6 +169,14 @@ pub struct ProjectPanelSettingsContent {
pub drag_and_drop: Option<bool>,
}
+impl ScrollbarVisibility for ProjectPanelSettings {
+ fn visibility(&self, cx: &ui::App) -> ShowScrollbar {
+ self.scrollbar
+ .show
+ .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
+ }
+}
+
impl Settings for ProjectPanelSettings {
type FileContent = ProjectPanelSettingsContent;
@@ -23,7 +23,6 @@ use remote::{
use settings::{Settings, SettingsStore, update_settings_file, watch_config_file};
use smol::stream::StreamExt as _;
use std::{
- any::Any,
borrow::Cow,
collections::BTreeSet,
path::PathBuf,
@@ -35,7 +34,7 @@ use std::{
};
use ui::{
IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Navigable, NavigableEntry,
- Scrollbar, ScrollbarState, Section, Tooltip, prelude::*,
+ Section, Tooltip, WithScrollbar, prelude::*,
};
use util::{
ResultExt,
@@ -284,7 +283,7 @@ impl RemoteEntry {
#[derive(Clone)]
struct DefaultState {
- scrollbar: ScrollbarState,
+ scroll_handle: ScrollHandle,
add_new_server: NavigableEntry,
servers: Vec<RemoteEntry>,
}
@@ -292,7 +291,6 @@ struct DefaultState {
impl DefaultState {
fn new(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self {
let handle = ScrollHandle::new();
- let scrollbar = ScrollbarState::new(handle.clone());
let add_new_server = NavigableEntry::new(&handle, cx);
let ssh_settings = SshSettings::get_global(cx);
@@ -333,7 +331,7 @@ impl DefaultState {
}
Self {
- scrollbar,
+ scroll_handle: handle,
add_new_server,
servers,
}
@@ -1449,7 +1447,6 @@ impl RemoteServerProjects {
}
}
- let scroll_state = state.scrollbar.parent_entity(&cx.entity());
let connect_button = div()
.id("ssh-connect-new-server-container")
.track_focus(&state.add_new_server.focus_handle)
@@ -1480,17 +1477,12 @@ impl RemoteServerProjects {
cx.notify();
}));
- let handle = &**scroll_state.scroll_handle() as &dyn Any;
- let Some(scroll_handle) = handle.downcast_ref::<ScrollHandle>() else {
- unreachable!()
- };
-
let mut modal_section = Navigable::new(
v_flex()
.track_focus(&self.focus_handle(cx))
.id("ssh-server-list")
.overflow_y_scroll()
- .track_scroll(scroll_handle)
+ .track_scroll(&state.scroll_handle)
.size_full()
.child(connect_button)
.child(
@@ -1585,17 +1577,7 @@ impl RemoteServerProjects {
)
.size_full(),
)
- .child(
- div()
- .occlude()
- .h_full()
- .absolute()
- .top_1()
- .bottom_1()
- .right_1()
- .w(px(8.))
- .children(Scrollbar::vertical(scroll_state)),
- ),
+ .vertical_scrollbar_for(state.scroll_handle, window, cx),
),
)
.into_any_element()
@@ -7,12 +7,11 @@ mod terminal_slash_command;
pub mod terminal_tab_tooltip;
use assistant_slash_command::SlashCommandRegistry;
-use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
+use editor::{EditorSettings, actions::SelectAll};
use gpui::{
Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
- ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored,
- deferred, div,
+ ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
};
use persistence::TERMINAL_DB;
use project::{Project, search::SearchQuery};
@@ -35,7 +34,9 @@ use terminal_scrollbar::TerminalScrollHandle;
use terminal_slash_command::TerminalSlashCommand;
use terminal_tab_tooltip::TerminalTooltip;
use ui::{
- ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*,
+ ContextMenu, Icon, IconName, Label, ScrollAxes, Scrollbars, Tooltip, WithScrollbar, h_flex,
+ prelude::*,
+ scrollbars::{self, GlobalSetting, ScrollbarVisibility},
};
use util::ResultExt;
use workspace::{
@@ -68,7 +69,6 @@ struct ImeState {
}
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
-const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.);
/// Event to transmit the scroll from the element to the view
#[derive(Clone, Debug, PartialEq)]
@@ -139,10 +139,7 @@ pub struct TerminalView {
show_breadcrumbs: bool,
block_below_cursor: Option<Rc<BlockProperties>>,
scroll_top: Pixels,
- scrollbar_state: ScrollbarState,
scroll_handle: TerminalScrollHandle,
- show_scrollbar: bool,
- hide_scrollbar_task: Option<Task<()>>,
ime_state: Option<ImeState>,
_subscriptions: Vec<Subscription>,
_terminal_subscriptions: Vec<Subscription>,
@@ -262,10 +259,7 @@ impl TerminalView {
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
block_below_cursor: None,
scroll_top: Pixels::ZERO,
- scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
- show_scrollbar: !Self::should_autohide_scrollbar(cx),
- hide_scrollbar_task: None,
cwd_serialized: false,
ime_state: None,
_subscriptions: vec![
@@ -836,136 +830,6 @@ impl TerminalView {
self.terminal = terminal;
}
- // Hack: Using editor in terminal causes cyclic dependency i.e. editor -> terminal -> project -> editor.
- fn map_show_scrollbar_from_editor_to_terminal(
- show_scrollbar: editor::ShowScrollbar,
- ) -> terminal_settings::ShowScrollbar {
- match show_scrollbar {
- editor::ShowScrollbar::Auto => terminal_settings::ShowScrollbar::Auto,
- editor::ShowScrollbar::System => terminal_settings::ShowScrollbar::System,
- editor::ShowScrollbar::Always => terminal_settings::ShowScrollbar::Always,
- editor::ShowScrollbar::Never => terminal_settings::ShowScrollbar::Never,
- }
- }
-
- fn should_show_scrollbar(cx: &App) -> bool {
- let show = TerminalSettings::get_global(cx)
- .scrollbar
- .show
- .unwrap_or_else(|| {
- Self::map_show_scrollbar_from_editor_to_terminal(
- EditorSettings::get_global(cx).scrollbar.show,
- )
- });
- match show {
- terminal_settings::ShowScrollbar::Auto => true,
- terminal_settings::ShowScrollbar::System => true,
- terminal_settings::ShowScrollbar::Always => true,
- terminal_settings::ShowScrollbar::Never => false,
- }
- }
-
- fn should_autohide_scrollbar(cx: &App) -> bool {
- let show = TerminalSettings::get_global(cx)
- .scrollbar
- .show
- .unwrap_or_else(|| {
- Self::map_show_scrollbar_from_editor_to_terminal(
- EditorSettings::get_global(cx).scrollbar.show,
- )
- });
- match show {
- terminal_settings::ShowScrollbar::Auto => true,
- terminal_settings::ShowScrollbar::System => cx
- .try_global::<ScrollbarAutoHide>()
- .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
- terminal_settings::ShowScrollbar::Always => false,
- terminal_settings::ShowScrollbar::Never => true,
- }
- }
-
- fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
- const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
- if !Self::should_autohide_scrollbar(cx) {
- return;
- }
- self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
- cx.background_executor()
- .timer(SCROLLBAR_SHOW_INTERVAL)
- .await;
- panel
- .update(cx, |panel, cx| {
- panel.show_scrollbar = false;
- cx.notify();
- })
- .log_err();
- }))
- }
-
- fn render_scrollbar(&self, window: &Window, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
- if !Self::should_show_scrollbar(cx)
- || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
- || !self.content_mode(window, cx).is_scrollable()
- {
- return None;
- }
-
- if self.terminal.read(cx).total_lines() == self.terminal.read(cx).viewport_lines() {
- return None;
- }
-
- self.scroll_handle.update(self.terminal.read(cx));
-
- if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() {
- self.terminal.update(cx, |term, _| {
- let delta = new_display_offset as i32 - term.last_content.display_offset as i32;
- match delta.cmp(&0) {
- std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize),
- std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize),
- std::cmp::Ordering::Equal => {}
- }
- });
- }
-
- Some(
- div()
- .occlude()
- .id("terminal-view-scroll")
- .on_mouse_move(cx.listener(|_, _, _window, cx| {
- cx.notify();
- cx.stop_propagation()
- }))
- .on_hover(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_any_mouse_down(|_, _window, cx| {
- cx.stop_propagation();
- })
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(|terminal_view, _, window, cx| {
- if !terminal_view.scrollbar_state.is_dragging()
- && !terminal_view.focus_handle.contains_focused(window, cx)
- {
- terminal_view.hide_scrollbar(cx);
- cx.notify();
- }
- cx.stop_propagation();
- }),
- )
- .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
- cx.notify();
- }))
- .absolute()
- .top_0()
- .bottom_0()
- .right_0()
- .h_full()
- .w(TERMINAL_SCROLLBAR_WIDTH)
- .children(Scrollbar::vertical(self.scrollbar_state.clone())),
- )
- }
-
fn rerun_button(task: &TaskState) -> Option<IconButton> {
if !task.show_rerun {
return None;
@@ -1120,6 +984,29 @@ fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexS
}
}
+struct TerminalScrollbarSettingsWrapper;
+
+impl GlobalSetting for TerminalScrollbarSettingsWrapper {
+ fn get_value(_cx: &App) -> &Self {
+ &Self
+ }
+}
+
+impl ScrollbarVisibility for TerminalScrollbarSettingsWrapper {
+ fn visibility(&self, cx: &App) -> scrollbars::ShowScrollbar {
+ TerminalSettings::get_global(cx)
+ .scrollbar
+ .show
+ .map(|value| match value {
+ terminal_settings::ShowScrollbar::Auto => scrollbars::ShowScrollbar::Auto,
+ terminal_settings::ShowScrollbar::System => scrollbars::ShowScrollbar::System,
+ terminal_settings::ShowScrollbar::Always => scrollbars::ShowScrollbar::Always,
+ terminal_settings::ShowScrollbar::Never => scrollbars::ShowScrollbar::Never,
+ })
+ .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
+ }
+}
+
impl TerminalView {
fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
self.clear_bell(cx);
@@ -1151,28 +1038,31 @@ impl TerminalView {
terminal.focus_out();
terminal.set_cursor_shape(CursorShape::Hollow);
});
- self.hide_scrollbar(cx);
cx.notify();
}
}
impl Render for TerminalView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ // TODO: this should be moved out of render
+ self.scroll_handle.update(self.terminal.read(cx));
+
+ if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() {
+ self.terminal.update(cx, |term, _| {
+ let delta = new_display_offset as i32 - term.last_content.display_offset as i32;
+ match delta.cmp(&0) {
+ std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize),
+ std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize),
+ std::cmp::Ordering::Equal => {}
+ }
+ });
+ }
+
let terminal_handle = self.terminal.clone();
let terminal_view_handle = cx.entity();
let focused = self.focus_handle.is_focused(window);
- // Always calculate scrollbar width to prevent layout shift
- let scrollbar_width = if Self::should_show_scrollbar(cx)
- && self.content_mode(window, cx).is_scrollable()
- && self.terminal.read(cx).total_lines() > self.terminal.read(cx).viewport_lines()
- {
- TERMINAL_SCROLLBAR_WIDTH
- } else {
- px(0.)
- };
-
div()
.id("terminal-view")
.size_full()
@@ -1209,21 +1099,12 @@ impl Render for TerminalView {
}
}),
)
- .on_hover(cx.listener(|this, hovered, window, cx| {
- if *hovered {
- this.show_scrollbar = true;
- this.hide_scrollbar_task.take();
- cx.notify();
- } else if !this.focus_handle.contains_focused(window, cx) {
- this.hide_scrollbar(cx);
- }
- }))
.child(
// TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
div()
+ .id("terminal-view-container")
.size_full()
.bg(cx.theme().colors().editor_background)
- .when(scrollbar_width > px(0.), |div| div.pr(scrollbar_width))
.child(TerminalElement::new(
terminal_handle,
terminal_view_handle,
@@ -1234,8 +1115,15 @@ impl Render for TerminalView {
self.block_below_cursor.clone(),
self.mode.clone(),
))
- .when_some(self.render_scrollbar(window, cx), |div, scrollbar| {
- div.child(scrollbar)
+ .when(self.content_mode(window, cx).is_scrollable(), |div| {
+ div.custom_scrollbars(
+ Scrollbars::for_settings::<TerminalScrollbarSettingsWrapper>()
+ .show_along(ScrollAxes::Vertical)
+ .with_track_along(ScrollAxes::Vertical)
+ .tracked_scroll_handle(self.scroll_handle.clone()),
+ window,
+ cx,
+ )
}),
)
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
@@ -21,6 +21,7 @@ gpui_macros.workspace = true
icons.workspace = true
itertools.workspace = true
menu.workspace = true
+schemars.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true
@@ -1,39 +1,783 @@
-use std::{
- any::Any,
- cell::{Cell, RefCell},
- fmt::Debug,
- ops::Range,
- rc::Rc,
- sync::Arc,
- time::Duration,
-};
+use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Not, time::Duration};
-use crate::{IntoElement, prelude::*, px, relative};
use gpui::{
- Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle,
- Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
- IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
- Point, ScrollHandle, ScrollWheelEvent, Size, Style, Task, UniformListScrollHandle, Window,
- quad,
+ Along, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Context,
+ Corner, Corners, CursorStyle, Div, Edges, Element, ElementId, Entity, EntityId,
+ GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
+ LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Negate,
+ ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful,
+ StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration,
+ UniformListScrollHandle, Window, prelude::FluentBuilder as _, px, quad, relative, size,
};
+use settings::SettingsStore;
+use smallvec::SmallVec;
+use theme::ActiveTheme as _;
+use util::ResultExt;
+
+use std::ops::Range;
+
+use crate::scrollbars::{ScrollbarVisibility, ShowScrollbar};
+
+const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_millis(1500);
+const SCROLLBAR_PADDING: Pixels = px(4.);
+
+pub mod scrollbars {
+ use gpui::{App, Global};
+ use schemars::JsonSchema;
+ use serde::{Deserialize, Serialize};
+ use settings::Settings;
+
+ /// When to show the scrollbar in the editor.
+ ///
+ /// Default: auto
+ #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+ #[serde(rename_all = "snake_case")]
+ pub enum ShowScrollbar {
+ /// Show the scrollbar if there's important information or
+ /// follow the system's configured behavior.
+ Auto,
+ /// Match the system's configured behavior.
+ System,
+ /// Always show the scrollbar.
+ Always,
+ /// Never show the scrollbar.
+ Never,
+ }
+
+ impl ShowScrollbar {
+ pub(super) fn show(&self) -> bool {
+ *self != Self::Never
+ }
+
+ pub(super) fn should_auto_hide(&self, cx: &mut App) -> bool {
+ match self {
+ Self::Auto => true,
+ Self::System => cx.default_global::<ScrollbarAutoHide>().should_hide(),
+ _ => false,
+ }
+ }
+ }
+
+ pub trait GlobalSetting {
+ fn get_value(cx: &App) -> &Self;
+ }
+
+ impl<T: Settings> GlobalSetting for T {
+ fn get_value(cx: &App) -> &T {
+ T::get_global(cx)
+ }
+ }
+
+ impl GlobalSetting for ShowScrollbar {
+ fn get_value(_cx: &App) -> &Self {
+ &ShowScrollbar::Always
+ }
+ }
+
+ pub trait ScrollbarVisibility: GlobalSetting + 'static {
+ fn visibility(&self, cx: &App) -> ShowScrollbar;
+ }
+
+ impl ScrollbarVisibility for ShowScrollbar {
+ fn visibility(&self, cx: &App) -> ShowScrollbar {
+ *ShowScrollbar::get_value(cx)
+ }
+ }
+
+ #[derive(Default)]
+ pub struct ScrollbarAutoHide(pub bool);
+
+ impl ScrollbarAutoHide {
+ pub fn should_hide(&self) -> bool {
+ self.0
+ }
+ }
+
+ impl Global for ScrollbarAutoHide {}
+}
+
+fn get_scrollbar_state<S, T>(
+ mut config: Scrollbars<S, T>,
+ caller_location: &'static std::panic::Location,
+ window: &mut Window,
+ cx: &mut App,
+) -> Entity<ScrollbarStateWrapper<S, T>>
+where
+ S: ScrollbarVisibility,
+ T: ScrollableHandle,
+{
+ let element_id = config.id.take().unwrap_or_else(|| caller_location.into());
+
+ window.use_keyed_state(element_id, cx, |window, cx| {
+ let parent_id = cx.entity_id();
+ ScrollbarStateWrapper(
+ cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, window, cx)),
+ )
+ })
+}
+
+pub trait WithScrollbar: Sized {
+ type Output;
+
+ fn custom_scrollbars<S, T>(
+ self,
+ config: Scrollbars<S, T>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self::Output
+ where
+ S: ScrollbarVisibility,
+ T: ScrollableHandle;
+
+ #[track_caller]
+ fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
+ self.custom_scrollbars(
+ Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()),
+ window,
+ cx,
+ )
+ }
+
+ #[track_caller]
+ fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
+ self.custom_scrollbars(
+ Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()),
+ window,
+ cx,
+ )
+ }
+
+ #[track_caller]
+ fn vertical_scrollbar_for<ScrollHandle: ScrollableHandle>(
+ self,
+ scroll_handle: ScrollHandle,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self::Output {
+ self.custom_scrollbars(
+ Scrollbars::new(ScrollAxes::Vertical)
+ .tracked_scroll_handle(scroll_handle)
+ .ensure_id(core::panic::Location::caller()),
+ window,
+ cx,
+ )
+ }
+}
+
+impl WithScrollbar for Stateful<Div> {
+ type Output = Self;
+
+ #[track_caller]
+ fn custom_scrollbars<S, T>(
+ self,
+ config: Scrollbars<S, T>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self::Output
+ where
+ S: ScrollbarVisibility,
+ T: ScrollableHandle,
+ {
+ render_scrollbar(
+ get_scrollbar_state(config, std::panic::Location::caller(), window, cx),
+ self,
+ cx,
+ )
+ }
+}
+
+impl WithScrollbar for Div {
+ type Output = Stateful<Div>;
+
+ #[track_caller]
+ fn custom_scrollbars<S, T>(
+ self,
+ config: Scrollbars<S, T>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self::Output
+ where
+ S: ScrollbarVisibility,
+ T: ScrollableHandle,
+ {
+ let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
+ // We know this ID stays consistent as long as the element is rendered for
+ // consecutive frames, which is sufficient for our use case here
+ let scrollbar_entity_id = scrollbar.entity_id();
+
+ render_scrollbar(
+ scrollbar,
+ self.id(("track-scroll", scrollbar_entity_id)),
+ cx,
+ )
+ }
+}
+
+fn render_scrollbar<S, T>(
+ scrollbar: Entity<ScrollbarStateWrapper<S, T>>,
+ div: Stateful<Div>,
+ cx: &App,
+) -> Stateful<Div>
+where
+ S: ScrollbarVisibility,
+ T: ScrollableHandle,
+{
+ let state = &scrollbar.read(cx).0;
+
+ div.when_some(state.read(cx).handle_to_track(), |this, handle| {
+ this.track_scroll(handle).when_some(
+ state.read(cx).visible_axes(),
+ |this, axes| match axes {
+ ScrollAxes::Horizontal => this.overflow_x_scroll(),
+ ScrollAxes::Vertical => this.overflow_y_scroll(),
+ ScrollAxes::Both => this.overflow_scroll(),
+ },
+ )
+ })
+ .when_some(
+ state
+ .read(cx)
+ .space_to_reserve_for(ScrollbarAxis::Horizontal),
+ |this, space| this.pb(space),
+ )
+ .when_some(
+ state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical),
+ |this, space| this.pr(space),
+ )
+ .child(state.clone())
+}
+
+impl<S: ScrollbarVisibility, T: ScrollableHandle> UniformListDecoration
+ for ScrollbarStateWrapper<S, T>
+{
+ fn compute(
+ &self,
+ _visible_range: Range<usize>,
+ _bounds: Bounds<Pixels>,
+ scroll_offset: Point<Pixels>,
+ _item_height: Pixels,
+ _item_count: usize,
+ _window: &mut Window,
+ _cx: &mut App,
+ ) -> gpui::AnyElement {
+ ScrollbarElement {
+ origin: scroll_offset.negate(),
+ state: self.0.clone(),
+ }
+ .into_any()
+ }
+}
+
+// impl WithScrollbar for UniformList {
+// type Output = Self;
+
+// #[track_caller]
+// fn custom_scrollbars<S, T>(
+// self,
+// config: Scrollbars<S, T>,
+// window: &mut Window,
+// cx: &mut App,
+// ) -> Self::Output
+// where
+// S: ScrollbarVisibilitySetting,
+// T: ScrollableHandle,
+// {
+// let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
+// self.when_some(
+// scrollbar.read_with(cx, |wrapper, cx| {
+// wrapper
+// .0
+// .read(cx)
+// .handle_to_track::<UniformListScrollHandle>()
+// .cloned()
+// }),
+// |this, handle| this.track_scroll(handle),
+// )
+// .with_decoration(scrollbar)
+// }
+// }
+
+pub enum ScrollAxes {
+ Horizontal,
+ Vertical,
+ Both,
+}
+
+impl ScrollAxes {
+ fn apply_to<T>(self, point: Point<T>, value: T) -> Point<T>
+ where
+ T: Debug + Default + PartialEq + Clone,
+ {
+ match self {
+ Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value),
+ Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value),
+ Self::Both => Point::new(value.clone(), value),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+enum ReservedSpace {
+ #[default]
+ None,
+ Thumb,
+ Track,
+}
+
+impl ReservedSpace {
+ fn is_visible(&self) -> bool {
+ *self != ReservedSpace::None
+ }
+
+ fn needs_scroll_track(&self) -> bool {
+ *self == ReservedSpace::Track
+ }
+}
+
+#[derive(Debug, Default)]
+enum ScrollbarWidth {
+ #[default]
+ Normal,
+ Small,
+ XSmall,
+}
+
+impl ScrollbarWidth {
+ fn to_pixels(&self) -> Pixels {
+ match self {
+ ScrollbarWidth::Normal => px(8.),
+ ScrollbarWidth::Small => px(6.),
+ ScrollbarWidth::XSmall => px(4.),
+ }
+ }
+}
+
+pub struct Scrollbars<S: ScrollbarVisibility = ShowScrollbar, T: ScrollableHandle = ScrollHandle> {
+ id: Option<ElementId>,
+ tracked_setting: PhantomData<S>,
+ tracked_entity: Option<Option<EntityId>>,
+ scrollable_handle: Box<dyn FnOnce() -> T>,
+ handle_was_added: bool,
+ visibility: Point<ReservedSpace>,
+ scrollbar_width: ScrollbarWidth,
+}
+
+impl Scrollbars {
+ pub fn new(show_along: ScrollAxes) -> Self {
+ Self::new_with_setting(show_along)
+ }
+
+ pub fn for_settings<S: ScrollbarVisibility>() -> Scrollbars<S> {
+ Scrollbars::<S>::new_with_setting(ScrollAxes::Both)
+ }
+}
+
+impl<S: ScrollbarVisibility> Scrollbars<S> {
+ fn new_with_setting(show_along: ScrollAxes) -> Self {
+ Self {
+ id: None,
+ tracked_setting: PhantomData,
+ handle_was_added: false,
+ scrollable_handle: Box::new(ScrollHandle::new),
+ tracked_entity: None,
+ visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb),
+ scrollbar_width: ScrollbarWidth::Normal,
+ }
+ }
+}
+
+impl<Setting: ScrollbarVisibility, ScrollHandle: ScrollableHandle>
+ Scrollbars<Setting, ScrollHandle>
+{
+ pub fn id(mut self, id: impl Into<ElementId>) -> Self {
+ self.id = Some(id.into());
+ self
+ }
+
+ fn ensure_id(mut self, id: impl Into<ElementId>) -> Self {
+ if self.id.is_none() {
+ self.id = Some(id.into());
+ }
+ self
+ }
+
+ /// Notify the current context whenever this scrollbar gets a scroll event
+ pub fn notify_content(mut self) -> Self {
+ self.tracked_entity = Some(None);
+ self
+ }
+
+ /// Set a parent model which should be notified whenever this scrollbar gets a scroll event.
+ pub fn tracked_entity(mut self, entity_id: EntityId) -> Self {
+ self.tracked_entity = Some(Some(entity_id));
+ self
+ }
+
+ pub fn tracked_scroll_handle<TrackedHandle: ScrollableHandle>(
+ self,
+ tracked_scroll_handle: TrackedHandle,
+ ) -> Scrollbars<Setting, TrackedHandle> {
+ let Self {
+ id,
+ tracked_setting,
+ tracked_entity: tracked_entity_id,
+ scrollbar_width,
+ visibility,
+ ..
+ } = self;
+
+ Scrollbars {
+ handle_was_added: true,
+ scrollable_handle: Box::new(|| tracked_scroll_handle),
+ id,
+ tracked_setting,
+ tracked_entity: tracked_entity_id,
+ visibility,
+ scrollbar_width,
+ }
+ }
+
+ pub fn show_along(mut self, along: ScrollAxes) -> Self {
+ self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb);
+ self
+ }
+
+ pub fn with_track_along(mut self, along: ScrollAxes) -> Self {
+ self.visibility = along.apply_to(self.visibility, ReservedSpace::Track);
+ self
+ }
+
+ pub fn width_sm(mut self) -> Self {
+ self.scrollbar_width = ScrollbarWidth::Small;
+ self
+ }
+
+ pub fn width_xs(mut self) -> Self {
+ self.scrollbar_width = ScrollbarWidth::XSmall;
+ self
+ }
+}
+
+#[derive(PartialEq, Eq)]
+enum VisibilityState {
+ Visible,
+ Hidden,
+ Disabled,
+}
+
+impl VisibilityState {
+ fn from_show_setting(show_setting: ShowScrollbar) -> Self {
+ if show_setting.show() {
+ Self::Visible
+ } else {
+ Self::Disabled
+ }
+ }
+
+ fn is_visible(&self) -> bool {
+ *self == VisibilityState::Visible
+ }
+
+ #[inline]
+ fn is_disabled(&self) -> bool {
+ *self == VisibilityState::Disabled
+ }
+}
+
+enum ParentHovered {
+ Yes(bool),
+ No(bool),
+}
+
+/// This is used to ensure notifies within the state do not notify the parent
+/// unintentionally.
+struct ScrollbarStateWrapper<S: ScrollbarVisibility, T: ScrollableHandle>(
+ Entity<ScrollbarState<S, T>>,
+);
+
+/// A scrollbar state that should be persisted across frames.
+struct ScrollbarState<S: ScrollbarVisibility, T: ScrollableHandle = ScrollHandle> {
+ thumb_state: ThumbState,
+ notify_id: Option<EntityId>,
+ manually_added: bool,
+ scroll_handle: T,
+ width: ScrollbarWidth,
+ tracked_setting: PhantomData<S>,
+ show_setting: ShowScrollbar,
+ visibility: Point<ReservedSpace>,
+ show_state: VisibilityState,
+ mouse_in_parent: bool,
+ last_prepaint_state: Option<ScrollbarPrepaintState>,
+ _auto_hide_task: Option<Task<()>>,
+}
+
+impl<S: ScrollbarVisibility, T: ScrollableHandle> ScrollbarState<S, T> {
+ fn new_from_config(
+ config: Scrollbars<S, T>,
+ parent_id: EntityId,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ cx.observe_global_in::<SettingsStore>(window, Self::settings_changed)
+ .detach();
+
+ ScrollbarState {
+ thumb_state: Default::default(),
+ notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)),
+ manually_added: config.handle_was_added,
+ scroll_handle: (config.scrollable_handle)(),
+ width: config.scrollbar_width,
+ visibility: config.visibility,
+ tracked_setting: PhantomData,
+ show_setting: S::get_value(cx).visibility(cx),
+ show_state: VisibilityState::Visible,
+ mouse_in_parent: true,
+ last_prepaint_state: None,
+ _auto_hide_task: None,
+ }
+ }
+
+ fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.set_show_scrollbar(S::get_value(cx).visibility(cx), window, cx);
+ }
+
+ /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet.
+ fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if self._auto_hide_task.is_none() {
+ self._auto_hide_task =
+ (self.visible() && self.show_setting.should_auto_hide(cx)).then(|| {
+ cx.spawn_in(window, async move |scrollbar_state, cx| {
+ cx.background_executor()
+ .timer(SCROLLBAR_SHOW_INTERVAL)
+ .await;
+ scrollbar_state
+ .update(cx, |state, cx| {
+ state.set_visibility(VisibilityState::Hidden, cx);
+ state._auto_hide_task.take()
+ })
+ .log_err();
+ })
+ });
+ }
+ }
+
+ fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.set_visibility(VisibilityState::Visible, cx);
+ self._auto_hide_task.take();
+ self.schedule_auto_hide(window, cx);
+ }
+
+ fn set_show_scrollbar(
+ &mut self,
+ show: ShowScrollbar,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.show_setting != show {
+ self.show_setting = show;
+ self.set_visibility(VisibilityState::from_show_setting(show), cx);
+ self.schedule_auto_hide(window, cx);
+ cx.notify();
+ }
+ }
+
+ fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context<Self>) {
+ if self.show_state != visibility {
+ self.show_state = visibility;
+ cx.notify();
+ }
+ }
+
+ #[inline]
+ fn visible_axes(&self) -> Option<ScrollAxes> {
+ match (&self.visibility.x, &self.visibility.y) {
+ (ReservedSpace::None, ReservedSpace::None) => None,
+ (ReservedSpace::None, _) => Some(ScrollAxes::Vertical),
+ (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal),
+ _ => Some(ScrollAxes::Both),
+ }
+ }
+
+ fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option<Pixels> {
+ (self.show_state.is_disabled().not() && self.visibility.along(axis).needs_scroll_track())
+ .then(|| self.space_to_reserve())
+ }
+
+ fn space_to_reserve(&self) -> Pixels {
+ self.width.to_pixels() + 2 * SCROLLBAR_PADDING
+ }
+
+ fn handle_to_track<Handle: ScrollableHandle>(&self) -> Option<&Handle> {
+ (!self.manually_added)
+ .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::<Handle>())
+ .flatten()
+ }
+
+ fn scroll_handle(&self) -> &T {
+ &self.scroll_handle
+ }
+
+ fn set_offset(&mut self, offset: Point<Pixels>, cx: &mut Context<Self>) {
+ self.scroll_handle.set_offset(offset);
+ self.notify_parent(cx);
+ cx.notify();
+ }
+
+ fn is_dragging(&self) -> bool {
+ self.thumb_state.is_dragging()
+ }
+
+ fn set_dragging(
+ &mut self,
+ axis: ScrollbarAxis,
+ drag_offset: Pixels,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx);
+ self.scroll_handle().drag_started();
+ }
+
+ fn update_hovered_thumb(
+ &mut self,
+ position: &Point<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.set_thumb_state(
+ if let Some(&ScrollbarLayout { axis, .. }) = self
+ .last_prepaint_state
+ .as_ref()
+ .and_then(|state| state.thumb_for_position(position))
+ {
+ ThumbState::Hover(axis)
+ } else {
+ ThumbState::Inactive
+ },
+ window,
+ cx,
+ );
+ }
+
+ fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context<Self>) {
+ if self.thumb_state != state {
+ if state == ThumbState::Inactive {
+ self.schedule_auto_hide(window, cx);
+ } else {
+ self.show_scrollbars(window, cx);
+ }
+ self.thumb_state = state;
+ cx.notify();
+ }
+ }
+
+ fn update_parent_hovered(&mut self, position: &Point<Pixels>) -> ParentHovered {
+ let last_parent_hovered = self.mouse_in_parent;
+ self.mouse_in_parent = self.parent_hovered(position);
+ let state_changed = self.mouse_in_parent != last_parent_hovered;
+ match self.mouse_in_parent {
+ true => ParentHovered::Yes(state_changed),
+ false => ParentHovered::No(state_changed),
+ }
+ }
+
+ fn parent_hovered(&self, position: &Point<Pixels>) -> bool {
+ self.last_prepaint_state
+ .as_ref()
+ .is_some_and(|state| state.parent_bounds.contains(position))
+ }
+
+ fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
+ self.last_prepaint_state
+ .as_ref()
+ .and_then(|state| state.hit_for_position(position))
+ }
+
+ fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> {
+ self.last_prepaint_state
+ .as_ref()
+ .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis))
+ }
+
+ fn thumb_ranges(
+ &self,
+ ) -> impl Iterator<Item = (ScrollbarAxis, Range<f32>, ReservedSpace)> + '_ {
+ const MINIMUM_THUMB_SIZE: Pixels = px(25.);
+ let max_offset = self.scroll_handle().max_offset();
+ let viewport_size = self.scroll_handle().viewport().size;
+ let current_offset = self.scroll_handle().offset();
+
+ [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical]
+ .into_iter()
+ .filter(|&axis| self.visibility.along(axis).is_visible())
+ .flat_map(move |axis| {
+ let max_offset = max_offset.along(axis);
+ let viewport_size = viewport_size.along(axis);
+ if max_offset.is_zero() || viewport_size.is_zero() {
+ return None;
+ }
+ let content_size = viewport_size + max_offset;
+ let visible_percentage = viewport_size / content_size;
+ let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
+ if thumb_size > viewport_size {
+ return None;
+ }
+ let current_offset = current_offset
+ .along(axis)
+ .clamp(-max_offset, Pixels::ZERO)
+ .abs();
+ let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
+ let thumb_percentage_start = start_offset / viewport_size;
+ let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
+ Some((
+ axis,
+ thumb_percentage_start..thumb_percentage_end,
+ self.visibility.along(axis),
+ ))
+ })
+ }
+
+ fn visible(&self) -> bool {
+ self.show_state.is_visible()
+ }
-pub struct Scrollbar {
- thumb: Range<f32>,
- state: ScrollbarState,
- kind: ScrollbarAxis,
+ #[inline]
+ fn disabled(&self) -> bool {
+ self.show_state.is_disabled()
+ }
+
+ fn notify_parent(&self, cx: &mut App) {
+ if let Some(entity_id) = self.notify_id {
+ cx.notify(entity_id);
+ }
+ }
}
-#[derive(Default, Debug, Clone, Copy)]
+impl<S: ScrollbarVisibility, T: ScrollableHandle> Render for ScrollbarState<S, T> {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ ScrollbarElement {
+ state: cx.entity(),
+ origin: Default::default(),
+ }
+ }
+}
+
+struct ScrollbarElement<S: ScrollbarVisibility, T: ScrollableHandle> {
+ origin: Point<Pixels>,
+ state: Entity<ScrollbarState<S, T>>,
+}
+
+#[derive(Default, Debug, PartialEq, Eq)]
enum ThumbState {
#[default]
Inactive,
- Hover,
- Dragging(Pixels),
+ Hover(ScrollbarAxis),
+ Dragging(ScrollbarAxis, Pixels),
}
impl ThumbState {
fn is_dragging(&self) -> bool {
- matches!(*self, ThumbState::Dragging(_))
+ matches!(*self, ThumbState::Dragging(..))
}
}
@@ -99,170 +843,110 @@ impl ScrollableHandle for ScrollHandle {
}
}
-pub trait ScrollableHandle: Any + Debug {
- fn content_size(&self) -> Size<Pixels> {
- self.viewport().size + self.max_offset()
- }
+pub trait ScrollableHandle: 'static + Any + Sized {
fn max_offset(&self) -> Size<Pixels>;
fn set_offset(&self, point: Point<Pixels>);
fn offset(&self) -> Point<Pixels>;
fn viewport(&self) -> Bounds<Pixels>;
fn drag_started(&self) {}
fn drag_ended(&self) {}
-}
-/// A scrollbar state that should be persisted across frames.
-#[derive(Clone, Debug)]
-pub struct ScrollbarState {
- thumb_state: Rc<Cell<ThumbState>>,
- parent_id: Option<EntityId>,
- scroll_handle: Arc<dyn ScrollableHandle>,
- auto_hide: Rc<RefCell<AutoHide>>,
+ fn scrollable_along(&self, axis: ScrollbarAxis) -> bool {
+ self.max_offset().along(axis) > Pixels::ZERO
+ }
+ fn content_size(&self) -> Size<Pixels> {
+ self.viewport().size + self.max_offset()
+ }
}
-#[derive(Debug)]
-enum AutoHide {
- Disabled,
- Hidden {
- parent_id: EntityId,
- },
- Visible {
- parent_id: EntityId,
- _task: Task<()>,
- },
+enum ScrollbarMouseEvent {
+ TrackClick,
+ ThumbDrag(Pixels),
}
-impl AutoHide {
- fn is_hidden(&self) -> bool {
- matches!(self, AutoHide::Hidden { .. })
- }
+struct ScrollbarLayout {
+ thumb_bounds: Bounds<Pixels>,
+ track_bounds: Bounds<Pixels>,
+ cursor_hitbox: Hitbox,
+ reserved_space: ReservedSpace,
+ axis: ScrollbarAxis,
}
-impl ScrollbarState {
- pub fn new(scroll: impl ScrollableHandle) -> Self {
- Self {
- thumb_state: Default::default(),
- parent_id: None,
- scroll_handle: Arc::new(scroll),
- auto_hide: Rc::new(RefCell::new(AutoHide::Disabled)),
- }
- }
-
- /// Set a parent model which should be notified whenever this Scrollbar gets a scroll event.
- pub fn parent_entity<V: 'static>(mut self, v: &Entity<V>) -> Self {
- self.parent_id = Some(v.entity_id());
- self
- }
-
- pub fn scroll_handle(&self) -> &Arc<dyn ScrollableHandle> {
- &self.scroll_handle
- }
-
- pub fn is_dragging(&self) -> bool {
- matches!(self.thumb_state.get(), ThumbState::Dragging(_))
- }
+impl ScrollbarLayout {
+ fn compute_click_offset(
+ &self,
+ event_position: Point<Pixels>,
+ max_offset: Size<Pixels>,
+ event_type: ScrollbarMouseEvent,
+ ) -> Pixels {
+ let Self {
+ track_bounds,
+ thumb_bounds,
+ axis,
+ ..
+ } = self;
+ let axis = *axis;
+
+ let viewport_size = track_bounds.size.along(axis);
+ let thumb_size = thumb_bounds.size.along(axis);
+ let thumb_offset = match event_type {
+ ScrollbarMouseEvent::TrackClick => thumb_size / 2.,
+ ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
+ };
- fn set_dragging(&self, drag_offset: Pixels) {
- self.set_thumb_state(ThumbState::Dragging(drag_offset));
- self.scroll_handle.drag_started();
- }
+ let thumb_start =
+ (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset)
+ .clamp(px(0.), viewport_size - thumb_size);
- fn set_thumb_hovered(&self, hovered: bool) {
- self.set_thumb_state(if hovered {
- ThumbState::Hover
+ let max_offset = max_offset.along(axis);
+ let percentage = if viewport_size > thumb_size {
+ thumb_start / (viewport_size - thumb_size)
} else {
- ThumbState::Inactive
- });
- }
-
- fn set_thumb_state(&self, state: ThumbState) {
- self.thumb_state.set(state);
- }
-
- fn thumb_range(&self, axis: ScrollbarAxis) -> Option<Range<f32>> {
- const MINIMUM_THUMB_SIZE: Pixels = px(25.);
- let max_offset = self.scroll_handle.max_offset().along(axis);
- let viewport_size = self.scroll_handle.viewport().size.along(axis);
- if max_offset.is_zero() || viewport_size.is_zero() {
- return None;
- }
- let content_size = viewport_size + max_offset;
- let visible_percentage = viewport_size / content_size;
- let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
- if thumb_size > viewport_size {
- return None;
- }
- let current_offset = self
- .scroll_handle
- .offset()
- .along(axis)
- .clamp(-max_offset, Pixels::ZERO)
- .abs();
- let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
- let thumb_percentage_start = start_offset / viewport_size;
- let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
- Some(thumb_percentage_start..thumb_percentage_end)
- }
-
- fn show_temporarily(&self, parent_id: EntityId, cx: &mut App) {
- const SHOW_INTERVAL: Duration = Duration::from_secs(1);
-
- let auto_hide = self.auto_hide.clone();
- auto_hide.replace(AutoHide::Visible {
- parent_id,
- _task: cx.spawn({
- let this = auto_hide.clone();
- async move |cx| {
- cx.background_executor().timer(SHOW_INTERVAL).await;
- this.replace(AutoHide::Hidden { parent_id });
- cx.update(|cx| {
- cx.notify(parent_id);
- })
- .ok();
- }
- }),
- });
- }
-
- fn unhide(&self, position: &Point<Pixels>, cx: &mut App) {
- let parent_id = match &*self.auto_hide.borrow() {
- AutoHide::Disabled => return,
- AutoHide::Hidden { parent_id } => *parent_id,
- AutoHide::Visible { parent_id, _task } => *parent_id,
+ 0.
};
- if self.scroll_handle().viewport().contains(position) {
- self.show_temporarily(parent_id, cx);
- }
+ -max_offset * percentage
}
}
-impl Scrollbar {
- pub fn vertical(state: ScrollbarState) -> Option<Self> {
- Self::new(state, ScrollbarAxis::Vertical)
+impl PartialEq for ScrollbarLayout {
+ fn eq(&self, other: &Self) -> bool {
+ self.axis == other.axis && self.thumb_bounds == other.thumb_bounds
}
+}
- pub fn horizontal(state: ScrollbarState) -> Option<Self> {
- Self::new(state, ScrollbarAxis::Horizontal)
+pub struct ScrollbarPrepaintState {
+ parent_bounds: Bounds<Pixels>,
+ thumbs: SmallVec<[ScrollbarLayout; 2]>,
+}
+
+impl ScrollbarPrepaintState {
+ fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
+ self.thumbs
+ .iter()
+ .find(|info| info.thumb_bounds.contains(position))
}
- fn new(state: ScrollbarState, kind: ScrollbarAxis) -> Option<Self> {
- let thumb = state.thumb_range(kind)?;
- Some(Self { thumb, state, kind })
+ fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
+ self.thumbs.iter().find(|info| {
+ if info.reserved_space.needs_scroll_track() {
+ info.track_bounds.contains(position)
+ } else {
+ info.thumb_bounds.contains(position)
+ }
+ })
}
+}
- /// Automatically hide the scrollbar when idle
- pub fn auto_hide<V: 'static>(self, cx: &mut Context<V>) -> Self {
- if matches!(*self.state.auto_hide.borrow(), AutoHide::Disabled) {
- self.state.show_temporarily(cx.entity_id(), cx);
- }
- self
+impl PartialEq for ScrollbarPrepaintState {
+ fn eq(&self, other: &Self) -> bool {
+ self.thumbs == other.thumbs
}
}
-impl Element for Scrollbar {
+impl<S: ScrollbarVisibility, T: ScrollableHandle> Element for ScrollbarElement<S, T> {
type RequestLayoutState = ();
- type PrepaintState = Hitbox;
+ type PrepaintState = Option<ScrollbarPrepaintState>;
fn id(&self) -> Option<ElementId> {
None