Jason Lee 2024-07-16 12:00:51 +08:00 committed by GitHub
parent b1bb22fd1d
commit 2b184ca8d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 828 additions and 413 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"rust-analyzer.check.command": "clippy"
}

26
Cargo.lock generated
View file

@ -2246,6 +2246,19 @@ dependencies = [
"xkbcommon",
]
[[package]]
name = "gpui-app"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"log",
"rust-embed",
"story",
"ui",
"workspace",
]
[[package]]
name = "gpui_macros"
version = "0.1.0"
@ -2898,19 +2911,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "main-app"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"log",
"rust-embed",
"story",
"ui",
"workspace",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"

View file

@ -91,7 +91,6 @@ cargo run
There have a part of UI components from [Zed](https://github.com/zed-industries/zed/tree/main/crates/ui), that are under GPL v3.0 license.
- workspace
- scrollbar
Other UI components are under Apache License.

View file

@ -1,5 +1,5 @@
[package]
name = "main-app"
name = "gpui-app"
version = "0.1.0"
edition = "2021"

View file

@ -2,8 +2,8 @@ use gpui::*;
use prelude::FluentBuilder as _;
use story::{
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory,
PopoverStory, ProgressStory, ResizableStory, StoryContainer, SwitchStory, TableStory,
TooltipStory,
PopoverStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory,
TableStory, TooltipStory,
};
use workspace::{dock::DockPosition, TitleBar, Workspace};
@ -20,7 +20,6 @@ actions!(workspace, [Open, CloseWindow]);
pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
cx.on_action(|_action: &Open, _cx: &mut AppContext| {});
cx.on_action(|_action: &CloseWindow, _cx| std::process::exit(0));
Theme::init(cx);
ui::init(cx);
@ -156,6 +155,15 @@ impl StoryWorkspace {
)
.detach();
StoryContainer::add_pane(
"Scrollable",
"A scrollable area with scroll bar",
ScrollableStory::view(cx).into(),
workspace.clone(),
cx,
)
.detach();
Self { workspace }
}
@ -239,13 +247,7 @@ impl Render for StoryWorkspace {
})
})
// left side
.child(
div()
.flex()
.items_center()
.on_mouse_move(|_, cx| cx.stop_propagation())
.child("GPUI App"),
)
.child(div().flex().items_center().child("GPUI App"))
.child(
div()
.flex()

View file

@ -8,6 +8,7 @@ mod picker_story;
mod popover_story;
mod progress_story;
mod resizable_story;
mod scrollable_story;
mod switch_story;
mod table_story;
mod tooltip_story;
@ -22,6 +23,7 @@ pub use picker_story::PickerStory;
pub use popover_story::PopoverStory;
pub use progress_story::ProgressStory;
pub use resizable_story::ResizableStory;
pub use scrollable_story::ScrollableStory;
pub use switch_story::SwitchStory;
pub use table_story::TableStory;
pub use tooltip_story::TooltipStory;

View file

@ -1,9 +1,9 @@
use std::sync::Arc;
use gpui::{
deferred, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView,
InteractiveElement as _, IntoElement, ParentElement, Render, Styled, View, ViewContext,
VisualContext as _, WeakView, WindowContext,
div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView, InteractiveElement as _,
IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext as _, WeakView,
WindowContext,
};
use ui::{
@ -201,7 +201,7 @@ impl Render for PickerStory {
)
})
.when(self.open, |this| {
this.child(deferred(
this.child(
div().absolute().size_full().top_0().left_0().child(
v_flex().flex().flex_col().items_center().child(
v_flex()
@ -215,7 +215,7 @@ impl Render for PickerStory {
})),
),
),
))
)
})
}
}

View file

