Jason Lee 2024-06-30 23:31:16 +08:00 committed by GitHub
parent 29dc803014
commit d183bacc73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 356 additions and 28 deletions

View file

@ -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.

View file

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

View file

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

View file

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

View file

@ -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 {

View file

@ -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;

View file

@ -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
View 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);
}
});
})
}
}

View file

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