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