scrollbar.rs

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