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