Add Scrollbar (#9)
https://github.com/huacnlee/gpui-component/assets/5518/5e4b7809-f2d0-4e45-a0f2-b9578323a11e
This commit is contained in:
parent
29dc803014
commit
d183bacc73
9 changed files with 356 additions and 28 deletions
|
|
@ -60,6 +60,7 @@ There have a part of UI components from [Zed](https://github.com/zed-industries/
|
||||||
|
|
||||||
- title_bar
|
- title_bar
|
||||||
- picker
|
- picker
|
||||||
|
- scrollbar
|
||||||
|
|
||||||
> I think we can discuss them with Zed team to change the license to MIT or Apache License for share to community.
|
> I think we can discuss them with Zed team to change the license to MIT or Apache License for share to community.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use core::time;
|
use core::time;
|
||||||
use std::{cell::RefCell, fmt::format, rc::Rc};
|
|
||||||
|
|
||||||
use fake::Fake;
|
use fake::Fake;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, impl_actions, prelude::FluentBuilder as _, px, Action, ElementId,
|
actions, div, prelude::FluentBuilder as _, px, ElementId, InteractiveElement, IntoElement,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, Styled, Timer, View,
|
ParentElement, Render, RenderOnce, Styled, Timer, View, ViewContext, VisualContext,
|
||||||
ViewContext, VisualContext, WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ui::{
|
use ui::{
|
||||||
|
|
@ -86,7 +85,6 @@ impl RenderOnce for CompanyListItem {
|
||||||
self.base
|
self.base
|
||||||
.px_3()
|
.px_3()
|
||||||
.py_1()
|
.py_1()
|
||||||
.rounded_lg()
|
|
||||||
.overflow_x_hidden()
|
.overflow_x_hidden()
|
||||||
.bg(bg_color)
|
.bg(bg_color)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -210,6 +208,7 @@ impl ListStory {
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
// .max_height(Some(px(350.0).into()))
|
||||||
.no_query()
|
.no_query()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -218,7 +217,7 @@ impl ListStory {
|
||||||
loop {
|
loop {
|
||||||
Timer::after(time::Duration::from_secs_f64(0.5)).await;
|
Timer::after(time::Duration::from_secs_f64(0.5)).await;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.company_list.update(cx, |picker, cx| {
|
this.company_list.update(cx, |picker, _| {
|
||||||
picker
|
picker
|
||||||
.delegate_mut()
|
.delegate_mut()
|
||||||
.companies
|
.companies
|
||||||
|
|
@ -257,13 +256,13 @@ impl Render for ListStory {
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.mb_4()
|
.mb_4()
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.h_full()
|
.h_full()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.p_1()
|
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.w(px(400.))
|
.w(px(400.))
|
||||||
|
.occlude()
|
||||||
.child(self.company_list.clone()),
|
.child(self.company_list.clone()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -280,7 +279,15 @@ impl Render for ListStory {
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(div().text_3xl().mb_6().child(company.name.clone()))
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.items_start()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
div().text_3xl().mb_6().child(company.name.clone()),
|
||||||
|
)
|
||||||
|
.child(format!("{:.2}", company.last_done)),
|
||||||
|
)
|
||||||
.child(company.industry.clone())
|
.child(company.industry.clone())
|
||||||
.child(company.description.clone()),
|
.child(company.description.clone()),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,8 @@ impl PickerStory {
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.modal(true)
|
.modal(true);
|
||||||
.max_height(Some(px(350.0).into()));
|
|
||||||
picker.focus(cx);
|
picker.focus(cx);
|
||||||
picker.set_query("c", cx);
|
picker.set_query("c", cx);
|
||||||
picker
|
picker
|
||||||
|
|
@ -213,12 +213,13 @@ impl Render for PickerStory {
|
||||||
.when(self.open, |this| {
|
.when(self.open, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div().absolute().size_full().top_0().left_0().child(
|
div().absolute().size_full().top_0().left_0().child(
|
||||||
v_flex()
|
v_flex().top_10().flex().flex_col().items_center().child(
|
||||||
.top_10()
|
div()
|
||||||
.flex()
|
.w(px(450.))
|
||||||
.flex_col()
|
.h(px(350.))
|
||||||
.items_center()
|
.occlude()
|
||||||
.child(h_flex().w(px(450.)).occlude().child(self.picker.clone())),
|
.child(self.picker.clone()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -266,9 +266,9 @@ impl ButtonStyle {
|
||||||
|
|
||||||
fn hovered(&self, cx: &WindowContext) -> ButtonStyles {
|
fn hovered(&self, cx: &WindowContext) -> ButtonStyles {
|
||||||
// Hover color = color/90
|
// Hover color = color/90
|
||||||
let bg = self.bg_color(cx).divide(0.9);
|
let bg = self.bg_color(cx);
|
||||||
let border = self.border_color(cx).divide(0.9);
|
let border = self.border_color(cx);
|
||||||
let fg = self.text_color(cx).divide(0.9);
|
let fg = self.text_color(cx);
|
||||||
|
|
||||||
ButtonStyles { bg, border, fg }
|
ButtonStyles { bg, border, fg }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ where
|
||||||
let picker = cx.new_view(|cx| {
|
let picker = cx.new_view(|cx| {
|
||||||
Picker::uniform_list(picker_delegate, cx)
|
Picker::uniform_list(picker_delegate, cx)
|
||||||
.no_query()
|
.no_query()
|
||||||
|
.scrollbar_enable(false)
|
||||||
.max_height(Some(rems(20.).into()))
|
.max_height(Some(rems(20.).into()))
|
||||||
});
|
});
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ mod event;
|
||||||
mod icon;
|
mod icon;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod prelude;
|
mod prelude;
|
||||||
|
mod scrollbar;
|
||||||
mod selectable;
|
mod selectable;
|
||||||
mod stock;
|
mod stock;
|
||||||
mod styled_ext;
|
mod styled_ext;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
use std::{cell::Cell, rc::Rc, time::Duration};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, list, prelude::FluentBuilder as _, px, rems, uniform_list, AppContext,
|
actions, div, list, prelude::FluentBuilder as _, px, uniform_list, AppContext, ClickEvent,
|
||||||
ClickEvent, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement,
|
||||||
IntoElement, KeyBinding, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent,
|
KeyBinding, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent,
|
||||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _, Task,
|
ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _, Task,
|
||||||
UniformListScrollHandle, View, ViewContext, VisualContext as _, WindowContext,
|
UniformListScrollHandle, View, ViewContext, VisualContext as _, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
@ -34,6 +36,7 @@ use crate::{
|
||||||
divider::Divider,
|
divider::Divider,
|
||||||
empty::Empty,
|
empty::Empty,
|
||||||
input::{TextEvent, TextInput},
|
input::{TextEvent, TextInput},
|
||||||
|
scrollbar::Scrollbar,
|
||||||
stock::*,
|
stock::*,
|
||||||
theme::ActiveTheme,
|
theme::ActiveTheme,
|
||||||
StyledExt as _,
|
StyledExt as _,
|
||||||
|
|
@ -149,6 +152,10 @@ pub struct Picker<D: PickerDelegate> {
|
||||||
/// Just a empty view for holding the focus
|
/// Just a empty view for holding the focus
|
||||||
head: View<Empty>,
|
head: View<Empty>,
|
||||||
pending_update_matches: Option<PendingUpdateMatches>,
|
pending_update_matches: Option<PendingUpdateMatches>,
|
||||||
|
scrollbar_enable: bool,
|
||||||
|
show_scrollbar: bool,
|
||||||
|
hide_scrollbar_task: Option<Task<()>>,
|
||||||
|
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: PickerDelegate> Picker<D> {
|
impl<D: PickerDelegate> Picker<D> {
|
||||||
|
|
@ -190,6 +197,10 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
max_height: None,
|
max_height: None,
|
||||||
element_container,
|
element_container,
|
||||||
pending_update_matches: None,
|
pending_update_matches: None,
|
||||||
|
scrollbar_enable: true,
|
||||||
|
show_scrollbar: false,
|
||||||
|
hide_scrollbar_task: None,
|
||||||
|
scrollbar_drag_state: Rc::new(Cell::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,6 +260,11 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scrollbar_enable(mut self, enable: bool) -> Self {
|
||||||
|
self.scrollbar_enable = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(input) = &self.query_input {
|
if let Some(input) = &self.query_input {
|
||||||
input.update(cx, |this, cx| this.set_text(query.to_string(), cx));
|
input.update(cx, |this, cx| this.set_text(query.to_string(), cx));
|
||||||
|
|
@ -327,6 +343,46 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Scrollbar> {
|
||||||
|
if !self.scrollbar_enable {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !self.show_scrollbar {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(scroll_handle) = match &self.element_container {
|
||||||
|
ElementContainer::List(_state) => None,
|
||||||
|
ElementContainer::UniformList(scroll_handle) => Some(scroll_handle.clone()),
|
||||||
|
} {
|
||||||
|
Scrollbar::new(
|
||||||
|
cx.view().clone().into(),
|
||||||
|
scroll_handle,
|
||||||
|
self.scrollbar_drag_state.clone(),
|
||||||
|
self.delegate.match_count(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
self.show_scrollbar = false;
|
||||||
|
self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(SCROLLBAR_SHOW_INTERVAL)
|
||||||
|
.await;
|
||||||
|
panel
|
||||||
|
.update(&mut cx, |panel, cx| {
|
||||||
|
panel.show_scrollbar = false;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn on_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
|
fn on_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.prevent_default();
|
cx.prevent_default();
|
||||||
|
|
@ -487,7 +543,9 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.id("picker")
|
||||||
.key_context("Picker")
|
.key_context("Picker")
|
||||||
|
.group("picker-group")
|
||||||
.size_full()
|
.size_full()
|
||||||
.track_focus(&focus_handle)
|
.track_focus(&focus_handle)
|
||||||
.when_some(self.width, |el, width| el.w(width))
|
.when_some(self.width, |el, width| el.w(width))
|
||||||
|
|
@ -499,19 +557,41 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
.on_action(cx.listener(Self::select_last))
|
.on_action(cx.listener(Self::select_last))
|
||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
|
.on_hover(cx.listener(move |this, hovered: &bool, cx| {
|
||||||
|
if *hovered {
|
||||||
|
this.show_scrollbar = true;
|
||||||
|
this.hide_scrollbar_task.take();
|
||||||
|
cx.notify();
|
||||||
|
} else if !focus_handle.is_focused(cx) {
|
||||||
|
this.hide_scrollbar(cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
// Render Query Input header
|
// Render Query Input header
|
||||||
.child(match self.query_input {
|
.child(match self.query_input {
|
||||||
Some(ref input) => self.delegate.render_query(input, cx),
|
Some(ref input) => self.delegate.render_query(input, cx),
|
||||||
None => div().child(self.head.clone()),
|
None => div().child(self.head.clone()),
|
||||||
})
|
})
|
||||||
.when(self.delegate.match_count() > 0, |el| {
|
.when(self.delegate.match_count() > 0, |this| {
|
||||||
el.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.min_h(px(100.))
|
.min_h(px(100.))
|
||||||
.when_some(self.max_height, |div, max_h| div.max_h(max_h))
|
.when_some(self.max_height, |div, max_h| div.max_h(max_h))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(self.render_element_container(cx)),
|
.child(self.render_element_container(cx))
|
||||||
|
.children(self.render_scrollbar(cx).map(|bar| {
|
||||||
|
// This for let the scrollbar can render on top of the list container.
|
||||||
|
div()
|
||||||
|
.occlude()
|
||||||
|
.absolute()
|
||||||
|
.h_full()
|
||||||
|
.left_auto()
|
||||||
|
.top_0()
|
||||||
|
.right_0()
|
||||||
|
.w(px(bar.width()))
|
||||||
|
.bottom_0()
|
||||||
|
.child(bar)
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(self.delegate.match_count() == 0, |el| {
|
.when(self.delegate.match_count() == 0, |el| {
|
||||||
|
|
|
||||||
227
crates/ui/src/scrollbar.rs
Normal file
227
crates/ui/src/scrollbar.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
use std::{cell::Cell, ops::Range, rc::Rc};
|
||||||
|
|
||||||
|
use crate::theme::{ActiveTheme, Colorize};
|
||||||
|
use gpui::{
|
||||||
|
fill, point, px, relative, AnyView, Bounds, ContentMask, Element, Hitbox, IntoElement,
|
||||||
|
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, Style,
|
||||||
|
UniformListScrollHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN_THUMB_PERCENTAGE_HEIGHT: f64 = 0.03;
|
||||||
|
const THUMB_RADIUS: Pixels = Pixels(5.0);
|
||||||
|
const THUMB_INSET: Pixels = Pixels(0.8);
|
||||||
|
|
||||||
|
pub struct Scrollbar {
|
||||||
|
width: f32,
|
||||||
|
view: AnyView,
|
||||||
|
handle: UniformListScrollHandle,
|
||||||
|
/// This is the state of the scrollbar thumb when it is being dragged.
|
||||||
|
/// It must ref from the parent view.
|
||||||
|
drag_state: Rc<Cell<Option<f32>>>,
|
||||||
|
items_count: usize,
|
||||||
|
thumb: Range<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scrollbar {
|
||||||
|
pub fn new(
|
||||||
|
view: AnyView,
|
||||||
|
handle: UniformListScrollHandle,
|
||||||
|
drag_state: Rc<Cell<Option<f32>>>,
|
||||||
|
items_count: usize,
|
||||||
|
show_scrollbar: bool,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let cloned_handle = handle.clone();
|
||||||
|
|
||||||
|
// Ref from: Zed crates\project_panel\src\project_panel.rs
|
||||||
|
// render_scrollbar method
|
||||||
|
let scroll_state = handle.0.borrow();
|
||||||
|
let last_item_height = scroll_state
|
||||||
|
.last_item_height
|
||||||
|
.filter(|_| show_scrollbar && items_count > 0)?;
|
||||||
|
let list_height = items_count as f64 * last_item_height.0 as f64;
|
||||||
|
let current_offset = scroll_state.base_handle.offset().y.0.min(0.).abs() as f64;
|
||||||
|
let mut percentage = current_offset / list_height;
|
||||||
|
let end_offset: f64 =
|
||||||
|
(current_offset + scroll_state.base_handle.bounds().size.height.0 as f64) / list_height;
|
||||||
|
let overshoot = (end_offset - 1.).clamp(0., 1.);
|
||||||
|
if overshoot > 0. {
|
||||||
|
percentage -= overshoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if percentage + MIN_THUMB_PERCENTAGE_HEIGHT > 1.0 || end_offset > list_height {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if list_height < scroll_state.base_handle.bounds().size.height.0 as f64 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end_offset = end_offset.clamp(percentage + MIN_THUMB_PERCENTAGE_HEIGHT, 1.);
|
||||||
|
|
||||||
|
let thumb = percentage as f32..end_offset as f32;
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
view,
|
||||||
|
items_count,
|
||||||
|
width: 12.0,
|
||||||
|
thumb,
|
||||||
|
handle: cloned_handle,
|
||||||
|
drag_state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> f32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for Scrollbar {
|
||||||
|
type Element = Self;
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScrollbarState {}
|
||||||
|
|
||||||
|
impl Element for Scrollbar {
|
||||||
|
type RequestLayoutState = ScrollbarState;
|
||||||
|
|
||||||
|
type PrepaintState = Hitbox;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<gpui::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
cx: &mut gpui::WindowContext,
|
||||||
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.flex_grow = 0.0;
|
||||||
|
style.flex_shrink = 1.;
|
||||||
|
style.size.width = px(self.width).into();
|
||||||
|
style.size.height = relative(1.).into();
|
||||||
|
|
||||||
|
(cx.request_layout(style, None), ScrollbarState {})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
bounds: gpui::Bounds<gpui::Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
cx: &mut gpui::WindowContext,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||||
|
cx.insert_hitbox(bounds, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
bounds: gpui::Bounds<gpui::Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
cx: &mut gpui::WindowContext,
|
||||||
|
) {
|
||||||
|
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||||
|
let is_draging = self.drag_state.get().is_some();
|
||||||
|
let bar_bg = cx.theme().scrollbar;
|
||||||
|
let thumb_bg = cx.theme().scrollbar_thumb;
|
||||||
|
let thumb_bg = if is_draging {
|
||||||
|
thumb_bg.darken(0.1)
|
||||||
|
} else {
|
||||||
|
thumb_bg
|
||||||
|
};
|
||||||
|
|
||||||
|
let thumb_top = self.thumb.start * bounds.size.height;
|
||||||
|
let thumb_bottom = self.thumb.end * bounds.size.height;
|
||||||
|
let thumb_percentage_size = self.thumb.end - self.thumb.start;
|
||||||
|
let thumb_bounds = Bounds::from_corners(
|
||||||
|
point(
|
||||||
|
bounds.origin.x + THUMB_INSET,
|
||||||
|
bounds.origin.y + thumb_top + THUMB_INSET,
|
||||||
|
),
|
||||||
|
point(
|
||||||
|
bounds.origin.x + bounds.size.width - (THUMB_INSET * 2),
|
||||||
|
bounds.origin.y + thumb_bottom - (THUMB_INSET * 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.paint_quad(fill(bounds, bar_bg));
|
||||||
|
cx.paint_quad(fill(thumb_bounds, thumb_bg).corner_radii(THUMB_RADIUS));
|
||||||
|
|
||||||
|
let handle = self.handle.clone();
|
||||||
|
let items_count = self.items_count;
|
||||||
|
|
||||||
|
let drag_state = self.drag_state.clone();
|
||||||
|
cx.on_mouse_event({
|
||||||
|
let scroll = self.handle.clone();
|
||||||
|
move |event: &MouseDownEvent, phase, _cx| {
|
||||||
|
if phase.bubble() && bounds.contains(&event.position) {
|
||||||
|
if !thumb_bounds.contains(&event.position) {
|
||||||
|
let scroll = scroll.0.borrow();
|
||||||
|
if let Some(last_height) = scroll.last_item_height {
|
||||||
|
let max_offset = items_count as f32 * last_height;
|
||||||
|
let percentage =
|
||||||
|
(event.position.y - bounds.origin.y) / bounds.size.height;
|
||||||
|
|
||||||
|
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||||
|
scroll
|
||||||
|
.base_handle
|
||||||
|
.set_offset(point(px(0.), -max_offset * percentage));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let thumb_top_offset =
|
||||||
|
(event.position.y - thumb_bounds.origin.y) / bounds.size.height;
|
||||||
|
drag_state.set(Some(thumb_top_offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.on_mouse_event({
|
||||||
|
let scroll = self.handle.clone();
|
||||||
|
move |event: &ScrollWheelEvent, phase, cx| {
|
||||||
|
if phase.bubble() && bounds.contains(&event.position) {
|
||||||
|
let scroll = scroll.0.borrow_mut();
|
||||||
|
let current_offset = scroll.base_handle.offset();
|
||||||
|
scroll
|
||||||
|
.base_handle
|
||||||
|
.set_offset(current_offset + event.delta.pixel_delta(cx.line_height()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let view_id = self.view.entity_id();
|
||||||
|
let drag_state = self.drag_state.clone();
|
||||||
|
cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
|
||||||
|
if let Some(drag_state) = drag_state.get().filter(|_| event.dragging()) {
|
||||||
|
let scroll = handle.0.borrow();
|
||||||
|
if let Some(last_height) = scroll.last_item_height {
|
||||||
|
let max_offset = items_count as f32 * last_height;
|
||||||
|
let percentage =
|
||||||
|
(event.position.y - bounds.origin.y) / bounds.size.height - drag_state;
|
||||||
|
|
||||||
|
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||||
|
scroll
|
||||||
|
.base_handle
|
||||||
|
.set_offset(point(px(0.), -max_offset * percentage));
|
||||||
|
cx.notify(view_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
drag_state.set(None);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let drag_state = self.drag_state.clone();
|
||||||
|
cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
|
||||||
|
if phase.bubble() {
|
||||||
|
drag_state.set(None);
|
||||||
|
cx.notify(view_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,13 +68,13 @@ impl Colorize for Hsla {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lighten(&self, amount: f32) -> Hsla {
|
fn lighten(&self, amount: f32) -> Hsla {
|
||||||
let l = (self.l + amount).min(1.0);
|
let l = (self.l * (1.0 + amount)).min(1.0);
|
||||||
|
|
||||||
Hsla { l, ..*self }
|
Hsla { l, ..*self }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn darken(&self, amount: f32) -> Hsla {
|
fn darken(&self, amount: f32) -> Hsla {
|
||||||
let l = (self.l - amount).max(0.0);
|
let l = (self.l * (1.0 - amount)).max(0.0);
|
||||||
|
|
||||||
Hsla { l, ..*self }
|
Hsla { l, ..*self }
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +104,8 @@ struct Colors {
|
||||||
pub input: Hsla,
|
pub input: Hsla,
|
||||||
pub ring: Hsla,
|
pub ring: Hsla,
|
||||||
pub selection: Hsla,
|
pub selection: Hsla,
|
||||||
|
pub scrollbar: Hsla,
|
||||||
|
pub scrollbar_thumb: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Colors {
|
impl Colors {
|
||||||
|
|
@ -154,6 +156,8 @@ impl Colors {
|
||||||
input: hsl(240.0, 5.9, 90.0),
|
input: hsl(240.0, 5.9, 90.0),
|
||||||
ring: hsl(240.0, 5.9, 10.0),
|
ring: hsl(240.0, 5.9, 10.0),
|
||||||
selection: hsl(211.0, 97.0, 85.0),
|
selection: hsl(211.0, 97.0, 85.0),
|
||||||
|
scrollbar: Hsla::transparent_black(),
|
||||||
|
scrollbar_thumb: hsl(240.0, 5.9, 90.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +207,8 @@ impl Colors {
|
||||||
input: hsl(240.0, 3.7, 15.9),
|
input: hsl(240.0, 3.7, 15.9),
|
||||||
ring: hsl(240.0, 4.9, 83.9),
|
ring: hsl(240.0, 4.9, 83.9),
|
||||||
selection: hsl(211.0, 97.0, 85.0),
|
selection: hsl(211.0, 97.0, 85.0),
|
||||||
|
scrollbar: Hsla::transparent_black(),
|
||||||
|
scrollbar_thumb: hsl(240.0, 3.7, 15.9),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +240,8 @@ pub struct Theme {
|
||||||
pub input: Hsla,
|
pub input: Hsla,
|
||||||
pub ring: Hsla,
|
pub ring: Hsla,
|
||||||
pub selection: Hsla,
|
pub selection: Hsla,
|
||||||
|
pub scrollbar: Hsla,
|
||||||
|
pub scrollbar_thumb: Hsla,
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +280,8 @@ impl From<Colors> for Theme {
|
||||||
border: colors.border,
|
border: colors.border,
|
||||||
input: colors.input,
|
input: colors.input,
|
||||||
ring: colors.ring,
|
ring: colors.ring,
|
||||||
|
scrollbar: colors.scrollbar,
|
||||||
|
scrollbar_thumb: colors.scrollbar_thumb,
|
||||||
selection: colors.selection,
|
selection: colors.selection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue