theme: Add to support always show scrollbar mode and default to match with system style. (#621)

Close #573

- Fix theme settings to keeping when switch theme mode.
This commit is contained in:
Jason Lee 2025-02-12 15:15:33 +08:00 committed by GitHub
parent ec3d8e50ed
commit 437f1b2098
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 188 additions and 72 deletions

View file

@ -0,0 +1,35 @@
use gpui::*;
use story::{Assets, ScrollableStory};
pub struct Example {
story: Entity<ScrollableStory>,
}
impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let story = ScrollableStory::view(window, cx);
Self { story }
}
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(window, cx))
}
}
impl Render for Example {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> 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);
});
}

View file

@ -6,12 +6,12 @@ use gpui::{
ParentElement, Pixels, Render, ScrollHandle, SharedString, Size, Styled, Window, ParentElement, Pixels, Render, ScrollHandle, SharedString, Size, Styled, Window,
}; };
use gpui_component::{ use gpui_component::{
button::Button, button::{Button, ButtonGroup},
divider::Divider, divider::Divider,
gray_100, gray_800, h_flex, gray_100, gray_800, h_flex,
label::Label, label::Label,
scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}, 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 { pub struct ScrollableStory {
@ -23,6 +23,7 @@ pub struct ScrollableStory {
item_sizes: Rc<Vec<Size<Pixels>>>, item_sizes: Rc<Vec<Size<Pixels>>>,
test_width: Pixels, test_width: Pixels,
axis: ScrollbarAxis, axis: ScrollbarAxis,
size_mode: usize,
message: SharedString, message: SharedString,
} }
@ -46,6 +47,7 @@ impl ScrollableStory {
item_sizes: Rc::new(item_sizes), item_sizes: Rc::new(item_sizes),
test_width, test_width,
axis: ScrollbarAxis::Both, axis: ScrollbarAxis::Both,
size_mode: 0,
message: SharedString::default(), message: SharedString::default(),
} }
} }
@ -55,6 +57,7 @@ impl ScrollableStory {
} }
pub fn change_test_cases(&mut self, n: usize, cx: &mut Context<Self>) { pub fn change_test_cases(&mut self, n: usize, cx: &mut Context<Self>) {
self.size_mode = n;
if n == 0 { if n == 0 {
self.items = (0..5000).map(|i| format!("Item {}", i)).collect::<Vec<_>>(); self.items = (0..5000).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
self.test_width = px(3000.); self.test_width = px(3000.);
@ -98,46 +101,66 @@ impl ScrollableStory {
.child( .child(
h_flex() h_flex()
.gap_2() .gap_2()
.child(Button::new("test-0").label("Size 0").on_click(cx.listener( .child(
|view, _, _, cx| { ButtonGroup::new("test-cases")
view.change_test_cases(0, cx); .child(
}, Button::new("test-0")
))) .label("Size 0")
.child(Button::new("test-1").label("Size 1").on_click(cx.listener( .selected(self.size_mode == 0),
|view, _, _, cx| { )
view.change_test_cases(1, cx); .child(
}, Button::new("test-1")
))) .label("Size 1")
.child(Button::new("test-2").label("Size 2").on_click(cx.listener( .selected(self.size_mode == 1),
|view, _, _, cx| { )
view.change_test_cases(2, cx); .child(
}, Button::new("test-2")
))) .label("Size 2")
.child(Button::new("test-3").label("Size 3").on_click(cx.listener( .selected(self.size_mode == 2),
|view, _, _, cx| { )
view.change_test_cases(3, cx); .child(
}, Button::new("test-3")
))) .label("Size 3")
.selected(self.size_mode == 3),
)
.on_click(cx.listener(|view, clicks: &Vec<usize>, _, 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(Divider::vertical().px_2())
.child( .child(
Button::new("test-axis-both") ButtonGroup::new("scrollbars")
.label("Both Scrollbar") .child(
.on_click(cx.listener(|view, _, _, cx| { Button::new("test-axis-both")
view.change_axis(ScrollbarAxis::Both, cx) .label("Both Scrollbar")
})), .selected(self.axis == ScrollbarAxis::Both),
) )
.child( .child(
Button::new("test-axis-vertical") Button::new("test-axis-vertical")
.label("Vertical") .label("Vertical")
.on_click(cx.listener(|view, _, _, cx| { .selected(self.axis == ScrollbarAxis::Vertical),
view.change_axis(ScrollbarAxis::Vertical, cx) )
})), .child(
) Button::new("test-axis-horizontal")
.child( .label("Horizontal")
Button::new("test-axis-horizontal") .selected(self.axis == ScrollbarAxis::Horizontal),
.label("Horizontal") )
.on_click(cx.listener(|view, _, _, cx| { .on_click(cx.listener(|view, clicks: &Vec<usize>, _, cx| {
view.change_axis(ScrollbarAxis::Horizontal, 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() .gap_4()
.child(self.render_buttons(cx)) .child(self.render_buttons(cx))
.child( .child(
div().w_full().child( div().w_full().flex_1().child(
div().relative().w_full().h(px(350.)).child( div().relative().size_full().child(
v_flex() v_flex()
.id("test-0") .id("test-0")
.relative() .relative()
@ -265,8 +288,8 @@ impl Render for ScrollableStory {
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.w_full() .w_full()
.overflow_hidden()
.max_h(px(400.)) .max_h(px(400.))
.min_h(px(200.))
.child( .child(
v_flex() v_flex()
.p_3() .p_3()

View file

@ -37,6 +37,12 @@ impl AppTitleBar {
let locale_selector = cx.new(|cx| LocaleSelector::new(window, cx)); let locale_selector = cx.new(|cx| LocaleSelector::new(window, cx));
let font_size_selector = cx.new(|cx| FontSizeSelector::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 theme_color_picker = cx.new(|cx| {
let mut picker = ColorPicker::new("theme-color-picker", window, cx) let mut picker = ColorPicker::new("theme-color-picker", window, cx)
.xsmall() .xsmall()
@ -299,6 +305,11 @@ impl Render for FontSizeSelector {
scroll_show == ScrollbarShow::Hover, scroll_show == ScrollbarShow::Hover,
Box::new(SelectScrollbarShow(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), .anchor(Corner::TopRight),
) )

View file

@ -70,6 +70,26 @@ impl ButtonGroup {
/// Sets the on_click handler for the ButtonGroup. /// Sets the on_click handler for the ButtonGroup.
/// ///
/// The handler first argument is a vector of the selected button indices. /// The handler first argument is a vector of the selected button indices.
///
/// The `&Vec<usize>` 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<usize>, _, 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( pub fn on_click(
mut self, mut self,
handler: impl Fn(&Vec<usize>, &mut Window, &mut App) + 'static, handler: impl Fn(&Vec<usize>, &mut Window, &mut App) + 'static,

View file

@ -18,18 +18,24 @@ pub enum ScrollbarShow {
#[default] #[default]
Scrolling, Scrolling,
Hover, Hover,
Always,
} }
impl ScrollbarShow { impl ScrollbarShow {
fn is_hover(&self) -> bool { fn is_hover(&self) -> bool {
matches!(self, Self::Hover) matches!(self, Self::Hover)
} }
fn is_always(&self) -> bool {
matches!(self, Self::Always)
}
} }
const BORDER_WIDTH: Pixels = px(0.); const BORDER_WIDTH: Pixels = px(0.);
const WIDTH: Pixels = px(12.);
const MIN_THUMB_SIZE: f32 = 80.; const MIN_THUMB_SIZE: f32 = 80.;
const THUMB_RADIUS: Pixels = Pixels(3.0); const THUMB_RADIUS: Pixels = Pixels(4.0);
const THUMB_INSET: Pixels = Pixels(4.); const THUMB_INSET: Pixels = Pixels(3.);
const FADE_OUT_DURATION: f32 = 3.0; const FADE_OUT_DURATION: f32 = 3.0;
const FADE_OUT_DELAY: f32 = 2.0; const FADE_OUT_DELAY: f32 = 2.0;
@ -153,6 +159,10 @@ impl ScrollbarState {
} }
fn is_scrollbar_visible(&self) -> bool { 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 { if let Some(last_time) = self.last_scroll_time {
let elapsed = Instant::now().duration_since(last_time).as_secs_f32(); let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
elapsed < FADE_OUT_DURATION elapsed < FADE_OUT_DURATION
@ -195,9 +205,9 @@ impl ScrollbarAxis {
match self { match self {
Self::Vertical => vec![Self::Vertical], Self::Vertical => vec![Self::Vertical],
Self::Horizontal => vec![Self::Horizontal], Self::Horizontal => vec![Self::Horizontal],
// This should keep vertical first, vertical is the primary axis // This should keep Horizontal first, Vertical is the primary axis
// if vertical not need display, then horizontal will not keep right margin. // if Vertical not need display, then Horizontal will not keep right margin.
Self::Both => vec![Self::Vertical, Self::Horizontal], Self::Both => vec![Self::Horizontal, Self::Vertical],
} }
} }
} }
@ -206,8 +216,6 @@ impl ScrollbarAxis {
pub struct Scrollbar { pub struct Scrollbar {
view_id: EntityId, view_id: EntityId,
axis: ScrollbarAxis, axis: ScrollbarAxis,
/// When is vertical, this is the height of the scrollbar.
width: Pixels,
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>, scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
scroll_size: gpui::Size<Pixels>, scroll_size: gpui::Size<Pixels>,
state: Rc<Cell<ScrollbarState>>, state: Rc<Cell<ScrollbarState>>,
@ -231,7 +239,6 @@ impl Scrollbar {
state, state,
axis, axis,
scroll_size, scroll_size,
width: px(12.),
scroll_handle: Rc::new(Box::new(scroll_handle)), scroll_handle: Rc::new(Box::new(scroll_handle)),
max_fps: 120, 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. // 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 { let margin_end = if has_both && !is_vertical {
self.width WIDTH
} else { } else {
px(0.) px(0.)
}; };
@ -478,31 +485,29 @@ impl Element for Scrollbar {
let bounds = Bounds { let bounds = Bounds {
origin: if is_vertical { origin: if is_vertical {
point( point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y)
hitbox.origin.x + hitbox.size.width - self.width,
hitbox.origin.y,
)
} else { } else {
point( point(
hitbox.origin.x, hitbox.origin.x,
hitbox.origin.y + hitbox.size.height - self.width, hitbox.origin.y + hitbox.size.height - WIDTH,
) )
}, },
size: gpui::Size { size: gpui::Size {
width: if is_vertical { width: if is_vertical {
self.width WIDTH
} else { } else {
hitbox.size.width hitbox.size.width
}, },
height: if is_vertical { height: if is_vertical {
hitbox.size.height hitbox.size.height
} else { } else {
self.width WIDTH
}, },
}, },
}; };
let state = self.state.clone(); 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_hover_to_show = cx.theme().scrollbar_show.is_hover();
let is_hovered_on_bar = state.get().hovered_axis == Some(axis); let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
let is_hovered_on_thumb = state.get().hovered_on_thumb == 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) = let (thumb_bg, bar_bg, bar_border, inset, radius) =
if state.get().dragged_axis == Some(axis) { if state.get().dragged_axis == Some(axis) {
Self::style_for_active(cx) 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 { if is_hovered_on_thumb {
Self::style_for_hovered_thumb(cx) Self::style_for_hovered_thumb(cx)
} else { } else {
@ -549,12 +560,12 @@ impl Element for Scrollbar {
let thumb_bounds = if is_vertical { let thumb_bounds = if is_vertical {
Bounds::from_corners( Bounds::from_corners(
point(bounds.origin.x, bounds.origin.y + thumb_start), 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 { } else {
Bounds::from_corners( Bounds::from_corners(
point(bounds.origin.x + thumb_start, bounds.origin.y), 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 { let thumb_fill_bounds = if is_vertical {
@ -564,7 +575,7 @@ impl Element for Scrollbar {
bounds.origin.y + thumb_start + inset, bounds.origin.y + thumb_start + inset,
), ),
point( point(
bounds.origin.x + self.width - inset, bounds.origin.x + WIDTH - inset,
bounds.origin.y + thumb_end - inset, bounds.origin.y + thumb_end - inset,
), ),
) )
@ -576,7 +587,7 @@ impl Element for Scrollbar {
), ),
point( point(
bounds.origin.x + thumb_end - inset, bounds.origin.x + thumb_end - inset,
bounds.origin.y + self.width - inset, bounds.origin.y + WIDTH - inset,
), ),
) )
}; };

View file

@ -1628,11 +1628,11 @@ where
.absolute() .absolute()
.top_0() .top_0()
.size_full() .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| { .when(self.scrollbar_visible.bottom, |this| {
this.child(self.render_horizontal_scrollbar(window, cx)) 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))
}), }),
) )
} }

View file

@ -7,7 +7,8 @@ use gpui::{
use crate::{scroll::ScrollbarShow, Colorize as _}; use crate::{scroll::ScrollbarShow, Colorize as _};
pub fn init(cx: &mut App) { 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 { pub trait ActiveTheme {
@ -244,7 +245,7 @@ impl ThemeColor {
primary_hover: hsl(223.0, 5.9, 15.0), primary_hover: hsl(223.0, 5.9, 15.0),
progress_bar: hsl(223.0, 5.9, 10.0), progress_bar: hsl(223.0, 5.9, 10.0),
ring: hsl(240.0, 5.9, 65.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: hsl(0., 0., 69.).opacity(0.9),
scrollbar_thumb_hover: hsl(0., 0., 59.), scrollbar_thumb_hover: hsl(0., 0., 59.),
secondary: hsl(240.0, 5.9, 96.9), 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::<Theme>().scrollbar_show = ScrollbarShow::Scrolling;
} else {
cx.global_mut::<Theme>().scrollbar_show = ScrollbarShow::Always;
}
}
pub fn change(mode: ThemeMode, window: Option<&mut Window>, cx: &mut App) { pub fn change(mode: ThemeMode, window: Option<&mut Window>, cx: &mut App) {
let colors = match mode { let colors = match mode {
ThemeMode::Light => ThemeColor::light(), ThemeMode::Light => ThemeColor::light(),
ThemeMode::Dark => ThemeColor::dark(), ThemeMode::Dark => ThemeColor::dark(),
}; };
let mut theme = Theme::from(colors); if !cx.has_global::<Theme>() {
theme.mode = mode; let theme = Theme::from(colors);
cx.set_global(theme);
}
let theme = cx.global_mut::<Theme>();
theme.mode = mode;
theme.colors = colors;
cx.set_global(theme);
if let Some(window) = window { if let Some(window) = window {
window.refresh(); window.refresh();
} }