scrollbar.rs

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