@ -0,0 +1,166 @@
use std::cell::Cell;
use std::rc::Rc;
use gpui::{
canvas, deferred, div, px, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle,
StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext, WindowContext,
};
use ui::button::Button;
use ui::divider::Divider;
use ui::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState};
use ui::theme::ActiveTheme;
use ui::{h_flex, v_flex, Clickable};
pub struct ScrollableStory {
scroll_handle: ScrollHandle,
scroll_size: gpui::Size<Pixels>,
scroll_state: Rc<Cell<ScrollbarState>>,
items: Vec<String>,
test_width: Pixels,
axis: ScrollbarAxis,
}
impl ScrollableStory {
fn new() -> Self {
Self {
scroll_handle: ScrollHandle::new(),
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_size: gpui::Size::default(),
items: (0..500).map(|i| format!("Item {}", i)).collect::<Vec<_>>(),
test_width: px(3000.),
axis: ScrollbarAxis::Both,
}
}
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.new_view(|_| Self::new())
}
pub fn change_test_cases(&mut self, n: usize, cx: &mut ViewContext<Self>) {
if n == 0 {
self.items = (0..500).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
self.test_width = px(3000.);
} else if n == 1 {
self.items = (0..100).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
self.test_width = px(10000.);
} else {
self.items = (0..500).map(|i| format!("Item {}", i)).collect::<Vec<_>>();
self.test_width = px(10000.);
}
self.scroll_state.set(ScrollbarState::default());
cx.notify();
}
pub fn change_axis(&mut self, axis: ScrollbarAxis, cx: &mut ViewContext<Self>) {
self.axis = axis;
cx.notify();
}
}
impl Render for ScrollableStory {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
let view = cx.view().clone();
v_flex()
.gap_4()
.child(
h_flex()
.gap_1()
.child(
Button::new("test-0", cx)
.label("Size 0")
.on_click(cx.listener(|view, _, cx| {
view.change_test_cases(0, cx);
})),
)
.child(
Button::new("test-1", cx)
.label("Size 1")
.on_click(cx.listener(|view, _, cx| {
view.change_test_cases(1, cx);
})),
)
.child(
Button::new("test-2", cx)
.label("Size 2")
.on_click(cx.listener(|view, _, cx| {
view.change_test_cases(2, cx);
})),
)
.child(Divider::vertical())
.child(
Button::new("test-axis-both", cx)
.label("Both Scrollbar")
.on_click(
cx.listener(|view, _, cx| {
view.change_axis(ScrollbarAxis::Both, cx)
}),
),
)
.child(
Button::new("test-axis-vertical", cx)
.label("Vertical")
.on_click(cx.listener(|view, _, cx| {
view.change_axis(ScrollbarAxis::Vertical, cx)
})),
)
.child(
Button::new("test-axis-both", cx)
.label("Horizontal")
.on_click(cx.listener(|view, _, cx| {
view.change_axis(ScrollbarAxis::Horizontal, cx)
})),
),
)
.child(
div()
.w_full()
.border_1()
.border_color(cx.theme().border)
.child(
div()
.relative()
.w_full()
.h(px(400.))
.child(deferred(
Scrollbar::both(
view,
self.scroll_state.clone(),
self.scroll_handle.clone(),
self.scroll_size,
)
.axis(self.axis),
))
.child(
div()
.id("scroll-story")
.overflow_scroll()
.p_4()
.size_full()
.track_scroll(&self.scroll_handle)
.child(
v_flex()
.gap_1()
.w(self.test_width)
.children(self.items.iter().map(|s| {
div().bg(cx.theme().card).child(s.clone())
}))
.child({
let view = cx.view().clone();
canvas(
move |bounds, cx| {
view.update(cx, |r, _| {
r.scroll_size = bounds.size
})
},
|_, _, _| {},
)
.absolute()
.size_full()
}),
),
),
),
)
}
}

View file

@ -5,8 +5,6 @@ mod event;
mod focusable;
mod icon;
mod scroll;
mod scrollbar;
mod selectable;
mod stack;
mod styled_ext;
@ -26,6 +24,7 @@ pub mod prelude;
pub mod progress;
pub mod radio;
pub mod resizable;
pub mod scroll;
pub mod switch;
pub mod tab;
pub mod table;

View file

