diff --git a/crates/story/examples/scrollable.rs b/crates/story/examples/scrollable.rs new file mode 100644 index 00000000..4457e86d --- /dev/null +++ b/crates/story/examples/scrollable.rs @@ -0,0 +1,35 @@ +use gpui::*; +use story::{Assets, ScrollableStory}; + +pub struct Example { + story: Entity, +} + +impl Example { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let story = ScrollableStory::view(window, cx); + + Self { story } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().p_4().size_full().child(self.story.clone()) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("Scrollable Example", Example::view, cx); + }); +} diff --git a/crates/story/src/scrollable_story.rs b/crates/story/src/scrollable_story.rs index 9bc93dc9..fd4ca2ab 100644 --- a/crates/story/src/scrollable_story.rs +++ b/crates/story/src/scrollable_story.rs @@ -6,12 +6,12 @@ use gpui::{ ParentElement, Pixels, Render, ScrollHandle, SharedString, Size, Styled, Window, }; use gpui_component::{ - button::Button, + button::{Button, ButtonGroup}, divider::Divider, gray_100, gray_800, h_flex, label::Label, scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}, - v_flex, v_virtual_list, ActiveTheme as _, StyledExt as _, + v_flex, v_virtual_list, ActiveTheme as _, Selectable, StyledExt as _, }; pub struct ScrollableStory { @@ -23,6 +23,7 @@ pub struct ScrollableStory { item_sizes: Rc>>, test_width: Pixels, axis: ScrollbarAxis, + size_mode: usize, message: SharedString, } @@ -46,6 +47,7 @@ impl ScrollableStory { item_sizes: Rc::new(item_sizes), test_width, axis: ScrollbarAxis::Both, + size_mode: 0, message: SharedString::default(), } } @@ -55,6 +57,7 @@ impl ScrollableStory { } pub fn change_test_cases(&mut self, n: usize, cx: &mut Context) { + self.size_mode = n; if n == 0 { self.items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); self.test_width = px(3000.); @@ -98,46 +101,66 @@ impl ScrollableStory { .child( h_flex() .gap_2() - .child(Button::new("test-0").label("Size 0").on_click(cx.listener( - |view, _, _, cx| { - view.change_test_cases(0, cx); - }, - ))) - .child(Button::new("test-1").label("Size 1").on_click(cx.listener( - |view, _, _, cx| { - view.change_test_cases(1, cx); - }, - ))) - .child(Button::new("test-2").label("Size 2").on_click(cx.listener( - |view, _, _, cx| { - view.change_test_cases(2, cx); - }, - ))) - .child(Button::new("test-3").label("Size 3").on_click(cx.listener( - |view, _, _, cx| { - view.change_test_cases(3, cx); - }, - ))) + .child( + ButtonGroup::new("test-cases") + .child( + Button::new("test-0") + .label("Size 0") + .selected(self.size_mode == 0), + ) + .child( + Button::new("test-1") + .label("Size 1") + .selected(self.size_mode == 1), + ) + .child( + Button::new("test-2") + .label("Size 2") + .selected(self.size_mode == 2), + ) + .child( + Button::new("test-3") + .label("Size 3") + .selected(self.size_mode == 3), + ) + .on_click(cx.listener(|view, clicks: &Vec, _, cx| { + if clicks.contains(&0) { + view.change_test_cases(0, cx) + } else if clicks.contains(&1) { + view.change_test_cases(1, cx) + } else if clicks.contains(&2) { + view.change_test_cases(2, cx) + } else if clicks.contains(&3) { + view.change_test_cases(3, cx) + } + })), + ) .child(Divider::vertical().px_2()) .child( - Button::new("test-axis-both") - .label("Both Scrollbar") - .on_click(cx.listener(|view, _, _, cx| { - view.change_axis(ScrollbarAxis::Both, cx) - })), - ) - .child( - Button::new("test-axis-vertical") - .label("Vertical") - .on_click(cx.listener(|view, _, _, cx| { - view.change_axis(ScrollbarAxis::Vertical, cx) - })), - ) - .child( - Button::new("test-axis-horizontal") - .label("Horizontal") - .on_click(cx.listener(|view, _, _, cx| { - view.change_axis(ScrollbarAxis::Horizontal, cx) + ButtonGroup::new("scrollbars") + .child( + Button::new("test-axis-both") + .label("Both Scrollbar") + .selected(self.axis == ScrollbarAxis::Both), + ) + .child( + Button::new("test-axis-vertical") + .label("Vertical") + .selected(self.axis == ScrollbarAxis::Vertical), + ) + .child( + Button::new("test-axis-horizontal") + .label("Horizontal") + .selected(self.axis == ScrollbarAxis::Horizontal), + ) + .on_click(cx.listener(|view, clicks: &Vec, _, cx| { + if clicks.contains(&0) { + view.change_axis(ScrollbarAxis::Both, cx) + } else if clicks.contains(&1) { + view.change_axis(ScrollbarAxis::Vertical, cx) + } else if clicks.contains(&2) { + view.change_axis(ScrollbarAxis::Horizontal, cx) + } })), ), ) @@ -179,8 +202,8 @@ impl Render for ScrollableStory { .gap_4() .child(self.render_buttons(cx)) .child( - div().w_full().child( - div().relative().w_full().h(px(350.)).child( + div().w_full().flex_1().child( + div().relative().size_full().child( v_flex() .id("test-0") .relative() @@ -265,8 +288,8 @@ impl Render for ScrollableStory { .border_1() .border_color(cx.theme().border) .w_full() - .overflow_hidden() .max_h(px(400.)) + .min_h(px(200.)) .child( v_flex() .p_3() diff --git a/crates/story/src/title_bar.rs b/crates/story/src/title_bar.rs index 3e5ba106..e599a0ea 100644 --- a/crates/story/src/title_bar.rs +++ b/crates/story/src/title_bar.rs @@ -37,6 +37,12 @@ impl AppTitleBar { let locale_selector = cx.new(|cx| LocaleSelector::new(window, cx)); let font_size_selector = cx.new(|cx| FontSizeSelector::new(window, cx)); + if cx.should_auto_hide_scrollbars() { + Theme::global_mut(cx).scrollbar_show = ScrollbarShow::Scrolling; + } else { + Theme::global_mut(cx).scrollbar_show = ScrollbarShow::Always; + } + let theme_color_picker = cx.new(|cx| { let mut picker = ColorPicker::new("theme-color-picker", window, cx) .xsmall() @@ -299,6 +305,11 @@ impl Render for FontSizeSelector { scroll_show == ScrollbarShow::Hover, Box::new(SelectScrollbarShow(ScrollbarShow::Hover)), ) + .menu_with_check( + "Always show Scrollbar", + scroll_show == ScrollbarShow::Always, + Box::new(SelectScrollbarShow(ScrollbarShow::Always)), + ) }) .anchor(Corner::TopRight), ) diff --git a/crates/ui/src/button/button_group.rs b/crates/ui/src/button/button_group.rs index ddd0fea1..fc395799 100644 --- a/crates/ui/src/button/button_group.rs +++ b/crates/ui/src/button/button_group.rs @@ -70,6 +70,26 @@ impl ButtonGroup { /// Sets the on_click handler for the ButtonGroup. /// /// The handler first argument is a vector of the selected button indices. + /// + /// The `&Vec` is the indices of the clicked (selected in `multiple` mode) buttons. + /// For example: `[0, 2, 3]` is means the first, third and fourth buttons are clicked. + /// + /// ```rust + /// ButtonGroup::new("size-button") + /// .child(Button::new("large").label("Large").selected(self.size == Size::Large)) + /// .child(Button::new("medium").label("Medium").selected(self.size == Size::Medium)) + /// .child(Button::new("small").label("Small").selected(self.size == Size::Small)) + /// .on_click(cx.listener(|view, clicks: &Vec, _, cx| { + /// if clicks.contains(&0) { + /// view.size = Size::Large; + /// } else if clicks.contains(&1) { + /// view.size = Size::Medium; + /// } else if clicks.contains(&2) { + /// view.size = Size::Small; + /// } + /// cx.notify(); + /// })) + /// ``` pub fn on_click( mut self, handler: impl Fn(&Vec, &mut Window, &mut App) + 'static, diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index b2e16081..bae1c07d 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -18,18 +18,24 @@ pub enum ScrollbarShow { #[default] Scrolling, Hover, + Always, } impl ScrollbarShow { fn is_hover(&self) -> bool { matches!(self, Self::Hover) } + + fn is_always(&self) -> bool { + matches!(self, Self::Always) + } } const BORDER_WIDTH: Pixels = px(0.); +const WIDTH: Pixels = px(12.); const MIN_THUMB_SIZE: f32 = 80.; -const THUMB_RADIUS: Pixels = Pixels(3.0); -const THUMB_INSET: Pixels = Pixels(4.); +const THUMB_RADIUS: Pixels = Pixels(4.0); +const THUMB_INSET: Pixels = Pixels(3.); const FADE_OUT_DURATION: f32 = 3.0; const FADE_OUT_DELAY: f32 = 2.0; @@ -153,6 +159,10 @@ impl ScrollbarState { } fn is_scrollbar_visible(&self) -> bool { + if self.hovered_axis.is_some() || self.dragged_axis.is_some() { + return true; + } + if let Some(last_time) = self.last_scroll_time { let elapsed = Instant::now().duration_since(last_time).as_secs_f32(); elapsed < FADE_OUT_DURATION @@ -195,9 +205,9 @@ impl ScrollbarAxis { match self { Self::Vertical => vec![Self::Vertical], Self::Horizontal => vec![Self::Horizontal], - // This should keep vertical first, vertical is the primary axis - // if vertical not need display, then horizontal will not keep right margin. - Self::Both => vec![Self::Vertical, Self::Horizontal], + // This should keep Horizontal first, Vertical is the primary axis + // if Vertical not need display, then Horizontal will not keep right margin. + Self::Both => vec![Self::Horizontal, Self::Vertical], } } } @@ -206,8 +216,6 @@ impl ScrollbarAxis { pub struct Scrollbar { view_id: EntityId, axis: ScrollbarAxis, - /// When is vertical, this is the height of the scrollbar. - width: Pixels, scroll_handle: Rc>, scroll_size: gpui::Size, state: Rc>, @@ -231,7 +239,6 @@ impl Scrollbar { state, axis, scroll_size, - width: px(12.), scroll_handle: Rc::new(Box::new(scroll_handle)), max_fps: 120, } @@ -459,7 +466,7 @@ impl Element for Scrollbar { // The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible. let margin_end = if has_both && !is_vertical { - self.width + WIDTH } else { px(0.) }; @@ -478,31 +485,29 @@ impl Element for Scrollbar { let bounds = Bounds { origin: if is_vertical { - point( - hitbox.origin.x + hitbox.size.width - self.width, - hitbox.origin.y, - ) + point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y) } else { point( hitbox.origin.x, - hitbox.origin.y + hitbox.size.height - self.width, + hitbox.origin.y + hitbox.size.height - WIDTH, ) }, size: gpui::Size { width: if is_vertical { - self.width + WIDTH } else { hitbox.size.width }, height: if is_vertical { hitbox.size.height } else { - self.width + WIDTH }, }, }; let state = self.state.clone(); + let is_always_to_show = cx.theme().scrollbar_show.is_always(); let is_hover_to_show = cx.theme().scrollbar_show.is_hover(); let is_hovered_on_bar = state.get().hovered_axis == Some(axis); let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis); @@ -510,7 +515,13 @@ impl Element for Scrollbar { let (thumb_bg, bar_bg, bar_border, inset, radius) = if state.get().dragged_axis == Some(axis) { Self::style_for_active(cx) - } else if is_hover_to_show && is_hovered_on_bar { + } else if is_hover_to_show && (is_hovered_on_bar || is_hovered_on_thumb) { + if is_hovered_on_thumb { + Self::style_for_hovered_thumb(cx) + } else { + Self::style_for_hovered_bar(cx) + } + } else if is_always_to_show { if is_hovered_on_thumb { Self::style_for_hovered_thumb(cx) } else { @@ -549,12 +560,12 @@ impl Element for Scrollbar { let thumb_bounds = if is_vertical { Bounds::from_corners( point(bounds.origin.x, bounds.origin.y + thumb_start), - point(bounds.origin.x + self.width, bounds.origin.y + thumb_end), + point(bounds.origin.x + WIDTH, bounds.origin.y + thumb_end), ) } else { Bounds::from_corners( point(bounds.origin.x + thumb_start, bounds.origin.y), - point(bounds.origin.x + thumb_end, bounds.origin.y + self.width), + point(bounds.origin.x + thumb_end, bounds.origin.y + WIDTH), ) }; let thumb_fill_bounds = if is_vertical { @@ -564,7 +575,7 @@ impl Element for Scrollbar { bounds.origin.y + thumb_start + inset, ), point( - bounds.origin.x + self.width - inset, + bounds.origin.x + WIDTH - inset, bounds.origin.y + thumb_end - inset, ), ) @@ -576,7 +587,7 @@ impl Element for Scrollbar { ), point( bounds.origin.x + thumb_end - inset, - bounds.origin.y + self.width - inset, + bounds.origin.y + WIDTH - inset, ), ) }; diff --git a/crates/ui/src/table.rs b/crates/ui/src/table.rs index 62f3e607..f0101d43 100644 --- a/crates/ui/src/table.rs +++ b/crates/ui/src/table.rs @@ -1628,11 +1628,11 @@ where .absolute() .top_0() .size_full() - .when(self.scrollbar_visible.right && rows_count > 0, |this| { - this.children(self.render_vertical_scrollbar(window, cx)) - }) .when(self.scrollbar_visible.bottom, |this| { this.child(self.render_horizontal_scrollbar(window, cx)) + }) + .when(self.scrollbar_visible.right && rows_count > 0, |this| { + this.children(self.render_vertical_scrollbar(window, cx)) }), ) } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index f701f4ef..a8984349 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -7,7 +7,8 @@ use gpui::{ use crate::{scroll::ScrollbarShow, Colorize as _}; pub fn init(cx: &mut App) { - Theme::sync_system_appearance(None, cx) + Theme::sync_system_appearance(None, cx); + Theme::sync_scrollbar_appearance(cx); } pub trait ActiveTheme { @@ -244,7 +245,7 @@ impl ThemeColor { primary_hover: hsl(223.0, 5.9, 15.0), progress_bar: hsl(223.0, 5.9, 10.0), ring: hsl(240.0, 5.9, 65.0), - scrollbar: hsl(0., 0., 97.).opacity(0.75), + scrollbar: hsl(0., 0., 92.).opacity(0.75), scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9), scrollbar_thumb_hover: hsl(0., 0., 59.), secondary: hsl(240.0, 5.9, 96.9), @@ -487,16 +488,31 @@ impl Theme { } } + /// Sync the Scrollbar showing behavior with the system + pub fn sync_scrollbar_appearance(cx: &mut App) { + if cx.should_auto_hide_scrollbars() { + cx.global_mut::().scrollbar_show = ScrollbarShow::Scrolling; + } else { + cx.global_mut::().scrollbar_show = ScrollbarShow::Always; + } + } + pub fn change(mode: ThemeMode, window: Option<&mut Window>, cx: &mut App) { let colors = match mode { ThemeMode::Light => ThemeColor::light(), ThemeMode::Dark => ThemeColor::dark(), }; - let mut theme = Theme::from(colors); - theme.mode = mode; + if !cx.has_global::() { + let theme = Theme::from(colors); + cx.set_global(theme); + } + + let theme = cx.global_mut::(); + + theme.mode = mode; + theme.colors = colors; - cx.set_global(theme); if let Some(window) = window { window.refresh(); }