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        ScrollbarState {
 525            thumb_state: Default::default(),
 526            notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)),
 527            manually_added: config.handle_was_added,
 528            scroll_handle: (config.scrollable_handle)(),
 529            width: config.scrollbar_width,
 530            visibility: config.visibility,
 531            tracked_setting: PhantomData,
 532            show_setting: S::get_value(cx).visibility(cx),
 533            show_state: VisibilityState::Visible,
 534            mouse_in_parent: true,
 535            last_prepaint_state: None,
 536            _auto_hide_task: None,
 537        }
 538    }
 539
 540    fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 541        self.set_show_scrollbar(S::get_value(cx).visibility(cx), window, cx);
 542    }
 543
 544    /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet.
 545    fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 546        if self._auto_hide_task.is_none() {
 547            self._auto_hide_task =
 548                (self.visible() && self.show_setting.should_auto_hide(cx)).then(|| {
 549                    cx.spawn_in(window, async move |scrollbar_state, cx| {
 550                        cx.background_executor()
 551                            .timer(SCROLLBAR_SHOW_INTERVAL)
 552                            .await;
 553                        scrollbar_state
 554                            .update(cx, |state, cx| {
 555                                state.set_visibility(VisibilityState::Hidden, cx);
 556                                state._auto_hide_task.take()
 557                            })
 558                            .log_err();
 559                    })
 560                });
 561        }
 562    }
 563
 564    fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 565        self.set_visibility(VisibilityState::Visible, cx);
 566        self._auto_hide_task.take();
 567        self.schedule_auto_hide(window, cx);
 568    }
 569
 570    fn set_show_scrollbar(
 571        &mut self,
 572        show: ShowScrollbar,
 573        window: &mut Window,
 574        cx: &mut Context<Self>,
 575    ) {
 576        if self.show_setting != show {
 577            self.show_setting = show;
 578            self.set_visibility(VisibilityState::from_show_setting(show), cx);
 579            self.schedule_auto_hide(window, cx);
 580            cx.notify();
 581        }
 582    }
 583
 584    fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context<Self>) {
 585        if self.show_state != visibility {
 586            self.show_state = visibility;
 587            cx.notify();
 588        }
 589    }
 590
 591    #[inline]
 592    fn visible_axes(&self) -> Option<ScrollAxes> {
 593        match (&self.visibility.x, &self.visibility.y) {
 594            (ReservedSpace::None, ReservedSpace::None) => None,
 595            (ReservedSpace::None, _) => Some(ScrollAxes::Vertical),
 596            (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal),
 597            _ => Some(ScrollAxes::Both),
 598        }
 599    }
 600
 601    fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option<Pixels> {
 602        (self.show_state.is_disabled().not() && self.visibility.along(axis).needs_scroll_track())
 603            .then(|| self.space_to_reserve())
 604    }
 605
 606    fn space_to_reserve(&self) -> Pixels {
 607        self.width.to_pixels() + 2 * SCROLLBAR_PADDING
 608    }
 609
 610    fn handle_to_track<Handle: ScrollableHandle>(&self) -> Option<&Handle> {
 611        (!self.manually_added)
 612            .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::<Handle>())
 613            .flatten()
 614    }
 615
 616    fn scroll_handle(&self) -> &T {
 617        &self.scroll_handle
 618    }
 619
 620    fn set_offset(&mut self, offset: Point<Pixels>, cx: &mut Context<Self>) {
 621        self.scroll_handle.set_offset(offset);
 622        self.notify_parent(cx);
 623        cx.notify();
 624    }
 625
 626    fn is_dragging(&self) -> bool {
 627        self.thumb_state.is_dragging()
 628    }
 629
 630    fn set_dragging(
 631        &mut self,
 632        axis: ScrollbarAxis,
 633        drag_offset: Pixels,
 634        window: &mut Window,
 635        cx: &mut Context<Self>,
 636    ) {
 637        self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx);
 638        self.scroll_handle().drag_started();
 639    }
 640
 641    fn update_hovered_thumb(
 642        &mut self,
 643        position: &Point<Pixels>,
 644        window: &mut Window,
 645        cx: &mut Context<Self>,
 646    ) {
 647        self.set_thumb_state(
 648            if let Some(&ScrollbarLayout { axis, .. }) = self
 649                .last_prepaint_state
 650                .as_ref()
 651                .and_then(|state| state.thumb_for_position(position))
 652            {
 653                ThumbState::Hover(axis)
 654            } else {
 655                ThumbState::Inactive
 656            },
 657            window,
 658            cx,
 659        );
 660    }
 661
 662    fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context<Self>) {
 663        if self.thumb_state != state {
 664            if state == ThumbState::Inactive {
 665                self.schedule_auto_hide(window, cx);
 666            } else {
 667                self.show_scrollbars(window, cx);
 668            }
 669            self.thumb_state = state;
 670            cx.notify();
 671        }
 672    }
 673
 674    fn update_parent_hovered(&mut self, position: &Point<Pixels>) -> ParentHovered {
 675        let last_parent_hovered = self.mouse_in_parent;
 676        self.mouse_in_parent = self.parent_hovered(position);
 677        let state_changed = self.mouse_in_parent != last_parent_hovered;
 678        match self.mouse_in_parent {
 679            true => ParentHovered::Yes(state_changed),
 680            false => ParentHovered::No(state_changed),
 681        }
 682    }
 683
 684    fn parent_hovered(&self, position: &Point<Pixels>) -> bool {
 685        self.last_prepaint_state
 686            .as_ref()
 687            .is_some_and(|state| state.parent_bounds.contains(position))
 688    }
 689
 690    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
 691        self.last_prepaint_state
 692            .as_ref()
 693            .and_then(|state| state.hit_for_position(position))
 694    }
 695
 696    fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> {
 697        self.last_prepaint_state
 698            .as_ref()
 699            .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis))
 700    }
 701
 702    fn thumb_ranges(
 703        &self,
 704    ) -> impl Iterator<Item = (ScrollbarAxis, Range<f32>, ReservedSpace)> + '_ {
 705        const MINIMUM_THUMB_SIZE: Pixels = px(25.);
 706        let max_offset = self.scroll_handle().max_offset();
 707        let viewport_size = self.scroll_handle().viewport().size;
 708        let current_offset = self.scroll_handle().offset();
 709
 710        [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical]
 711            .into_iter()
 712            .filter(|&axis| self.visibility.along(axis).is_visible())
 713            .flat_map(move |axis| {
 714                let max_offset = max_offset.along(axis);
 715                let viewport_size = viewport_size.along(axis);
 716                if max_offset.is_zero() || viewport_size.is_zero() {
 717                    return None;
 718                }
 719                let content_size = viewport_size + max_offset;
 720                let visible_percentage = viewport_size / content_size;
 721                let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
 722                if thumb_size > viewport_size {
 723                    return None;
 724                }
 725                let current_offset = current_offset
 726                    .along(axis)
 727                    .clamp(-max_offset, Pixels::ZERO)
 728                    .abs();
 729                let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
 730                let thumb_percentage_start = start_offset / viewport_size;
 731                let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
 732                Some((
 733                    axis,
 734                    thumb_percentage_start..thumb_percentage_end,
 735                    self.visibility.along(axis),
 736                ))
 737            })
 738    }
 739
 740    fn visible(&self) -> bool {
 741        self.show_state.is_visible()
 742    }
 743
 744    #[inline]
 745    fn disabled(&self) -> bool {
 746        self.show_state.is_disabled()
 747    }
 748
 749    fn notify_parent(&self, cx: &mut App) {
 750        if let Some(entity_id) = self.notify_id {
 751            cx.notify(entity_id);
 752        }
 753    }
 754}
 755
 756impl<S: ScrollbarVisibility, T: ScrollableHandle> Render for ScrollbarState<S, T> {
 757    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 758        ScrollbarElement {
 759            state: cx.entity(),
 760            origin: Default::default(),
 761        }
 762    }
 763}
 764
 765struct ScrollbarElement<S: ScrollbarVisibility, T: ScrollableHandle> {
 766    origin: Point<Pixels>,
 767    state: Entity<ScrollbarState<S, T>>,
 768}
 769
 770#[derive(Default, Debug, PartialEq, Eq)]
 771enum ThumbState {
 772    #[default]
 773    Inactive,
 774    Hover(ScrollbarAxis),
 775    Dragging(ScrollbarAxis, Pixels),
 776}
 777
 778impl ThumbState {
 779    fn is_dragging(&self) -> bool {
 780        matches!(*self, ThumbState::Dragging(..))
 781    }
 782}
 783
 784impl ScrollableHandle for UniformListScrollHandle {
 785    fn max_offset(&self) -> Size<Pixels> {
 786        self.0.borrow().base_handle.max_offset()
 787    }
 788
 789    fn set_offset(&self, point: Point<Pixels>) {
 790        self.0.borrow().base_handle.set_offset(point);
 791    }
 792
 793    fn offset(&self) -> Point<Pixels> {
 794        self.0.borrow().base_handle.offset()
 795    }
 796
 797    fn viewport(&self) -> Bounds<Pixels> {
 798        self.0.borrow().base_handle.bounds()
 799    }
 800}
 801
 802impl ScrollableHandle for ListState {
 803    fn max_offset(&self) -> Size<Pixels> {
 804        self.max_offset_for_scrollbar()
 805    }
 806
 807    fn set_offset(&self, point: Point<Pixels>) {
 808        self.set_offset_from_scrollbar(point);
 809    }
 810
 811    fn offset(&self) -> Point<Pixels> {
 812        self.scroll_px_offset_for_scrollbar()
 813    }
 814
 815    fn drag_started(&self) {
 816        self.scrollbar_drag_started();
 817    }
 818
 819    fn drag_ended(&self) {
 820        self.scrollbar_drag_ended();
 821    }
 822
 823    fn viewport(&self) -> Bounds<Pixels> {
 824        self.viewport_bounds()
 825    }
 826}
 827
 828impl ScrollableHandle for ScrollHandle {
 829    fn max_offset(&self) -> Size<Pixels> {
 830        self.max_offset()
 831    }
 832
 833    fn set_offset(&self, point: Point<Pixels>) {
 834        self.set_offset(point);
 835    }
 836
 837    fn offset(&self) -> Point<Pixels> {
 838        self.offset()
 839    }
 840
 841    fn viewport(&self) -> Bounds<Pixels> {
 842        self.bounds()
 843    }
 844}
 845
 846pub trait ScrollableHandle: 'static + Any + Sized {
 847    fn max_offset(&self) -> Size<Pixels>;
 848    fn set_offset(&self, point: Point<Pixels>);
 849    fn offset(&self) -> Point<Pixels>;
 850    fn viewport(&self) -> Bounds<Pixels>;
 851    fn drag_started(&self) {}
 852    fn drag_ended(&self) {}
 853
 854    fn scrollable_along(&self, axis: ScrollbarAxis) -> bool {
 855        self.max_offset().along(axis) > Pixels::ZERO
 856    }
 857    fn content_size(&self) -> Size<Pixels> {
 858        self.viewport().size + self.max_offset()
 859    }
 860}
 861
 862enum ScrollbarMouseEvent {
 863    TrackClick,
 864    ThumbDrag(Pixels),
 865}
 866
 867struct ScrollbarLayout {
 868    thumb_bounds: Bounds<Pixels>,
 869    track_bounds: Bounds<Pixels>,
 870    cursor_hitbox: Hitbox,
 871    reserved_space: ReservedSpace,
 872    axis: ScrollbarAxis,
 873}
 874
 875impl ScrollbarLayout {
 876    fn compute_click_offset(
 877        &self,
 878        event_position: Point<Pixels>,
 879        max_offset: Size<Pixels>,
 880        event_type: ScrollbarMouseEvent,
 881    ) -> Pixels {
 882        let Self {
 883            track_bounds,
 884            thumb_bounds,
 885            axis,
 886            ..
 887        } = self;
 888        let axis = *axis;
 889
 890        let viewport_size = track_bounds.size.along(axis);
 891        let thumb_size = thumb_bounds.size.along(axis);
 892        let thumb_offset = match event_type {
 893            ScrollbarMouseEvent::TrackClick => thumb_size / 2.,
 894            ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
 895        };
 896
 897        let thumb_start =
 898            (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset)
 899                .clamp(px(0.), viewport_size - thumb_size);
 900
 901        let max_offset = max_offset.along(axis);
 902        let percentage = if viewport_size > thumb_size {
 903            thumb_start / (viewport_size - thumb_size)
 904        } else {
 905            0.
 906        };
 907
 908        -max_offset * percentage
 909    }
 910}
 911
 912impl PartialEq for ScrollbarLayout {
 913    fn eq(&self, other: &Self) -> bool {
 914        self.axis == other.axis && self.thumb_bounds == other.thumb_bounds
 915    }
 916}
 917
 918pub struct ScrollbarPrepaintState {
 919    parent_bounds: Bounds<Pixels>,
 920    thumbs: SmallVec<[ScrollbarLayout; 2]>,
 921}
 922
 923impl ScrollbarPrepaintState {
 924    fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
 925        self.thumbs
 926            .iter()
 927            .find(|info| info.thumb_bounds.contains(position))
 928    }
 929
 930    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
 931        self.thumbs.iter().find(|info| {
 932            if info.reserved_space.needs_scroll_track() {
 933                info.track_bounds.contains(position)
 934            } else {
 935                info.thumb_bounds.contains(position)
 936            }
 937        })
 938    }
 939}
 940
 941impl PartialEq for ScrollbarPrepaintState {
 942    fn eq(&self, other: &Self) -> bool {
 943        self.thumbs == other.thumbs
 944    }
 945}
 946
 947impl<S: ScrollbarVisibility, T: ScrollableHandle> Element for ScrollbarElement<S, T> {
 948    type RequestLayoutState = ();
 949    type PrepaintState = Option<ScrollbarPrepaintState>;
 950
 951    fn id(&self) -> Option<ElementId> {
 952        None
 953    }
 954
 955    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
 956        None
 957    }
 958
 959    fn request_layout(
 960        &mut self,
 961        _id: Option<&GlobalElementId>,
 962        _inspector_id: Option<&gpui::InspectorElementId>,
 963        window: &mut Window,
 964        cx: &mut App,
 965    ) -> (LayoutId, Self::RequestLayoutState) {
 966        let scrollbar_style = Style {
 967            position: Position::Absolute,
 968            inset: Edges::default(),
 969            size: size(relative(1.), relative(1.)).map(Into::into),
 970            ..Default::default()
 971        };
 972
 973        (window.request_layout(scrollbar_style, None, cx), ())
 974    }
 975
 976    fn prepaint(
 977        &mut self,
 978        _id: Option<&GlobalElementId>,
 979        _inspector_id: Option<&gpui::InspectorElementId>,
 980        bounds: Bounds<Pixels>,
 981        _request_layout: &mut Self::RequestLayoutState,
 982        window: &mut Window,
 983        cx: &mut App,
 984    ) -> Self::PrepaintState {
 985        let prepaint_state = self
 986            .state
 987            .read(cx)
 988            .disabled()
 989            .not()
 990            .then(|| ScrollbarPrepaintState {
 991                parent_bounds: bounds,
 992                thumbs: {
 993                    let thumb_ranges = self.state.read(cx).thumb_ranges().collect::<Vec<_>>();
 994                    let width = self.state.read(cx).width.to_pixels();
 995
 996                    let additional_padding = if thumb_ranges.len() == 2 {
 997                        width
 998                    } else {
 999                        Pixels::ZERO
1000                    };
1001
1002                    thumb_ranges
1003                        .into_iter()
1004                        .map(|(axis, thumb_range, reserved_space)| {
1005                            let track_anchor = match axis {
1006                                ScrollbarAxis::Horizontal => Corner::BottomLeft,
1007                                ScrollbarAxis::Vertical => Corner::TopRight,
1008                            };
1009                            let Bounds { origin, size } = Bounds::from_corner_and_size(
1010                                track_anchor,
1011                                bounds
1012                                    .corner(track_anchor)
1013                                    .apply_along(axis.invert(), |corner| {
1014                                        corner - SCROLLBAR_PADDING
1015                                    }),
1016                                bounds.size.apply_along(axis.invert(), |_| width),
1017                            );
1018                            let scroll_track_bounds = Bounds::new(self.origin + origin, size);
1019
1020                            let padded_bounds = scroll_track_bounds.extend(match axis {
1021                                ScrollbarAxis::Horizontal => Edges {
1022                                    right: -SCROLLBAR_PADDING,
1023                                    left: -SCROLLBAR_PADDING,
1024                                    ..Default::default()
1025                                },
1026                                ScrollbarAxis::Vertical => Edges {
1027                                    top: -SCROLLBAR_PADDING,
1028                                    bottom: -SCROLLBAR_PADDING,
1029                                    ..Default::default()
1030                                },
1031                            });
1032
1033                            let available_space =
1034                                padded_bounds.size.along(axis) - additional_padding;
1035
1036                            let thumb_offset = thumb_range.start * available_space;
1037                            let thumb_end = thumb_range.end * available_space;
1038                            let thumb_bounds = Bounds::new(
1039                                padded_bounds
1040                                    .origin
1041                                    .apply_along(axis, |origin| origin + thumb_offset),
1042                                padded_bounds
1043                                    .size
1044                                    .apply_along(axis, |_| thumb_end - thumb_offset),
1045                            );
1046
1047                            ScrollbarLayout {
1048                                thumb_bounds,
1049                                track_bounds: padded_bounds,
1050                                axis,
1051                                cursor_hitbox: window.insert_hitbox(
1052                                    if reserved_space.needs_scroll_track() {
1053                                        padded_bounds
1054                                    } else {
1055                                        thumb_bounds
1056                                    },
1057                                    HitboxBehavior::BlockMouseExceptScroll,
1058                                ),
1059                                reserved_space,
1060                            }
1061                        })
1062                        .collect()
1063                },
1064            });
1065        if prepaint_state
1066            .as_ref()
1067            .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
1068        {
1069            self.state
1070                .update(cx, |state, cx| state.show_scrollbars(window, cx));
1071        }
1072
1073        prepaint_state
1074    }
1075
1076    fn paint(
1077        &mut self,
1078        _id: Option<&GlobalElementId>,
1079        _inspector_id: Option<&gpui::InspectorElementId>,
1080        Bounds { origin, size }: Bounds<Pixels>,
1081        _request_layout: &mut Self::RequestLayoutState,
1082        prepaint_state: &mut Self::PrepaintState,
1083        window: &mut Window,
1084        cx: &mut App,
1085    ) {
1086        let Some(prepaint_state) = prepaint_state.take() else {
1087            return;
1088        };
1089
1090        let bounds = Bounds::new(self.origin + origin, size);
1091        window.with_content_mask(Some(ContentMask { bounds }), |window| {
1092            let colors = cx.theme().colors();
1093
1094            if self.state.read(cx).visible() {
1095                for ScrollbarLayout {
1096                    thumb_bounds,
1097                    cursor_hitbox,
1098                    axis,
1099                    reserved_space,
1100                    ..
1101                } in &prepaint_state.thumbs
1102                {
1103                    const MAXIMUM_OPACITY: f32 = 0.7;
1104                    let thumb_state = &self.state.read(cx).thumb_state;
1105                    let (thumb_base_color, hovered) = match thumb_state {
1106                        ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1107                            (colors.scrollbar_thumb_active_background, false)
1108                        }
1109                        ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1110                            (colors.scrollbar_thumb_hover_background, true)
1111                        }
1112                        _ => (colors.scrollbar_thumb_background, false),
1113                    };
1114
1115                    let blending_color = if hovered || reserved_space.needs_scroll_track() {
1116                        colors.surface_background
1117                    } else {
1118                        let blend_color = colors.surface_background;
1119                        blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1120                    };
1121
1122                    let thumb_background = blending_color.blend(thumb_base_color);
1123
1124                    window.paint_quad(quad(
1125                        *thumb_bounds,
1126                        Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size),
1127                        thumb_background,
1128                        Edges::default(),
1129                        Hsla::transparent_black(),
1130                        BorderStyle::default(),
1131                    ));
1132
1133                    if thumb_state.is_dragging() {
1134                        window.set_window_cursor_style(CursorStyle::Arrow);
1135                    } else {
1136                        window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1137                    }
1138                }
1139            }
1140
1141            self.state.update(cx, |state, _| {
1142                state.last_prepaint_state = Some(prepaint_state)
1143            });
1144
1145            window.on_mouse_event({
1146                let state = self.state.clone();
1147
1148                move |event: &MouseDownEvent, phase, window, cx| {
1149                    state.update(cx, |state, cx| {
1150                        let Some(scrollbar_layout) = (phase.capture()
1151                            && event.button == MouseButton::Left)
1152                            .then(|| state.hit_for_position(&event.position))
1153                            .flatten()
1154                        else {
1155                            return;
1156                        };
1157
1158                        let ScrollbarLayout {
1159                            thumb_bounds, axis, ..
1160                        } = scrollbar_layout;
1161
1162                        if thumb_bounds.contains(&event.position) {
1163                            let offset =
1164                                event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1165                            state.set_dragging(*axis, offset, window, cx);
1166                        } else {
1167                            let scroll_handle = state.scroll_handle();
1168                            let click_offset = scrollbar_layout.compute_click_offset(
1169                                event.position,
1170                                scroll_handle.max_offset(),
1171                                ScrollbarMouseEvent::TrackClick,
1172                            );
1173                            state.set_offset(
1174                                scroll_handle.offset().apply_along(*axis, |_| click_offset),
1175                                cx,
1176                            );
1177                        };
1178
1179                        cx.stop_propagation();
1180                    });
1181                }
1182            });
1183
1184            window.on_mouse_event({
1185                let state = self.state.clone();
1186
1187                move |event: &ScrollWheelEvent, phase, window, cx| {
1188                    if phase.capture() {
1189                        state.update(cx, |state, cx| {
1190                            state.update_hovered_thumb(&event.position, window, cx)
1191                        });
1192                    }
1193                }
1194            });
1195
1196            window.on_mouse_event({
1197                let state = self.state.clone();
1198
1199                move |event: &MouseMoveEvent, phase, window, cx| {
1200                    if !phase.capture() {
1201                        return;
1202                    }
1203
1204                    match state.read(cx).thumb_state {
1205                        ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1206                            if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1207                                let scroll_handle = state.read(cx).scroll_handle();
1208                                let drag_offset = scrollbar_layout.compute_click_offset(
1209                                    event.position,
1210                                    scroll_handle.max_offset(),
1211                                    ScrollbarMouseEvent::ThumbDrag(drag_state),
1212                                );
1213                                let new_offset =
1214                                    scroll_handle.offset().apply_along(axis, |_| drag_offset);
1215
1216                                state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1217                                cx.stop_propagation();
1218                            }
1219                        }
1220                        _ => state.update(cx, |state, cx| {
1221                            match state.update_parent_hovered(&event.position) {
1222                                ParentHovered::Yes(state_changed)
1223                                    if event.pressed_button.is_none() =>
1224                                {
1225                                    if state_changed {
1226                                        state.show_scrollbars(window, cx);
1227                                    }
1228                                    state.update_hovered_thumb(&event.position, window, cx);
1229                                    if state.thumb_state != ThumbState::Inactive {
1230                                        cx.stop_propagation();
1231                                    }
1232                                }
1233                                ParentHovered::No(state_changed) if state_changed => {
1234                                    state.set_thumb_state(ThumbState::Inactive, window, cx);
1235                                }
1236                                _ => {}
1237                            }
1238                        }),
1239                    }
1240                }
1241            });
1242
1243            window.on_mouse_event({
1244                let state = self.state.clone();
1245                move |event: &MouseUpEvent, phase, window, cx| {
1246                    if !phase.capture() {
1247                        return;
1248                    }
1249
1250                    state.update(cx, |state, cx| {
1251                        if state.is_dragging() {
1252                            state.scroll_handle().drag_ended();
1253                        }
1254
1255                        if !state.parent_hovered(&event.position) {
1256                            state.schedule_auto_hide(window, cx);
1257                            return;
1258                        }
1259
1260                        state.update_hovered_thumb(&event.position, window, cx);
1261                    });
1262                }
1263            });
1264        })
1265    }
1266}
1267
1268impl<S: ScrollbarVisibility, T: ScrollableHandle> IntoElement for ScrollbarElement<S, T> {
1269    type Element = Self;
1270
1271    fn into_element(self) -> Self::Element {
1272        self
1273    }
1274}