@ -1,16 +1,17 @@
use std::{cell::Cell, rc::Rc, time::Duration};
use std::{cell::Cell, rc::Rc};
use gpui::prelude::FluentBuilder as _;
use crate::input::{TextEvent, TextInput};
use crate::scroll::ScrollbarState;
use crate::theme::{ActiveTheme, Colorize as _};
use crate::{scrollbar::Scrollbar, v_flex};
use crate::{scroll::Scrollbar, v_flex};
use crate::{Icon, IconName};
use gpui::{
actions, div, px, uniform_list, AppContext, FocusHandle, FocusableView,
actions, deferred, div, px, uniform_list, AppContext, FocusHandle, FocusableView,
InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton,
ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Task,
UniformListScrollHandle, View, ViewContext, VisualContext as _,
ParentElement as _, Render, Styled as _, UniformListScrollHandle, View, ViewContext,
VisualContext as _,
};
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
@ -53,9 +54,7 @@ pub struct List<D: ListDelegate> {
enable_scrollbar: bool,
vertical_scroll_handle: UniformListScrollHandle,
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
show_scrollbar: bool,
hide_scrollbar_task: Option<Task<()>>,
scrollbar_state: Rc<Cell<ScrollbarState>>,
selected_index: Option<usize>,
}
@ -81,9 +80,7 @@ where
query_input: Some(query_input),
selected_index: None,
vertical_scroll_handle: UniformListScrollHandle::new(),
scrollbar_drag_state: Rc::new(Cell::new(None)),
show_scrollbar: false,
hide_scrollbar_task: None,
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
max_height: None,
enable_scrollbar: true,
}
@ -120,55 +117,16 @@ where
if !self.enable_scrollbar {
return None;
}
if !self.show_scrollbar {
return None;
}
Scrollbar::new(
cx.view().clone(),
self.vertical_scroll_handle.clone(),
self.scrollbar_drag_state.clone(),
self.delegate.items_count(),
true,
Some(
deferred(Scrollbar::uniform_scroll(
cx.view().clone(),
self.scrollbar_state.clone(),
self.vertical_scroll_handle.clone(),
self.delegate.items_count(),
))
.with_priority(2),
)
.map(|bar| {
div()
.occlude()
.absolute()
.h_full()
.left_auto()
.top_0()
.right_0()
.w(px(bar.width()))
.bottom_0()
.child(bar)
})
}
fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
self.show_scrollbar = false;
self.hide_scrollbar_task = Some(cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(1)).await;
this.update(&mut cx, |this, cx| {
this.show_scrollbar = false;
cx.notify();
})
.ok();
}))
}
fn on_hover_to_autohide_scrollbar(&mut self, hovered: &bool, cx: &mut ViewContext<Self>) {
if !self.enable_scrollbar {
return;
}
if *hovered {
self.show_scrollbar = true;
self.hide_scrollbar_task.take();
cx.notify();
} else if !self.focus_handle.is_focused(cx) {
self.hide_scrollbar(cx);
}
}
fn scroll_to_selected_item(&mut self, _cx: &mut ViewContext<Self>) {
@ -262,12 +220,12 @@ where
.id("list")
.track_focus(&self.focus_handle)
.size_full()
.relative()
.overflow_hidden()
.on_action(cx.listener(Self::action_cancel))
.on_action(cx.listener(Self::action_confirm))
.on_action(cx.listener(Self::action_select_next))
.on_action(cx.listener(Self::action_select_prev))
.on_hover(cx.listener(Self::on_hover_to_autohide_scrollbar))
.when_some(self.query_input.clone(), |this, input| {
this.child(
div()
@ -280,9 +238,11 @@ where
.child(
v_flex()
.flex_grow()
.relative()
.min_h(px(100.))
.when_some(self.max_height, |this, h| this.max_h(h))
.overflow_hidden()
.children(self.render_scrollbar(cx))
.child(
uniform_list(view, "uniform-list", items_count, {
move |list, visible_range, cx| {
@ -315,8 +275,7 @@ where
.with_sizing_behavior(sizing_behavior)
.track_scroll(vertical_scroll_handle)
.into_any_element(),
)
.children(self.render_scrollbar(cx)),
),
)
}
}

View file

@ -1,3 +1,5 @@
mod scrollable;
mod scrollbar;
pub use scrollable::*;
pub use scrollbar::*;

View file

@ -7,7 +7,7 @@ use gpui::{
/// The scroll axis direction.
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollAxis {
pub enum ScrollableAxis {
/// Horizontal scroll.
Horizontal,
/// Vertical scroll.
@ -21,14 +21,18 @@ pub enum ScrollAxis {
/// This is only can handle once axis scrolling.
pub struct ScrollableMask {
view: AnyView,
axis: ScrollAxis,
axis: ScrollableAxis,
scroll_handle: ScrollHandle,
debug: Option<Hsla>,
}
impl ScrollableMask {
/// Create a new scrollable mask element.
pub fn new(view: impl Into<AnyView>, axis: ScrollAxis, scroll_handle: &ScrollHandle) -> Self {
pub fn new(
view: impl Into<AnyView>,
axis: ScrollableAxis,
scroll_handle: &ScrollHandle,
) -> Self {
Self {
view: view.into(),
scroll_handle: scroll_handle.clone(),
@ -123,7 +127,7 @@ impl Element for ScrollableMask {
let scroll_handle = self.scroll_handle.clone();
let old_offset = scroll_handle.offset();
let view_id = self.view.entity_id();
let is_horizontal = self.axis == ScrollAxis::Horizontal;
let is_horizontal = self.axis == ScrollableAxis::Horizontal;
move |event: &ScrollWheelEvent, _, cx| {
if bounds.contains(&mouse_position) {

View file

@ -0,0 +1,498 @@
use std::{cell::Cell, rc::Rc};
use crate::theme::ActiveTheme;
use gpui::{
fill, point, px, relative, AnyView, Bounds, ContentMask, Element, Hitbox, IntoElement,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Position, ScrollHandle, Style,
UniformListScrollHandle,
};
const MIN_THUMB_SIZE: f32 = 80.;
const THUMB_RADIUS: Pixels = Pixels(5.0);
const THUMB_INSET: Pixels = Pixels(2.5);
pub trait ScrollHandleOffsetable {
fn offset(&self) -> Point<Pixels>;
fn set_offset(&self, offset: Point<Pixels>);
fn is_uniform_list(&self) -> bool {
false
}
}
impl ScrollHandleOffsetable for ScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.offset()
}
fn set_offset(&self, offset: Point<Pixels>) {
self.set_offset(offset);
}
}
impl ScrollHandleOffsetable for UniformListScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.0.borrow().base_handle.offset()
}
fn set_offset(&self, offset: Point<Pixels>) {
self.0.borrow_mut().base_handle.set_offset(offset)
}
fn is_uniform_list(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScrollbarState {
hovered_axis: Option<ScrollbarAxis>,
dragged_axis: Option<ScrollbarAxis>,
drag_pos: Point<Pixels>,
visible: bool,
}
impl Default for ScrollbarState {
fn default() -> Self {
Self {
hovered_axis: None,
dragged_axis: None,
drag_pos: point(px(0.), px(0.)),
visible: true,
}
}
}
impl ScrollbarState {
pub fn new() -> Self {
Self::default()
}
fn with_drag_pos(&self, axis: ScrollbarAxis, pos: Point<Pixels>) -> Self {
let mut state = *self;
if axis.is_vertical() {
state.drag_pos.y = pos.y;
} else {
state.drag_pos.x = pos.x;
}
state.dragged_axis = Some(axis);
state
}
fn with_unset_drag_pos(&self) -> Self {
let mut state = *self;
state.dragged_axis = None;
state
}
fn with_hovered(&self, axis: Option<ScrollbarAxis>) -> Self {
let mut state = *self;
state.hovered_axis = axis;
state
}
fn with_visiable(&self, visiable: bool) -> Self {
let mut state = *self;
state.visible = visiable;
state
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollbarAxis {
Vertical,
Horizontal,
Both,
}
impl ScrollbarAxis {
#[inline]
fn is_vertical(&self) -> bool {
matches!(self, Self::Vertical)
}
#[inline]
fn is_both(&self) -> bool {
matches!(self, Self::Both)
}
#[inline]
pub fn has_vertical(&self) -> bool {
matches!(self, Self::Vertical | Self::Both)
}
#[inline]
pub fn has_horizontal(&self) -> bool {
matches!(self, Self::Horizontal | Self::Both)
}
#[inline]
fn all(&self) -> Vec<ScrollbarAxis> {
match self {
Self::Vertical => vec![Self::Vertical],
Self::Horizontal => vec![Self::Horizontal],
Self::Both => vec![Self::Vertical, Self::Horizontal],
}
}
}
/// Scrollbar control for scroll-area or a uniform-list.
pub struct Scrollbar {
view: AnyView,
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>>,
}
impl Scrollbar {
fn new(
view: AnyView,
state: Rc<Cell<ScrollbarState>>,
axis: ScrollbarAxis,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> Self {
Self {
view,
state,
axis,
scroll_size,
width: px(11.),
scroll_handle: Rc::new(Box::new(scroll_handle)),
}
}
/// Create with vertical and horizontal scrollbar.
pub fn both(
view: impl Into<AnyView>,
state: Rc<Cell<ScrollbarState>>,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> Self {
Self::new(
view.into(),
state,
ScrollbarAxis::Both,
scroll_handle,
scroll_size,
)
}
/// Create with horizontal scrollbar.
pub fn horizontal(
view: impl Into<AnyView>,
state: Rc<Cell<ScrollbarState>>,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> Self {
Self::new(
view.into(),
state,
ScrollbarAxis::Horizontal,
scroll_handle,
scroll_size,
)
}
/// Create with vertical scrollbar.
pub fn vertical(
view: impl Into<AnyView>,
state: Rc<Cell<ScrollbarState>>,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> Self {
Self::new(
view.into(),
state,
ScrollbarAxis::Vertical,
scroll_handle,
scroll_size,
)
}
/// Create vertical scrollbar for uniform list.
pub fn uniform_scroll(
view: impl Into<AnyView>,
state: Rc<Cell<ScrollbarState>>,
scroll_handle: UniformListScrollHandle,
items_count: usize,
) -> Self {
let last_item_height = scroll_handle.0.borrow().last_item_height.unwrap_or(px(10.));
let max_height = items_count as f32 * last_item_height;
let scroll_size = gpui::size(px(0.), max_height);
Self::new(
view.into(),
state,
ScrollbarAxis::Vertical,
scroll_handle,
scroll_size,
)
}
/// Set scrollbar axis.
pub fn axis(mut self, axis: ScrollbarAxis) -> Self {
self.axis = axis;
self
}
}
impl IntoElement for Scrollbar {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for Scrollbar {
type RequestLayoutState = ();
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.position = Position::Absolute;
style.flex_grow = 1.0;
style.flex_shrink = 1.0;
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
(cx.request_layout(style, None), ())
}
fn prepaint(
&mut self,
_: Option<&gpui::GlobalElementId>,
bounds: Bounds<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<Pixels>,
_: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
cx: &mut gpui::WindowContext,
) {
let hitbox_bounds = hitbox.bounds;
let is_both = self.axis.is_both();
cx.with_content_mask(
Some(ContentMask {
bounds: hitbox_bounds,
}),
|cx| {
for axis in self.axis.all().into_iter() {
let is_vertical = axis.is_vertical();
let (scroll_area_size, container_size, scroll_position) = if is_vertical {
(
self.scroll_size.height,
hitbox_bounds.size.height,
self.scroll_handle.offset().y,
)
} else {
(
self.scroll_size.width,
hitbox_bounds.size.width,
self.scroll_handle.offset().x,
)
};
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
let margin_end = if !is_vertical && is_both {
self.width
} else {
px(0.)
};
let thumb_length = (container_size / scroll_area_size * container_size)
.max(px(MIN_THUMB_SIZE));
let thumb_start = -(scroll_position / (scroll_area_size - container_size)
* (container_size - margin_end - thumb_length));
let thumb_end = (thumb_start + thumb_length).min(container_size - margin_end);
let bounds = Bounds {
origin: if is_vertical {
point(
hitbox_bounds.origin.x + hitbox_bounds.size.width - self.width,
hitbox_bounds.origin.y,
)
} else {
point(
hitbox_bounds.origin.x,
hitbox_bounds.origin.y + hitbox_bounds.size.height - self.width,
)
},
size: gpui::Size {
width: if is_vertical {
self.width
} else {
hitbox_bounds.size.width
},
height: if is_vertical {
hitbox_bounds.size.height
} else {
self.width
},
},
};
let thumb_bg = cx.theme().scrollbar_thumb;
let state = self.state.clone();
let (thumb_bg, bar_bg, inset) = if state.get().dragged_axis == Some(axis) {
(thumb_bg, cx.theme().scrollbar, px(0.))
} else if state.get().hovered_axis == Some(axis) {
(thumb_bg, cx.theme().scrollbar, px(0.))
} else {
(thumb_bg, cx.theme().transparent, THUMB_INSET)
};
let thumb_bounds = if is_vertical {
Bounds::from_corners(
point(
bounds.origin.x + inset,
bounds.origin.y + thumb_start + inset,
),
point(
bounds.origin.x + self.width - inset,
bounds.origin.y + thumb_end - (inset * 2),
),
)
} else {
Bounds::from_corners(
point(
bounds.origin.x + thumb_start + inset,
bounds.origin.y + inset,
),
point(
bounds.origin.x + thumb_end - (inset * 2),
bounds.origin.y + self.width - inset,
),
)
};
if state.get().visible {
cx.paint_quad(fill(bounds, bar_bg));
cx.paint_quad(
fill(thumb_bounds, thumb_bg).corner_radii(THUMB_RADIUS - inset),
);
}
cx.on_mouse_event({
let state = self.state.clone();
let view_id = self.view.entity_id();
move |event: &MouseDownEvent, phase, cx| {
if phase.bubble() && thumb_bounds.contains(&event.position) {
let drag_pos = if is_vertical {
point(
state.get().drag_pos.x,
event.position.y - thumb_bounds.origin.y,
)
} else {
point(
event.position.x - thumb_bounds.origin.x,
state.get().drag_pos.y,
)
};
cx.stop_propagation();
state.set(state.get().with_drag_pos(axis, drag_pos));
cx.notify(view_id);
}
}
});
cx.on_mouse_event({
let scroll_handle = self.scroll_handle.clone();
let state = self.state.clone();
let view_id = self.view.entity_id();
move |event: &MouseMoveEvent, _, cx| {
if thumb_bounds.contains(&event.position) {
if state.get().hovered_axis != Some(axis) {
state.set(state.get().with_hovered(Some(axis)));
cx.notify(view_id);
}
} else {
if state.get().hovered_axis == Some(axis) {
if state.get().hovered_axis.is_some() {
state.set(state.get().with_hovered(None));
cx.notify(view_id);
}
}
}
// If mouse out of the bounds, hide scrollbar
if hitbox_bounds.contains(&event.position)
|| state.get().dragged_axis.is_some()
{
if !state.get().visible {
state.set(state.get().with_visiable(true));
cx.notify(view_id);
}
} else {
if state.get().visible {
state.set(state.get().with_visiable(false));
cx.notify(view_id);
}
}
// Move thumb position on dragging
if state.get().dragged_axis == Some(axis) && event.dragging() {
let drag_pos = state.get().drag_pos;
let percentage = if is_vertical {
(event.position.y - bounds.origin.y - drag_pos.y)
/ container_size
} else {
(event.position.x - bounds.origin.x - drag_pos.x)
/ container_size
}
.min(1.);
let offset = if is_vertical {
point(scroll_handle.offset().x, -percentage * scroll_area_size)
} else {
point(-percentage * scroll_area_size, scroll_handle.offset().y)
};
scroll_handle.set_offset(offset);
cx.notify(view_id);
}
}
});
cx.on_mouse_event({
let view_id = self.view.entity_id();
let state = self.state.clone();
move |_event: &MouseUpEvent, phase, cx| {
if phase.bubble() {
state.set(state.get().with_unset_drag_pos());
cx.notify(view_id);
}
}
});
}
},
);
}
}

View file

@ -1,252 +0,0 @@
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.05;
const THUMB_RADIUS: Pixels = Pixels(5.0);
const THUMB_INSET: Pixels = Pixels(0.8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MouseState {
Normal,
Down,
}
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>>>,
mouse_state: Rc<Cell<MouseState>>,
items_count: usize,
thumb: Range<f32>,
}
impl Scrollbar {
pub fn new(
view: impl Into<AnyView>,
handle: UniformListScrollHandle,
drag_state: Rc<Cell<Option<f32>>>,
items_count: usize,
show_scrollbar: bool,
) -> Option<Self> {
let cloned_handle = handle.clone();
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: view.into(),
items_count,
width: 12.0,
thumb,
handle: cloned_handle,
drag_state,
mouse_state: Rc::new(Cell::new(MouseState::Normal)),
})
}
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 is_active = self.mouse_state.get() == MouseState::Down;
let bar_bg = cx.theme().scrollbar;
let thumb_bg = cx.theme().scrollbar_thumb;
let thumb_bg = if is_draging || is_active {
thumb_bg.darken(0.3)
} 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;
cx.on_mouse_event({
let scroll = self.handle.clone();
let drag_state = self.drag_state.clone();
let mouse_state = self.mouse_state.clone();
let view_id = self.view.entity_id();
move |event: &MouseDownEvent, phase, cx| {
if phase.bubble() && bounds.contains(&event.position) {
if thumb_bounds.contains(&event.position) {
let thumb_top_offset =
(event.position.y - thumb_bounds.origin.y) / bounds.size.height;
drag_state.set(Some(thumb_top_offset));
mouse_state.set(MouseState::Down);
cx.notify(view_id);
} else {
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));
}
}
}
}
});
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()));
}
}
});
cx.on_mouse_event({
let drag_state = self.drag_state.clone();
let view_id = self.view.entity_id();
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);
}
}
});
cx.on_mouse_event({
let view_id = self.view.entity_id();
let mouse_state = self.mouse_state.clone();
let drag_state = self.drag_state.clone();
move |_event: &MouseUpEvent, phase, cx| {
if phase.bubble() {
drag_state.set(None);
mouse_state.set(MouseState::Normal);
cx.notify(view_id);
}
}
});
})
}
}

73
crates/ui/src/skeleton.rs Normal file
View file

@ -0,0 +1,73 @@
use std::time::Duration;
use gpui::{
div, Animation, AnimationExt, Div, Element, InteractiveElement, Interactivity, IntoElement,
ParentElement as _, RenderOnce, Stateful, Styled,
};
pub struct Skeleton {
base: Stateful<Div>,
}
impl Skeleton {
pub fn new() -> Self {
Self {
base: div().id("skeleton").w_full().h_4(),
}
}
}
impl Styled for Skeleton {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl IntoElement for Skeleton {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for Skeleton {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn request_layout(
&mut self,
id: Option<&gpui::GlobalElementId>,
cx: &mut gpui::WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let (layout_id, _) = self.base.request_layout(id, cx);
(layout_id, ())
}
fn prepaint(
&mut self,
id: Option<&gpui::GlobalElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
cx: &mut gpui::WindowContext,
) -> Self::PrepaintState {
self.base.prepaint(id, bounds, request_layout, cx);
()
}
fn paint(
&mut self,
id: Option<&gpui::GlobalElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
cx: &mut gpui::WindowContext,
) {
todo!()
}
}

View file

@ -1,9 +1,8 @@
use std::{cell::Cell, rc::Rc, time::Duration};
use std::{cell::Cell, rc::Rc};
use crate::{
h_flex,
scroll::{ScrollAxis, ScrollableMask},
scrollbar::Scrollbar,
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
theme::{ActiveTheme, Colorize},
v_flex,
};
@ -11,7 +10,7 @@ use gpui::{
actions, deferred, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Div,
FocusHandle, FocusableView, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
ParentElement as _, Render, ScrollHandle, SharedString, StatefulInteractiveElement as _,
Styled, Task, UniformListScrollHandle, ViewContext, WindowContext,
Styled, UniformListScrollHandle, ViewContext, WindowContext,
};
actions!(
@ -52,9 +51,8 @@ pub struct Table<D: TableDelegate> {
horizontal_scroll_handle: ScrollHandle,
vertical_scroll_handle: UniformListScrollHandle,
col_groups: Vec<ColGroup>,
show_scrollbar: bool,
hide_scrollbar_task: Option<Task<()>>,
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
scrollbar_state: Rc<Cell<ScrollbarState>>,
selection_state: SelectionState,
selected_row: Option<usize>,
@ -112,9 +110,7 @@ where
col_groups: Vec::new(),
horizontal_scroll_handle: ScrollHandle::new(),
vertical_scroll_handle: UniformListScrollHandle::new(),
show_scrollbar: false,
hide_scrollbar_task: None,
scrollbar_drag_state: Rc::new(Cell::new(None)),
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
selection_state: SelectionState::Row,
selected_row: None,
selected_col: None,
@ -141,43 +137,6 @@ where
cx.notify();
}
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
Scrollbar::new(
cx.view().clone(),
self.vertical_scroll_handle.clone(),
self.scrollbar_drag_state.clone(),
self.delegate.rows_count(),
true,
)
.map(|bar| {
deferred(
div()
.occlude()
.absolute()
.h_full()
.left_auto()
.top_0()
.right_0()
.w(px(bar.width()))
.bottom_0()
.child(bar),
)
.with_priority(1)
})
}
fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
self.show_scrollbar = false;
self.hide_scrollbar_task = Some(cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(1)).await;
this.update(&mut cx, |this, cx| {
this.show_scrollbar = false;
cx.notify();
})
.ok();
}))
}
fn scroll_to_selected_row(&mut self, _cx: &mut ViewContext<Self>) {
if let Some(row_ix) = self.selected_row {
self.vertical_scroll_handle.scroll_to_item(row_ix);
@ -191,16 +150,6 @@ where
}
}
fn on_hover_to_autohide_scrollbar(&mut self, hovered: &bool, cx: &mut ViewContext<Self>) {
if *hovered {
self.show_scrollbar = true;
self.hide_scrollbar_task.take();
cx.notify();
} else if !self.focus_handle.is_focused(cx) {
self.hide_scrollbar(cx);
}
}
fn on_row_click(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Row;
self.selected_row = Some(row_ix);
@ -301,6 +250,18 @@ where
div()
}
}
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let view = cx.view().clone();
let state = self.scrollbar_state.clone();
Some(deferred(Scrollbar::uniform_scroll(
view,
state,
self.vertical_scroll_handle.clone(),
self.delegate.rows_count(),
)))
}
}
impl<D> FocusableView for Table<D>
@ -338,10 +299,8 @@ where
.on_action(cx.listener(Self::action_select_prev))
.on_action(cx.listener(Self::action_select_next_column))
.on_action(cx.listener(Self::action_select_prev_column))
.on_hover(cx.listener(Self::on_hover_to_autohide_scrollbar))
.size_full()
.overflow_hidden()
.children(self.render_scrollbar(cx))
.child(
v_flex()
.flex_grow()
@ -449,10 +408,11 @@ where
.border_1()
.border_color(cx.theme().border)
.bg(cx.theme().card)
.children(self.render_scrollbar(cx))
.child(inner_table)
.child(ScrollableMask::new(
cx.view().clone(),
ScrollAxis::Horizontal,
ScrollableAxis::Horizontal,
&horizontal_scroll_handle,
))
}

View file

@ -192,8 +192,8 @@ impl Colors {
input: hsl(240.0, 5.9, 90.0),
ring: hsl(240.0, 5.9, 65.0),
selection: hsl(211.0, 97.0, 85.0),
scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 5.9, 85.0).opacity(0.7),
scrollbar: hsl(0., 0., 98.),
scrollbar_thumb: hsl(0., 0., 49.),
panel: hsl(0.0, 0.0, 100.0),
tab_bar: hsl(240.0, 4.8, 95.9),
}
@ -251,8 +251,8 @@ impl Colors {
input: hsl(240.0, 3.7, 15.9),
ring: hsl(240.0, 4.9, 83.9),
selection: hsl(211.0, 97.0, 22.0),
scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 3.7, 15.9).opacity(0.7),
scrollbar: hsl(240., 1., 15.),
scrollbar_thumb: hsl(0., 0., 58.),
panel: hsl(299.0, 2., 9.),
tab_bar: hsl(299.0, 2., 9.),
}