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,
};
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<Vec<Size<Pixels>>>,
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>) {
self.size_mode = n;
if n == 0 {
self.items = (0..5000).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
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<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(
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<usize>, _, 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()

View file

@ -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),
)

View file

@ -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<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(
mut self,
handler: impl Fn(&Vec<usize>, &mut Window, &mut App) + 'static,

View file

@ -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<Box<dyn ScrollHandleOffsetable>>,
scroll_size: gpui::Size<Pixels>,
state: Rc<Cell<ScrollbarState>>,
@ -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,
),
)
};

View file

@ -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))
}),
)
}

View file

@ -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::<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) {
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::<Theme>() {
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 {
window.refresh();
}