New Scrollbar (#33)
https://github.com/user-attachments/assets/30d890bc-49cc-4abe-8652-387ddbb73198
This commit is contained in:
parent
b1bb22fd1d
commit
2b184ca8d5
17 changed files with 828 additions and 413 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.check.command": "clippy"
|
||||
}
|
||||
26
Cargo.lock
generated
26
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "main-app"
|
||||
name = "gpui-app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
})),
|
||||
),
|
||||
),
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
166
crates/story/src/scrollable_story.rs
Normal file
166
crates/story/src/scrollable_story.rs
Normal 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()
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
mod scrollable;
|
||||
mod scrollbar;
|
||||
|
||||
pub use scrollable::*;
|
||||
pub use scrollbar::*;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
498
crates/ui/src/scroll/scrollbar.rs
Normal file
498
crates/ui/src/scroll/scrollbar.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
73
crates/ui/src/skeleton.rs
Normal 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!()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue