scrollbar.rs

   1use std::{any::Any, fmt::Debug, ops::Not, time::Duration};
   2
   3use gpui::{
   4    Along, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Context,
   5    Corner, Corners, CursorStyle, Div, Edges, Element, ElementId, Entity, EntityId,
   6    GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
   7    LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Negate,
   8    ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful,
   9    StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration,
  10    UniformListScrollHandle, Window, prelude::FluentBuilder as _, px, quad, relative, size,
  11};
  12use settings::SettingsStore;
  13use smallvec::SmallVec;
  14use theme::ActiveTheme as _;
  15use util::ResultExt;
  16
  17use std::ops::Range;
  18
  19use crate::scrollbars::{ScrollbarVisibility, ShowScrollbar};
  20
  21const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_millis(1500);
  22const SCROLLBAR_PADDING: Pixels = px(4.);
  23
  24pub mod scrollbars {
  25    use gpui::{App, Global};
  26    use schemars::JsonSchema;
  27    use serde::{Deserialize, Serialize};
  28    use settings::Settings;
  29
  30    /// When to show the scrollbar in the editor.
  31    ///
  32    /// Default: auto
  33    #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
  34    #[serde(rename_all = "snake_case")]
  35    pub enum ShowScrollbar {
  36        /// Show the scrollbar if there's important information or
  37        /// follow the system's configured behavior.
  38        Auto,
  39        /// Match the system's configured behavior.
  40        System,
  41        /// Always show the scrollbar.
  42        Always,
  43        /// Never show the scrollbar.
  44        Never,
  45    }
  46
  47    impl From<settings::ShowScrollbar> for ShowScrollbar {
  48        fn from(value: settings::ShowScrollbar) -> Self {
  49            match value {
  50                settings::ShowScrollbar::Auto => ShowScrollbar::Auto,
  51                settings::ShowScrollbar::System => ShowScrollbar::System,
  52                settings::ShowScrollbar::Always => ShowScrollbar::Always,
  53                settings::ShowScrollbar::Never => ShowScrollbar::Never,
  54            }
  55        }
  56    }
  57
  58    impl ShowScrollbar {
  59        pub(super) fn show(&self) -> bool {
  60            *self != Self::Never
  61        }
  62
  63        pub(super) fn should_auto_hide(&self, cx: &mut App) -> bool {
  64            match self {
  65                Self::Auto => true,
  66                Self::System => cx.default_global::<ScrollbarAutoHide>().should_hide(),
  67                _ => false,
  68            }
  69        }
  70    }
  71
  72    pub trait GlobalSetting {
  73        fn get_value(cx: &App) -> &Self;
  74    }
  75
  76    impl<T: Settings> GlobalSetting for T {
  77        fn get_value(cx: &App) -> &T {
  78            T::get_global(cx)
  79        }
  80    }
  81
  82    pub trait ScrollbarVisibility: GlobalSetting + 'static {
  83        fn visibility(&self, cx: &App) -> ShowScrollbar;
  84    }
  85
  86    #[derive(Default)]
  87    pub struct ScrollbarAutoHide(pub bool);
  88
  89    impl ScrollbarAutoHide {
  90        pub fn should_hide(&self) -> bool {
  91            self.0
  92        }
  93    }
  94
  95    impl Global for ScrollbarAutoHide {}
  96}
  97
  98fn get_scrollbar_state<T>(
  99    mut config: Scrollbars<T>,
 100    caller_location: &'static std::panic::Location,
 101    window: &mut Window,
 102    cx: &mut App,
 103) -> Entity<ScrollbarStateWrapper<T>>
 104where
 105    T: ScrollableHandle,
 106{
 107    let element_id = config.id.take().unwrap_or_else(|| caller_location.into());
 108
 109    window.use_keyed_state(element_id, cx, |window, cx| {
 110        let parent_id = cx.entity_id();
 111        ScrollbarStateWrapper(
 112            cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, window, cx)),
 113        )
 114    })
 115}
 116
 117pub trait WithScrollbar: Sized {
 118    type Output;
 119
 120    fn custom_scrollbars<T>(
 121        self,
 122        config: Scrollbars<T>,
 123        window: &mut Window,
 124        cx: &mut App,
 125    ) -> Self::Output
 126    where
 127        T: ScrollableHandle;
 128
 129    #[track_caller]
 130    fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
 131        self.custom_scrollbars(
 132            Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()),
 133            window,
 134            cx,
 135        )
 136    }
 137
 138    #[track_caller]
 139    fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
 140        self.custom_scrollbars(
 141            Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()),
 142            window,
 143            cx,
 144        )
 145    }
 146
 147    #[track_caller]
 148    fn vertical_scrollbar_for<ScrollHandle: ScrollableHandle>(
 149        self,
 150        scroll_handle: ScrollHandle,
 151        window: &mut Window,
 152        cx: &mut App,
 153    ) -> Self::Output {
 154        self.custom_scrollbars(
 155            Scrollbars::new(ScrollAxes::Vertical)
 156                .tracked_scroll_handle(scroll_handle)
 157                .ensure_id(core::panic::Location::caller()),
 158            window,
 159            cx,
 160        )
 161    }
 162}
 163
 164impl WithScrollbar for Stateful<Div> {
 165    type Output = Self;
 166
 167    #[track_caller]
 168    fn custom_scrollbars<T>(
 169        self,
 170        config: Scrollbars<T>,
 171        window: &mut Window,
 172        cx: &mut App,
 173    ) -> Self::Output
 174    where
 175        T: ScrollableHandle,
 176    {
 177        render_scrollbar(
 178            get_scrollbar_state(config, std::panic::Location::caller(), window, cx),
 179            self,
 180            cx,
 181        )
 182    }
 183}
 184
 185impl WithScrollbar for Div {
 186    type Output = Stateful<Div>;
 187
 188    #[track_caller]
 189    fn custom_scrollbars<T>(
 190        self,
 191        config: Scrollbars<T>,
 192        window: &mut Window,
 193        cx: &mut App,
 194    ) -> Self::Output
 195    where
 196        T: ScrollableHandle,
 197    {
 198        let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
 199        // We know this ID stays consistent as long as the element is rendered for
 200        // consecutive frames, which is sufficient for our use case here
 201        let scrollbar_entity_id = scrollbar.entity_id();
 202
 203        render_scrollbar(
 204            scrollbar,
 205            self.id(("track-scroll", scrollbar_entity_id)),
 206            cx,
 207        )
 208    }
 209}
 210
 211fn render_scrollbar<T>(
 212    scrollbar: Entity<ScrollbarStateWrapper<T>>,
 213    div: Stateful<Div>,
 214    cx: &App,
 215) -> Stateful<Div>
 216where
 217    T: ScrollableHandle,
 218{
 219    let state = &scrollbar.read(cx).0;
 220
 221    div.when_some(state.read(cx).handle_to_track(), |this, handle| {
 222        this.track_scroll(handle).when_some(
 223            state.read(cx).visible_axes(),
 224            |this, axes| match axes {
 225                ScrollAxes::Horizontal => this.overflow_x_scroll(),
 226                ScrollAxes::Vertical => this.overflow_y_scroll(),
 227                ScrollAxes::Both => this.overflow_scroll(),
 228            },
 229        )
 230    })
 231    .when_some(
 232        state
 233            .read(cx)
 234            .space_to_reserve_for(ScrollbarAxis::Horizontal),
 235        |this, space| this.pb(space),
 236    )
 237    .when_some(
 238        state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical),
 239        |this, space| this.pr(space),
 240    )
 241    .child(state.clone())
 242}
 243
 244impl<T: ScrollableHandle> UniformListDecoration for ScrollbarStateWrapper<T> {
 245    fn compute(
 246        &self,
 247        _visible_range: Range<usize>,
 248        _bounds: Bounds<Pixels>,
 249        scroll_offset: Point<Pixels>,
 250        _item_height: Pixels,
 251        _item_count: usize,
 252        _window: &mut Window,
 253        _cx: &mut App,
 254    ) -> gpui::AnyElement {
 255        ScrollbarElement {
 256            origin: scroll_offset.negate(),
 257            state: self.0.clone(),
 258        }
 259        .into_any()
 260    }
 261}
 262
 263// impl WithScrollbar for UniformList {
 264//     type Output = Self;
 265
 266//     #[track_caller]
 267//     fn custom_scrollbars<S, T>(
 268//         self,
 269//         config: Scrollbars<S, T>,
 270//         window: &mut Window,
 271//         cx: &mut App,
 272//     ) -> Self::Output
 273//     where
 274//         S: ScrollbarVisibilitySetting,
 275//         T: ScrollableHandle,
 276//     {
 277//         let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
 278//         self.when_some(
 279//             scrollbar.read_with(cx, |wrapper, cx| {
 280//                 wrapper
 281//                     .0
 282//                     .read(cx)
 283//                     .handle_to_track::<UniformListScrollHandle>()
 284//                     .cloned()
 285//             }),
 286//             |this, handle| this.track_scroll(handle),
 287//         )
 288//         .with_decoration(scrollbar)
 289//     }
 290// }
 291
 292pub enum ScrollAxes {
 293    Horizontal,
 294    Vertical,
 295    Both,
 296}
 297
 298impl ScrollAxes {
 299    fn apply_to<T>(self, point: Point<T>, value: T) -> Point<T>
 300    where
 301        T: Debug + Default + PartialEq + Clone,
 302    {
 303        match self {
 304            Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value),
 305            Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value),
 306            Self::Both => Point::new(value.clone(), value),
 307        }
 308    }
 309}
 310
 311#[derive(Clone, Debug, Default, PartialEq)]
 312enum ReservedSpace {
 313    #[default]
 314    None,
 315    Thumb,
 316    Track,
 317}
 318
 319impl ReservedSpace {
 320    fn is_visible(&self) -> bool {
 321        *self != ReservedSpace::None
 322    }
 323
 324    fn needs_scroll_track(&self) -> bool {
 325        *self == ReservedSpace::Track
 326    }
 327}
 328
 329#[derive(Debug, Default, Clone, Copy)]
 330enum ScrollbarWidth {
 331    #[default]
 332    Normal,
 333    Small,
 334    XSmall,
 335}
 336
 337impl ScrollbarWidth {
 338    fn to_pixels(&self) -> Pixels {
 339        match self {
 340            ScrollbarWidth::Normal => px(8.),
 341            ScrollbarWidth::Small => px(6.),
 342            ScrollbarWidth::XSmall => px(4.),
 343        }
 344    }
 345}
 346
 347#[derive(Clone)]
 348pub struct Scrollbars<T: ScrollableHandle = ScrollHandle> {
 349    id: Option<ElementId>,
 350    get_visibility: fn(&App) -> ShowScrollbar,
 351    tracked_entity: Option<Option<EntityId>>,
 352    scrollable_handle: T,
 353    handle_was_added: bool,
 354    visibility: Point<ReservedSpace>,
 355    scrollbar_width: ScrollbarWidth,
 356}
 357
 358impl Scrollbars {
 359    pub fn new(show_along: ScrollAxes) -> Self {
 360        Self::new_with_setting(show_along, |_| ShowScrollbar::Always)
 361    }
 362
 363    pub fn for_settings<S: ScrollbarVisibility>() -> Scrollbars {
 364        Scrollbars::new_with_setting(ScrollAxes::Both, |cx| S::get_value(cx).visibility(cx))
 365    }
 366}
 367
 368impl Scrollbars {
 369    fn new_with_setting(show_along: ScrollAxes, get_visibility: fn(&App) -> ShowScrollbar) -> Self {
 370        Self {
 371            id: None,
 372            get_visibility,
 373            handle_was_added: false,
 374            scrollable_handle: ScrollHandle::new(),
 375            tracked_entity: None,
 376            visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb),
 377            scrollbar_width: ScrollbarWidth::Normal,
 378        }
 379    }
 380}
 381
 382impl<ScrollHandle: ScrollableHandle> Scrollbars<ScrollHandle> {
 383    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
 384        self.id = Some(id.into());
 385        self
 386    }
 387
 388    fn ensure_id(mut self, id: impl Into<ElementId>) -> Self {
 389        if self.id.is_none() {
 390            self.id = Some(id.into());
 391        }
 392        self
 393    }
 394
 395    /// Notify the current context whenever this scrollbar gets a scroll event
 396    pub fn notify_content(mut self) -> Self {
 397        self.tracked_entity = Some(None);
 398        self
 399    }
 400
 401    /// Set a parent model which should be notified whenever this scrollbar gets a scroll event.
 402    pub fn tracked_entity(mut self, entity_id: EntityId) -> Self {
 403        self.tracked_entity = Some(Some(entity_id));
 404        self
 405    }
 406
 407    pub fn tracked_scroll_handle<TrackedHandle: ScrollableHandle>(
 408        self,
 409        tracked_scroll_handle: TrackedHandle,
 410    ) -> Scrollbars<TrackedHandle> {
 411        let Self {
 412            id,
 413            tracked_entity: tracked_entity_id,
 414            scrollbar_width,
 415            visibility,
 416            get_visibility,
 417            ..
 418        } = self;
 419
 420        Scrollbars {
 421            handle_was_added: true,
 422            scrollable_handle: tracked_scroll_handle,
 423            id,
 424            tracked_entity: tracked_entity_id,
 425            visibility,
 426            scrollbar_width,
 427            get_visibility,
 428        }
 429    }
 430
 431    pub fn show_along(mut self, along: ScrollAxes) -> Self {
 432        self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb);
 433        self
 434    }
 435
 436    pub fn with_track_along(mut self, along: ScrollAxes) -> Self {
 437        self.visibility = along.apply_to(self.visibility, ReservedSpace::Track);
 438        self
 439    }
 440
 441    pub fn width_sm(mut self) -> Self {
 442        self.scrollbar_width = ScrollbarWidth::Small;
 443        self
 444    }
 445
 446    pub fn width_xs(mut self) -> Self {
 447        self.scrollbar_width = ScrollbarWidth::XSmall;
 448        self
 449    }
 450}
 451
 452#[derive(PartialEq, Eq)]
 453enum VisibilityState {
 454    Visible,
 455    Hidden,
 456    Disabled,
 457}
 458
 459impl VisibilityState {
 460    fn from_show_setting(show_setting: ShowScrollbar) -> Self {
 461        if show_setting.show() {
 462            Self::Visible
 463        } else {
 464            Self::Disabled
 465        }
 466    }
 467
 468    fn is_visible(&self) -> bool {
 469        *self == VisibilityState::Visible
 470    }
 471
 472    #[inline]
 473    fn is_disabled(&self) -> bool {
 474        *self == VisibilityState::Disabled
 475    }
 476}
 477
 478enum ParentHovered {
 479    Yes(bool),
 480    No(bool),
 481}
 482
 483/// This is used to ensure notifies within the state do not notify the parent
 484/// unintentionally.
 485struct ScrollbarStateWrapper<T: ScrollableHandle>(Entity<ScrollbarState<T>>);
 486
 487/// A scrollbar state that should be persisted across frames.
 488struct ScrollbarState<T: ScrollableHandle = ScrollHandle> {
 489    thumb_state: ThumbState,
 490    notify_id: Option<EntityId>,
 491    manually_added: bool,
 492    scroll_handle: T,
 493    width: ScrollbarWidth,
 494    show_setting: ShowScrollbar,
 495    get_visibility: fn(&App) -> ShowScrollbar,
 496    visibility: Point<ReservedSpace>,
 497    show_state: VisibilityState,
 498    mouse_in_parent: bool,
 499    last_prepaint_state: Option<ScrollbarPrepaintState>,
 500    _auto_hide_task: Option<Task<()>>,
 501}
 502
 503impl<T: ScrollableHandle> ScrollbarState<T> {
 504    fn new_from_config(
 505        config: Scrollbars<T>,
 506        parent_id: EntityId,
 507        window: &mut Window,
 508        cx: &mut Context<Self>,
 509    ) -> Self {
 510        cx.observe_global_in::<SettingsStore>(window, Self::settings_changed)
 511            .detach();
 512
 513        let show_setting = (config.get_visibility)(cx);
 514        ScrollbarState {
 515            thumb_state: Default::default(),
 516            notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)),
 517            manually_added: config.handle_was_added,
 518            scroll_handle: config.scrollable_handle,
 519            width: config.scrollbar_width,
 520            visibility: config.visibility,
 521            show_setting,
 522            get_visibility: config.get_visibility,
 523            show_state: VisibilityState::from_show_setting(show_setting),
 524            mouse_in_parent: true,
 525            last_prepaint_state: None,
 526            _auto_hide_task: None,
 527        }
 528    }
 529
 530    fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 531        self.set_show_scrollbar((self.get_visibility)(cx), window, cx);
 532    }
 533
 534    /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet.
 535    fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 536        if self._auto_hide_task.is_none() {
 537            self._auto_hide_task =
 538                (self.visible() && self.show_setting.should_auto_hide(cx)).then(|| {
 539                    cx.spawn_in(window, async move |scrollbar_state, cx| {
 540                        cx.background_executor()
 541                            .timer(SCROLLBAR_SHOW_INTERVAL)
 542                            .await;
 543                        scrollbar_state
 544                            .update(cx, |state, cx| {
 545                                state.set_visibility(VisibilityState::Hidden, cx);
 546                                state._auto_hide_task.take()
 547                            })
 548                            .log_err();
 549                    })
 550                });
 551        }
 552    }
 553
 554    fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 555        self.set_visibility(VisibilityState::Visible, cx);
 556        self._auto_hide_task.take();
 557        self.schedule_auto_hide(window, cx);
 558    }
 559
 560    fn set_show_scrollbar(
 561        &mut self,
 562        show: ShowScrollbar,
 563        window: &mut Window,
 564        cx: &mut Context<Self>,
 565    ) {
 566        if self.show_setting != show {
 567            self.show_setting = show;
 568            self.set_visibility(VisibilityState::from_show_setting(show), cx);
 569            self.schedule_auto_hide(window, cx);
 570            cx.notify();
 571        }
 572    }
 573
 574    fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context<Self>) {
 575        if self.show_state != visibility {
 576            self.show_state = visibility;
 577            cx.notify();
 578        }
 579    }
 580
 581    #[inline]
 582    fn visible_axes(&self) -> Option<ScrollAxes> {
 583        match (&self.visibility.x, &self.visibility.y) {
 584            (ReservedSpace::None, ReservedSpace::None) => None,
 585            (ReservedSpace::None, _) => Some(ScrollAxes::Vertical),
 586            (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal),
 587            _ => Some(ScrollAxes::Both),
 588        }
 589    }
 590
 591    fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option<Pixels> {
 592        (self.show_state.is_disabled().not() && self.visibility.along(axis).needs_scroll_track())
 593            .then(|| self.space_to_reserve())
 594    }
 595
 596    fn space_to_reserve(&self) -> Pixels {
 597        self.width.to_pixels() + 2 * SCROLLBAR_PADDING
 598    }
 599
 600    fn handle_to_track<Handle: ScrollableHandle>(&self) -> Option<&Handle> {
 601        (!self.manually_added)
 602            .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::<Handle>())
 603            .flatten()
 604    }
 605
 606    fn scroll_handle(&self) -> &T {
 607        &self.scroll_handle
 608    }
 609
 610    fn set_offset(&mut self, offset: Point<Pixels>, cx: &mut Context<Self>) {
 611        self.scroll_handle.set_offset(offset);
 612        self.notify_parent(cx);
 613        cx.notify();
 614    }
 615
 616    fn is_dragging(&self) -> bool {
 617        self.thumb_state.is_dragging()
 618    }
 619
 620    fn set_dragging(
 621        &mut self,
 622        axis: ScrollbarAxis,
 623        drag_offset: Pixels,
 624        window: &mut Window,
 625        cx: &mut Context<Self>,
 626    ) {
 627        self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx);
 628        self.scroll_handle().drag_started();
 629    }
 630
 631    fn update_hovered_thumb(
 632        &mut self,
 633        position: &Point<Pixels>,
 634        window: &mut Window,
 635        cx: &mut Context<Self>,
 636    ) {
 637        self.set_thumb_state(
 638            if let Some(&ScrollbarLayout { axis, .. }) = self
 639                .last_prepaint_state
 640                .as_ref()
 641                .and_then(|state| state.thumb_for_position(position))
 642            {
 643                ThumbState::Hover(axis)
 644            } else {
 645                ThumbState::Inactive
 646            },
 647            window,
 648            cx,
 649        );
 650    }
 651
 652    fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context<Self>) {
 653        if self.thumb_state != state {
 654            if state == ThumbState::Inactive {
 655                self.schedule_auto_hide(window, cx);
 656            } else {
 657                self.show_scrollbars(window, cx);
 658            }
 659            self.thumb_state = state;
 660            cx.notify();
 661        }
 662    }
 663
 664    fn update_parent_hovered(&mut self, position: &Point<Pixels>) -> ParentHovered {
 665        let last_parent_hovered = self.mouse_in_parent;
 666        self.mouse_in_parent = self.parent_hovered(position);
 667        let state_changed = self.mouse_in_parent != last_parent_hovered;
 668        match self.mouse_in_parent {
 669            true => ParentHovered::Yes(state_changed),
 670            false => ParentHovered::No(state_changed),
 671        }
 672    }
 673
 674    fn parent_hovered(&self, position: &Point<Pixels>) -> bool {
 675        self.last_prepaint_state
 676            .as_ref()
 677            .is_some_and(|state| state.parent_bounds.contains(position))
 678    }
 679
 680    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
 681        self.last_prepaint_state
 682            .as_ref()
 683            .and_then(|state| state.hit_for_position(position))
 684    }
 685
 686    fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> {
 687        self.last_prepaint_state
 688            .as_ref()
 689            .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis))
 690    }
 691
 692    fn thumb_ranges(
 693        &self,
 694    ) -> impl Iterator<Item = (ScrollbarAxis, Range<f32>, ReservedSpace)> + '_ {
 695        const MINIMUM_THUMB_SIZE: Pixels = px(25.);
 696        let max_offset = self.scroll_handle().max_offset();
 697        let viewport_size = self.scroll_handle().viewport().size;
 698        let current_offset = self.scroll_handle().offset();
 699
 700        [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical]
 701            .into_iter()
 702            .filter(|&axis| self.visibility.along(axis).is_visible())
 703            .flat_map(move |axis| {
 704                let max_offset = max_offset.along(axis);
 705                let viewport_size = viewport_size.along(axis);
 706                if max_offset.is_zero() || viewport_size.is_zero() {
 707                    return None;
 708                }
 709                let content_size = viewport_size + max_offset;
 710                let visible_percentage = viewport_size / content_size;
 711                let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
 712                if thumb_size > viewport_size {
 713                    return None;
 714                }
 715                let current_offset = current_offset
 716                    .along(axis)
 717                    .clamp(-max_offset, Pixels::ZERO)
 718                    .abs();
 719                let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
 720                let thumb_percentage_start = start_offset / viewport_size;
 721                let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
 722                Some((
 723                    axis,
 724                    thumb_percentage_start..thumb_percentage_end,
 725                    self.visibility.along(axis),
 726                ))
 727            })
 728    }
 729
 730    fn visible(&self) -> bool {
 731        self.show_state.is_visible()
 732    }
 733
 734    #[inline]
 735    fn disabled(&self) -> bool {
 736        self.show_state.is_disabled()
 737    }
 738
 739    fn notify_parent(&self, cx: &mut App) {
 740        if let Some(entity_id) = self.notify_id {
 741            cx.notify(entity_id);
 742        }
 743    }
 744}
 745
 746impl<T: ScrollableHandle> Render for ScrollbarState<T> {
 747    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 748        ScrollbarElement {
 749            state: cx.entity(),
 750            origin: Default::default(),
 751        }
 752    }
 753}
 754
 755struct ScrollbarElement<T: ScrollableHandle> {
 756    origin: Point<Pixels>,
 757    state: Entity<ScrollbarState<T>>,
 758}
 759
 760#[derive(Default, Debug, PartialEq, Eq)]
 761enum ThumbState {
 762    #[default]
 763    Inactive,
 764    Hover(ScrollbarAxis),
 765    Dragging(ScrollbarAxis, Pixels),
 766}
 767
 768impl ThumbState {
 769    fn is_dragging(&self) -> bool {
 770        matches!(*self, ThumbState::Dragging(..))
 771    }
 772}
 773
 774impl ScrollableHandle for UniformListScrollHandle {
 775    fn max_offset(&self) -> Size<Pixels> {
 776        self.0.borrow().base_handle.max_offset()
 777    }
 778
 779    fn set_offset(&self, point: Point<Pixels>) {
 780        self.0.borrow().base_handle.set_offset(point);
 781    }
 782
 783    fn offset(&self) -> Point<Pixels> {
 784        self.0.borrow().base_handle.offset()
 785    }
 786
 787    fn viewport(&self) -> Bounds<Pixels> {
 788        self.0.borrow().base_handle.bounds()
 789    }
 790}
 791
 792impl ScrollableHandle for ListState {
 793    fn max_offset(&self) -> Size<Pixels> {
 794        self.max_offset_for_scrollbar()
 795    }
 796
 797    fn set_offset(&self, point: Point<Pixels>) {
 798        self.set_offset_from_scrollbar(point);
 799    }
 800
 801    fn offset(&self) -> Point<Pixels> {
 802        self.scroll_px_offset_for_scrollbar()
 803    }
 804
 805    fn drag_started(&self) {
 806        self.scrollbar_drag_started();
 807    }
 808
 809    fn drag_ended(&self) {
 810        self.scrollbar_drag_ended();
 811    }
 812
 813    fn viewport(&self) -> Bounds<Pixels> {
 814        self.viewport_bounds()
 815    }
 816}
 817
 818impl ScrollableHandle for ScrollHandle {
 819    fn max_offset(&self) -> Size<Pixels> {
 820        self.max_offset()
 821    }
 822
 823    fn set_offset(&self, point: Point<Pixels>) {
 824        self.set_offset(point);
 825    }
 826
 827    fn offset(&self) -> Point<Pixels> {
 828        self.offset()
 829    }
 830
 831    fn viewport(&self) -> Bounds<Pixels> {
 832        self.bounds()
 833    }
 834}
 835
 836pub trait ScrollableHandle: 'static + Any + Sized {
 837    fn max_offset(&self) -> Size<Pixels>;
 838    fn set_offset(&self, point: Point<Pixels>);
 839    fn offset(&self) -> Point<Pixels>;
 840    fn viewport(&self) -> Bounds<Pixels>;
 841    fn drag_started(&self) {}
 842    fn drag_ended(&self) {}
 843
 844    fn scrollable_along(&self, axis: ScrollbarAxis) -> bool {
 845        self.max_offset().along(axis) > Pixels::ZERO
 846    }
 847    fn content_size(&self) -> Size<Pixels> {
 848        self.viewport().size + self.max_offset()
 849    }
 850}
 851
 852enum ScrollbarMouseEvent {
 853    TrackClick,
 854    ThumbDrag(Pixels),
 855}
 856
 857struct ScrollbarLayout {
 858    thumb_bounds: Bounds<Pixels>,
 859    track_bounds: Bounds<Pixels>,
 860    cursor_hitbox: Hitbox,
 861    reserved_space: ReservedSpace,
 862    axis: ScrollbarAxis,
 863}
 864
 865impl ScrollbarLayout {
 866    fn compute_click_offset(
 867        &self,
 868        event_position: Point<Pixels>,
 869        max_offset: Size<Pixels>,
 870        event_type: ScrollbarMouseEvent,
 871    ) -> Pixels {
 872        let Self {
 873            track_bounds,
 874            thumb_bounds,
 875            axis,
 876            ..
 877        } = self;
 878        let axis = *axis;
 879
 880        let viewport_size = track_bounds.size.along(axis);
 881        let thumb_size = thumb_bounds.size.along(axis);
 882        let thumb_offset = match event_type {
 883            ScrollbarMouseEvent::TrackClick => thumb_size / 2.,
 884            ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
 885        };
 886
 887        let thumb_start =
 888            (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset)
 889                .clamp(px(0.), viewport_size - thumb_size);
 890
 891        let max_offset = max_offset.along(axis);
 892        let percentage = if viewport_size > thumb_size {
 893            thumb_start / (viewport_size - thumb_size)
 894        } else {
 895            0.
 896        };
 897
 898        -max_offset * percentage
 899    }
 900}
 901
 902impl PartialEq for ScrollbarLayout {
 903    fn eq(&self, other: &Self) -> bool {
 904        self.axis == other.axis && self.thumb_bounds == other.thumb_bounds
 905    }
 906}
 907
 908pub struct ScrollbarPrepaintState {
 909    parent_bounds: Bounds<Pixels>,
 910    thumbs: SmallVec<[ScrollbarLayout; 2]>,
 911}
 912
 913impl ScrollbarPrepaintState {
 914    fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
 915        self.thumbs
 916            .iter()
 917            .find(|info| info.thumb_bounds.contains(position))
 918    }
 919
 920    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
 921        self.thumbs.iter().find(|info| {
 922            if info.reserved_space.needs_scroll_track() {
 923                info.track_bounds.contains(position)
 924            } else {
 925                info.thumb_bounds.contains(position)
 926            }
 927        })
 928    }
 929}
 930
 931impl PartialEq for ScrollbarPrepaintState {
 932    fn eq(&self, other: &Self) -> bool {
 933        self.thumbs == other.thumbs
 934    }
 935}
 936
 937impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
 938    type RequestLayoutState = ();
 939    type PrepaintState = Option<ScrollbarPrepaintState>;
 940
 941    fn id(&self) -> Option<ElementId> {
 942        None
 943    }
 944
 945    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
 946        None
 947    }
 948
 949    fn request_layout(
 950        &mut self,
 951        _id: Option<&GlobalElementId>,
 952        _inspector_id: Option<&gpui::InspectorElementId>,
 953        window: &mut Window,
 954        cx: &mut App,
 955    ) -> (LayoutId, Self::RequestLayoutState) {
 956        let scrollbar_style = Style {
 957            position: Position::Absolute,
 958            inset: Edges::default(),
 959            size: size(relative(1.), relative(1.)).map(Into::into),
 960            ..Default::default()
 961        };
 962
 963        (window.request_layout(scrollbar_style, None, cx), ())
 964    }
 965
 966    fn prepaint(
 967        &mut self,
 968        _id: Option<&GlobalElementId>,
 969        _inspector_id: Option<&gpui::InspectorElementId>,
 970        bounds: Bounds<Pixels>,
 971        _request_layout: &mut Self::RequestLayoutState,
 972        window: &mut Window,
 973        cx: &mut App,
 974    ) -> Self::PrepaintState {
 975        let prepaint_state = self
 976            .state
 977            .read(cx)
 978            .disabled()
 979            .not()
 980            .then(|| ScrollbarPrepaintState {
 981                parent_bounds: bounds,
 982                thumbs: {
 983                    let thumb_ranges = self.state.read(cx).thumb_ranges().collect::<Vec<_>>();
 984                    let width = self.state.read(cx).width.to_pixels();
 985
 986                    let additional_padding = if thumb_ranges.len() == 2 {
 987                        width
 988                    } else {
 989                        Pixels::ZERO
 990                    };
 991
 992                    thumb_ranges
 993                        .into_iter()
 994                        .map(|(axis, thumb_range, reserved_space)| {
 995                            let track_anchor = match axis {
 996                                ScrollbarAxis::Horizontal => Corner::BottomLeft,
 997                                ScrollbarAxis::Vertical => Corner::TopRight,
 998                            };
 999                            let Bounds { origin, size } = Bounds::from_corner_and_size(
1000                                track_anchor,
1001                                bounds
1002                                    .corner(track_anchor)
1003                                    .apply_along(axis.invert(), |corner| {
1004                                        corner - SCROLLBAR_PADDING
1005                                    }),
1006                                bounds.size.apply_along(axis.invert(), |_| width),
1007                            );
1008                            let scroll_track_bounds = Bounds::new(self.origin + origin, size);
1009
1010                            let padded_bounds = scroll_track_bounds.extend(match axis {
1011                                ScrollbarAxis::Horizontal => Edges {
1012                                    right: -SCROLLBAR_PADDING,
1013                                    left: -SCROLLBAR_PADDING,
1014                                    ..Default::default()
1015                                },
1016                                ScrollbarAxis::Vertical => Edges {
1017                                    top: -SCROLLBAR_PADDING,
1018                                    bottom: -SCROLLBAR_PADDING,
1019                                    ..Default::default()
1020                                },
1021                            });
1022
1023                            let available_space =
1024                                padded_bounds.size.along(axis) - additional_padding;
1025
1026                            let thumb_offset = thumb_range.start * available_space;
1027                            let thumb_end = thumb_range.end * available_space;
1028                            let thumb_bounds = Bounds::new(
1029                                padded_bounds
1030                                    .origin
1031                                    .apply_along(axis, |origin| origin + thumb_offset),
1032                                padded_bounds
1033                                    .size
1034                                    .apply_along(axis, |_| thumb_end - thumb_offset),
1035                            );
1036
1037                            ScrollbarLayout {
1038                                thumb_bounds,
1039                                track_bounds: padded_bounds,
1040                                axis,
1041                                cursor_hitbox: window.insert_hitbox(
1042                                    if reserved_space.needs_scroll_track() {
1043                                        padded_bounds
1044                                    } else {
1045                                        thumb_bounds
1046                                    },
1047                                    HitboxBehavior::BlockMouseExceptScroll,
1048                                ),
1049                                reserved_space,
1050                            }
1051                        })
1052                        .collect()
1053                },
1054            });
1055        if prepaint_state
1056            .as_ref()
1057            .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
1058        {
1059            self.state
1060                .update(cx, |state, cx| state.show_scrollbars(window, cx));
1061        }
1062
1063        prepaint_state
1064    }
1065
1066    fn paint(
1067        &mut self,
1068        _id: Option<&GlobalElementId>,
1069        _inspector_id: Option<&gpui::InspectorElementId>,
1070        Bounds { origin, size }: Bounds<Pixels>,
1071        _request_layout: &mut Self::RequestLayoutState,
1072        prepaint_state: &mut Self::PrepaintState,
1073        window: &mut Window,
1074        cx: &mut App,
1075    ) {
1076        let Some(prepaint_state) = prepaint_state.take() else {
1077            return;
1078        };
1079
1080        let bounds = Bounds::new(self.origin + origin, size);
1081        window.with_content_mask(Some(ContentMask { bounds }), |window| {
1082            let colors = cx.theme().colors();
1083
1084            if self.state.read(cx).visible() {
1085                for ScrollbarLayout {
1086                    thumb_bounds,
1087                    cursor_hitbox,
1088                    axis,
1089                    reserved_space,
1090                    ..
1091                } in &prepaint_state.thumbs
1092                {
1093                    const MAXIMUM_OPACITY: f32 = 0.7;
1094                    let thumb_state = &self.state.read(cx).thumb_state;
1095                    let (thumb_base_color, hovered) = match thumb_state {
1096                        ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1097                            (colors.scrollbar_thumb_active_background, false)
1098                        }
1099                        ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1100                            (colors.scrollbar_thumb_hover_background, true)
1101                        }
1102                        _ => (colors.scrollbar_thumb_background, false),
1103                    };
1104
1105                    let blending_color = if hovered || reserved_space.needs_scroll_track() {
1106                        colors.surface_background
1107                    } else {
1108                        let blend_color = colors.surface_background;
1109                        blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1110                    };
1111
1112                    let thumb_background = blending_color.blend(thumb_base_color);
1113
1114                    window.paint_quad(quad(
1115                        *thumb_bounds,
1116                        Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size),
1117                        thumb_background,
1118                        Edges::default(),
1119                        Hsla::transparent_black(),
1120                        BorderStyle::default(),
1121                    ));
1122
1123                    if thumb_state.is_dragging() {
1124                        window.set_window_cursor_style(CursorStyle::Arrow);
1125                    } else {
1126                        window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1127                    }
1128                }
1129            }
1130
1131            self.state.update(cx, |state, _| {
1132                state.last_prepaint_state = Some(prepaint_state)
1133            });
1134
1135            window.on_mouse_event({
1136                let state = self.state.clone();
1137
1138                move |event: &MouseDownEvent, phase, window, cx| {
1139                    state.update(cx, |state, cx| {
1140                        let Some(scrollbar_layout) = (phase.capture()
1141                            && event.button == MouseButton::Left)
1142                            .then(|| state.hit_for_position(&event.position))
1143                            .flatten()
1144                        else {
1145                            return;
1146                        };
1147
1148                        let ScrollbarLayout {
1149                            thumb_bounds, axis, ..
1150                        } = scrollbar_layout;
1151
1152                        if thumb_bounds.contains(&event.position) {
1153                            let offset =
1154                                event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1155                            state.set_dragging(*axis, offset, window, cx);
1156                        } else {
1157                            let scroll_handle = state.scroll_handle();
1158                            let click_offset = scrollbar_layout.compute_click_offset(
1159                                event.position,
1160                                scroll_handle.max_offset(),
1161                                ScrollbarMouseEvent::TrackClick,
1162                            );
1163                            state.set_offset(
1164                                scroll_handle.offset().apply_along(*axis, |_| click_offset),
1165                                cx,
1166                            );
1167                        };
1168
1169                        cx.stop_propagation();
1170                    });
1171                }
1172            });
1173
1174            window.on_mouse_event({
1175                let state = self.state.clone();
1176
1177                move |event: &ScrollWheelEvent, phase, window, cx| {
1178                    if phase.capture() {
1179                        state.update(cx, |state, cx| {
1180                            state.update_hovered_thumb(&event.position, window, cx)
1181                        });
1182                    }
1183                }
1184            });
1185
1186            window.on_mouse_event({
1187                let state = self.state.clone();
1188
1189                move |event: &MouseMoveEvent, phase, window, cx| {
1190                    if !phase.capture() {
1191                        return;
1192                    }
1193
1194                    match state.read(cx).thumb_state {
1195                        ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1196                            if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1197                                let scroll_handle = state.read(cx).scroll_handle();
1198                                let drag_offset = scrollbar_layout.compute_click_offset(
1199                                    event.position,
1200                                    scroll_handle.max_offset(),
1201                                    ScrollbarMouseEvent::ThumbDrag(drag_state),
1202                                );
1203                                let new_offset =
1204                                    scroll_handle.offset().apply_along(axis, |_| drag_offset);
1205
1206                                state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1207                                cx.stop_propagation();
1208                            }
1209                        }
1210                        _ => state.update(cx, |state, cx| {
1211                            match state.update_parent_hovered(&event.position) {
1212                                ParentHovered::Yes(state_changed)
1213                                    if event.pressed_button.is_none() =>
1214                                {
1215                                    if state_changed {
1216                                        state.show_scrollbars(window, cx);
1217                                    }
1218                                    state.update_hovered_thumb(&event.position, window, cx);
1219                                    if state.thumb_state != ThumbState::Inactive {
1220                                        cx.stop_propagation();
1221                                    }
1222                                }
1223                                ParentHovered::No(state_changed) if state_changed => {
1224                                    state.set_thumb_state(ThumbState::Inactive, window, cx);
1225                                }
1226                                _ => {}
1227                            }
1228                        }),
1229                    }
1230                }
1231            });
1232
1233            window.on_mouse_event({
1234                let state = self.state.clone();
1235                move |event: &MouseUpEvent, phase, window, cx| {
1236                    if !phase.capture() {
1237                        return;
1238                    }
1239
1240                    state.update(cx, |state, cx| {
1241                        if state.is_dragging() {
1242                            state.scroll_handle().drag_ended();
1243                        }
1244
1245                        if !state.parent_hovered(&event.position) {
1246                            state.schedule_auto_hide(window, cx);
1247                            return;
1248                        }
1249
1250                        state.update_hovered_thumb(&event.position, window, cx);
1251                    });
1252                }
1253            });
1254        })
1255    }
1256}
1257
1258impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
1259    type Element = Self;
1260
1261    fn into_element(self) -> Self::Element {
1262        self
1263    }
1264}