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:
parent
ec3d8e50ed
commit
437f1b2098
7 changed files with 188 additions and 72 deletions
35
crates/story/examples/scrollable.rs
Normal file
35
crates/story/examples/scrollable.rs
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue