scrollbar.rs

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