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",
|
"xkbcommon",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpui-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"rust-embed",
|
||||||
|
"story",
|
||||||
|
"ui",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -2898,19 +2911,6 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "main-app"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"gpui",
|
|
||||||
"log",
|
|
||||||
"rust-embed",
|
|
||||||
"story",
|
|
||||||
"ui",
|
|
||||||
"workspace",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
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.
|
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
|
- workspace
|
||||||
- scrollbar
|
|
||||||
|
|
||||||
Other UI components are under Apache License.
|
Other UI components are under Apache License.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "main-app"
|
name = "gpui-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use gpui::*;
|
||||||
use prelude::FluentBuilder as _;
|
use prelude::FluentBuilder as _;
|
||||||
use story::{
|
use story::{
|
||||||
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory,
|
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory,
|
||||||
PopoverStory, ProgressStory, ResizableStory, StoryContainer, SwitchStory, TableStory,
|
PopoverStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory,
|
||||||
TooltipStory,
|
TableStory, TooltipStory,
|
||||||
};
|
};
|
||||||
use workspace::{dock::DockPosition, TitleBar, Workspace};
|
use workspace::{dock::DockPosition, TitleBar, Workspace};
|
||||||
|
|
||||||
|
|
@ -20,7 +20,6 @@ actions!(workspace, [Open, CloseWindow]);
|
||||||
|
|
||||||
pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
cx.on_action(|_action: &Open, _cx: &mut AppContext| {});
|
cx.on_action(|_action: &Open, _cx: &mut AppContext| {});
|
||||||
cx.on_action(|_action: &CloseWindow, _cx| std::process::exit(0));
|
|
||||||
|
|
||||||
Theme::init(cx);
|
Theme::init(cx);
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
|
@ -156,6 +155,15 @@ impl StoryWorkspace {
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
StoryContainer::add_pane(
|
||||||
|
"Scrollable",
|
||||||
|
"A scrollable area with scroll bar",
|
||||||
|
ScrollableStory::view(cx).into(),
|
||||||
|
workspace.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
|
||||||
Self { workspace }
|
Self { workspace }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,13 +247,7 @@ impl Render for StoryWorkspace {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// left side
|
// left side
|
||||||
.child(
|
.child(div().flex().items_center().child("GPUI App"))
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
|
||||||
.child("GPUI App"),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ mod picker_story;
|
||||||
mod popover_story;
|
mod popover_story;
|
||||||
mod progress_story;
|
mod progress_story;
|
||||||
mod resizable_story;
|
mod resizable_story;
|
||||||
|
mod scrollable_story;
|
||||||
mod switch_story;
|
mod switch_story;
|
||||||
mod table_story;
|
mod table_story;
|
||||||
mod tooltip_story;
|
mod tooltip_story;
|
||||||
|
|
@ -22,6 +23,7 @@ pub use picker_story::PickerStory;
|
||||||
pub use popover_story::PopoverStory;
|
pub use popover_story::PopoverStory;
|
||||||
pub use progress_story::ProgressStory;
|
pub use progress_story::ProgressStory;
|
||||||
pub use resizable_story::ResizableStory;
|
pub use resizable_story::ResizableStory;
|
||||||
|
pub use scrollable_story::ScrollableStory;
|
||||||
pub use switch_story::SwitchStory;
|
pub use switch_story::SwitchStory;
|
||||||
pub use table_story::TableStory;
|
pub use table_story::TableStory;
|
||||||
pub use tooltip_story::TooltipStory;
|
pub use tooltip_story::TooltipStory;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView,
|
div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView, InteractiveElement as _,
|
||||||
InteractiveElement as _, IntoElement, ParentElement, Render, Styled, View, ViewContext,
|
IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext as _, WeakView,
|
||||||
VisualContext as _, WeakView, WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ui::{
|
use ui::{
|
||||||
|
|
@ -201,7 +201,7 @@ impl Render for PickerStory {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(self.open, |this| {
|
.when(self.open, |this| {
|
||||||
this.child(deferred(
|
this.child(
|
||||||
div().absolute().size_full().top_0().left_0().child(
|
div().absolute().size_full().top_0().left_0().child(
|
||||||
v_flex().flex().flex_col().items_center().child(
|
v_flex().flex().flex_col().items_center().child(
|
||||||
v_flex()
|
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 focusable;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
|
||||||
mod scroll;
|
|
||||||
mod scrollbar;
|
|
||||||
mod selectable;
|
mod selectable;
|
||||||
mod stack;
|
mod stack;
|
||||||
mod styled_ext;
|
mod styled_ext;
|
||||||
|
|
@ -26,6 +24,7 @@ pub mod prelude;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub mod radio;
|
pub mod radio;
|
||||||
pub mod resizable;
|
pub mod resizable;
|
||||||
|
pub mod scroll;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
pub mod table;
|
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 gpui::prelude::FluentBuilder as _;
|
||||||
|
|
||||||
use crate::input::{TextEvent, TextInput};
|
use crate::input::{TextEvent, TextInput};
|
||||||
|
use crate::scroll::ScrollbarState;
|
||||||
use crate::theme::{ActiveTheme, Colorize as _};
|
use crate::theme::{ActiveTheme, Colorize as _};
|
||||||
use crate::{scrollbar::Scrollbar, v_flex};
|
use crate::{scroll::Scrollbar, v_flex};
|
||||||
use crate::{Icon, IconName};
|
use crate::{Icon, IconName};
|
||||||
use gpui::{
|
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,
|
InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton,
|
||||||
ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Task,
|
ParentElement as _, Render, Styled as _, UniformListScrollHandle, View, ViewContext,
|
||||||
UniformListScrollHandle, View, ViewContext, VisualContext as _,
|
VisualContext as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
|
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
|
||||||
|
|
@ -53,9 +54,7 @@ pub struct List<D: ListDelegate> {
|
||||||
|
|
||||||
enable_scrollbar: bool,
|
enable_scrollbar: bool,
|
||||||
vertical_scroll_handle: UniformListScrollHandle,
|
vertical_scroll_handle: UniformListScrollHandle,
|
||||||
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
scrollbar_state: Rc<Cell<ScrollbarState>>,
|
||||||
show_scrollbar: bool,
|
|
||||||
hide_scrollbar_task: Option<Task<()>>,
|
|
||||||
|
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
@ -81,9 +80,7 @@ where
|
||||||
query_input: Some(query_input),
|
query_input: Some(query_input),
|
||||||
selected_index: None,
|
selected_index: None,
|
||||||
vertical_scroll_handle: UniformListScrollHandle::new(),
|
vertical_scroll_handle: UniformListScrollHandle::new(),
|
||||||
scrollbar_drag_state: Rc::new(Cell::new(None)),
|
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
|
||||||
show_scrollbar: false,
|
|
||||||
hide_scrollbar_task: None,
|
|
||||||
max_height: None,
|
max_height: None,
|
||||||
enable_scrollbar: true,
|
enable_scrollbar: true,
|
||||||
}
|
}
|
||||||
|
|
@ -120,55 +117,16 @@ where
|
||||||
if !self.enable_scrollbar {
|
if !self.enable_scrollbar {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !self.show_scrollbar {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Scrollbar::new(
|
Some(
|
||||||
cx.view().clone(),
|
deferred(Scrollbar::uniform_scroll(
|
||||||
self.vertical_scroll_handle.clone(),
|
cx.view().clone(),
|
||||||
self.scrollbar_drag_state.clone(),
|
self.scrollbar_state.clone(),
|
||||||
self.delegate.items_count(),
|
self.vertical_scroll_handle.clone(),
|
||||||
true,
|
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>) {
|
fn scroll_to_selected_item(&mut self, _cx: &mut ViewContext<Self>) {
|
||||||
|
|
@ -262,12 +220,12 @@ where
|
||||||
.id("list")
|
.id("list")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.relative()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.on_action(cx.listener(Self::action_cancel))
|
.on_action(cx.listener(Self::action_cancel))
|
||||||
.on_action(cx.listener(Self::action_confirm))
|
.on_action(cx.listener(Self::action_confirm))
|
||||||
.on_action(cx.listener(Self::action_select_next))
|
.on_action(cx.listener(Self::action_select_next))
|
||||||
.on_action(cx.listener(Self::action_select_prev))
|
.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| {
|
.when_some(self.query_input.clone(), |this, input| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
|
|
@ -280,9 +238,11 @@ where
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
|
.relative()
|
||||||
.min_h(px(100.))
|
.min_h(px(100.))
|
||||||
.when_some(self.max_height, |this, h| this.max_h(h))
|
.when_some(self.max_height, |this, h| this.max_h(h))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
.children(self.render_scrollbar(cx))
|
||||||
.child(
|
.child(
|
||||||
uniform_list(view, "uniform-list", items_count, {
|
uniform_list(view, "uniform-list", items_count, {
|
||||||
move |list, visible_range, cx| {
|
move |list, visible_range, cx| {
|
||||||
|
|
@ -315,8 +275,7 @@ where
|
||||||
.with_sizing_behavior(sizing_behavior)
|
.with_sizing_behavior(sizing_behavior)
|
||||||
.track_scroll(vertical_scroll_handle)
|
.track_scroll(vertical_scroll_handle)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
)
|
),
|
||||||
.children(self.render_scrollbar(cx)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
mod scrollable;
|
mod scrollable;
|
||||||
|
mod scrollbar;
|
||||||
|
|
||||||
pub use scrollable::*;
|
pub use scrollable::*;
|
||||||
|
pub use scrollbar::*;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use gpui::{
|
||||||
/// The scroll axis direction.
|
/// The scroll axis direction.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ScrollAxis {
|
pub enum ScrollableAxis {
|
||||||
/// Horizontal scroll.
|
/// Horizontal scroll.
|
||||||
Horizontal,
|
Horizontal,
|
||||||
/// Vertical scroll.
|
/// Vertical scroll.
|
||||||
|
|
@ -21,14 +21,18 @@ pub enum ScrollAxis {
|
||||||
/// This is only can handle once axis scrolling.
|
/// This is only can handle once axis scrolling.
|
||||||
pub struct ScrollableMask {
|
pub struct ScrollableMask {
|
||||||
view: AnyView,
|
view: AnyView,
|
||||||
axis: ScrollAxis,
|
axis: ScrollableAxis,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
debug: Option<Hsla>,
|
debug: Option<Hsla>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollableMask {
|
impl ScrollableMask {
|
||||||
/// Create a new scrollable mask element.
|
/// 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 {
|
Self {
|
||||||
view: view.into(),
|
view: view.into(),
|
||||||
scroll_handle: scroll_handle.clone(),
|
scroll_handle: scroll_handle.clone(),
|
||||||
|
|
@ -123,7 +127,7 @@ impl Element for ScrollableMask {
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
let old_offset = scroll_handle.offset();
|
let old_offset = scroll_handle.offset();
|
||||||
let view_id = self.view.entity_id();
|
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| {
|
move |event: &ScrollWheelEvent, _, cx| {
|
||||||
if bounds.contains(&mouse_position) {
|
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::{
|
use crate::{
|
||||||
h_flex,
|
h_flex,
|
||||||
scroll::{ScrollAxis, ScrollableMask},
|
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
|
||||||
scrollbar::Scrollbar,
|
|
||||||
theme::{ActiveTheme, Colorize},
|
theme::{ActiveTheme, Colorize},
|
||||||
v_flex,
|
v_flex,
|
||||||
};
|
};
|
||||||
|
|
@ -11,7 +10,7 @@ use gpui::{
|
||||||
actions, deferred, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Div,
|
actions, deferred, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Div,
|
||||||
FocusHandle, FocusableView, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
|
FocusHandle, FocusableView, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
|
||||||
ParentElement as _, Render, ScrollHandle, SharedString, StatefulInteractiveElement as _,
|
ParentElement as _, Render, ScrollHandle, SharedString, StatefulInteractiveElement as _,
|
||||||
Styled, Task, UniformListScrollHandle, ViewContext, WindowContext,
|
Styled, UniformListScrollHandle, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
|
|
@ -52,9 +51,8 @@ pub struct Table<D: TableDelegate> {
|
||||||
horizontal_scroll_handle: ScrollHandle,
|
horizontal_scroll_handle: ScrollHandle,
|
||||||
vertical_scroll_handle: UniformListScrollHandle,
|
vertical_scroll_handle: UniformListScrollHandle,
|
||||||
col_groups: Vec<ColGroup>,
|
col_groups: Vec<ColGroup>,
|
||||||
show_scrollbar: bool,
|
|
||||||
hide_scrollbar_task: Option<Task<()>>,
|
scrollbar_state: Rc<Cell<ScrollbarState>>,
|
||||||
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
|
||||||
|
|
||||||
selection_state: SelectionState,
|
selection_state: SelectionState,
|
||||||
selected_row: Option<usize>,
|
selected_row: Option<usize>,
|
||||||
|
|
@ -112,9 +110,7 @@ where
|
||||||
col_groups: Vec::new(),
|
col_groups: Vec::new(),
|
||||||
horizontal_scroll_handle: ScrollHandle::new(),
|
horizontal_scroll_handle: ScrollHandle::new(),
|
||||||
vertical_scroll_handle: UniformListScrollHandle::new(),
|
vertical_scroll_handle: UniformListScrollHandle::new(),
|
||||||
show_scrollbar: false,
|
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
|
||||||
hide_scrollbar_task: None,
|
|
||||||
scrollbar_drag_state: Rc::new(Cell::new(None)),
|
|
||||||
selection_state: SelectionState::Row,
|
selection_state: SelectionState::Row,
|
||||||
selected_row: None,
|
selected_row: None,
|
||||||
selected_col: None,
|
selected_col: None,
|
||||||
|
|
@ -141,43 +137,6 @@ where
|
||||||
cx.notify();
|
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>) {
|
fn scroll_to_selected_row(&mut self, _cx: &mut ViewContext<Self>) {
|
||||||
if let Some(row_ix) = self.selected_row {
|
if let Some(row_ix) = self.selected_row {
|
||||||
self.vertical_scroll_handle.scroll_to_item(row_ix);
|
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>) {
|
fn on_row_click(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) {
|
||||||
self.selection_state = SelectionState::Row;
|
self.selection_state = SelectionState::Row;
|
||||||
self.selected_row = Some(row_ix);
|
self.selected_row = Some(row_ix);
|
||||||
|
|
@ -301,6 +250,18 @@ where
|
||||||
div()
|
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>
|
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_prev))
|
||||||
.on_action(cx.listener(Self::action_select_next_column))
|
.on_action(cx.listener(Self::action_select_next_column))
|
||||||
.on_action(cx.listener(Self::action_select_prev_column))
|
.on_action(cx.listener(Self::action_select_prev_column))
|
||||||
.on_hover(cx.listener(Self::on_hover_to_autohide_scrollbar))
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.children(self.render_scrollbar(cx))
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
|
|
@ -449,10 +408,11 @@ where
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(cx.theme().card)
|
.bg(cx.theme().card)
|
||||||
|
.children(self.render_scrollbar(cx))
|
||||||
.child(inner_table)
|
.child(inner_table)
|
||||||
.child(ScrollableMask::new(
|
.child(ScrollableMask::new(
|
||||||
cx.view().clone(),
|
cx.view().clone(),
|
||||||
ScrollAxis::Horizontal,
|
ScrollableAxis::Horizontal,
|
||||||
&horizontal_scroll_handle,
|
&horizontal_scroll_handle,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,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, 65.0),
|
ring: hsl(240.0, 5.9, 65.0),
|
||||||
selection: hsl(211.0, 97.0, 85.0),
|
selection: hsl(211.0, 97.0, 85.0),
|
||||||
scrollbar: Hsla::transparent_black(),
|
scrollbar: hsl(0., 0., 98.),
|
||||||
scrollbar_thumb: hsl(240.0, 5.9, 85.0).opacity(0.7),
|
scrollbar_thumb: hsl(0., 0., 49.),
|
||||||
panel: hsl(0.0, 0.0, 100.0),
|
panel: hsl(0.0, 0.0, 100.0),
|
||||||
tab_bar: hsl(240.0, 4.8, 95.9),
|
tab_bar: hsl(240.0, 4.8, 95.9),
|
||||||
}
|
}
|
||||||
|
|
@ -251,8 +251,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, 22.0),
|
selection: hsl(211.0, 97.0, 22.0),
|
||||||
scrollbar: Hsla::transparent_black(),
|
scrollbar: hsl(240., 1., 15.),
|
||||||
scrollbar_thumb: hsl(240.0, 3.7, 15.9).opacity(0.7),
|
scrollbar_thumb: hsl(0., 0., 58.),
|
||||||
panel: hsl(299.0, 2., 9.),
|
panel: hsl(299.0, 2., 9.),
|
||||||
tab_bar: hsl(299.0, 2., 9.),
|
tab_bar: hsl(299.0, 2., 9.),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue