wip: refactor
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Corners, Div, Edges,
|
||||
ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
h_flex,
|
||||
indicator::Indicator,
|
||||
@@ -5,11 +11,6 @@ use crate::{
|
||||
tooltip::Tooltip,
|
||||
Disableable, Icon, Selectable, Sizable, Size,
|
||||
};
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Corners, Div, Edges,
|
||||
ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext,
|
||||
};
|
||||
|
||||
pub enum ButtonRounded {
|
||||
None,
|
||||
@@ -574,7 +575,8 @@ impl ButtonVariant {
|
||||
fn active(&self, cx: &WindowContext) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().primary_active,
|
||||
ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => {
|
||||
ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_active,
|
||||
ButtonVariant::Ghost => {
|
||||
if cx.theme().mode.is_dark() {
|
||||
cx.theme().secondary.lighten(0.2).opacity(0.8)
|
||||
} else {
|
||||
@@ -607,7 +609,8 @@ impl ButtonVariant {
|
||||
fn selected(&self, cx: &WindowContext) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().primary_active,
|
||||
ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => {
|
||||
ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_active,
|
||||
ButtonVariant::Ghost => {
|
||||
if cx.theme().mode.is_dark() {
|
||||
cx.theme().secondary.lighten(0.2).opacity(0.8)
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use gpui::{
|
||||
anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, AnchorCorner,
|
||||
AppContext, Bounds, ElementId, EventEmitter, FocusHandle, FocusableView, Hsla,
|
||||
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||
Render, SharedString, StatefulInteractiveElement as _, Styled, View, ViewContext,
|
||||
VisualContext,
|
||||
anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, AppContext, Bounds,
|
||||
Corner, ElementId, EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement as _,
|
||||
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, Render, SharedString,
|
||||
StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -63,7 +62,7 @@ pub struct ColorPicker {
|
||||
hovered_color: Option<Hsla>,
|
||||
label: Option<SharedString>,
|
||||
size: Size,
|
||||
anchor: AnchorCorner,
|
||||
anchor: Corner,
|
||||
color_input: View<TextInput>,
|
||||
|
||||
open: bool,
|
||||
@@ -112,7 +111,7 @@ impl ColorPicker {
|
||||
hovered_color: None,
|
||||
size: Size::Medium,
|
||||
label: None,
|
||||
anchor: AnchorCorner::TopLeft,
|
||||
anchor: Corner::TopLeft,
|
||||
color_input,
|
||||
open: false,
|
||||
bounds: Bounds::default(),
|
||||
@@ -149,8 +148,8 @@ impl ColorPicker {
|
||||
|
||||
/// Set the anchor corner of the color picker.
|
||||
///
|
||||
/// Default is `AnchorCorner::TopLeft`.
|
||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||
/// Default is `Corner::TopLeft`.
|
||||
pub fn anchor(mut self, anchor: Corner) -> Self {
|
||||
self.anchor = anchor;
|
||||
self
|
||||
}
|
||||
@@ -262,13 +261,12 @@ impl ColorPicker {
|
||||
}
|
||||
|
||||
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||
match self.anchor {
|
||||
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||
}
|
||||
.corner(bounds)
|
||||
bounds.corner(match self.anchor {
|
||||
Corner::TopLeft => Corner::BottomLeft,
|
||||
Corner::TopRight => Corner::BottomRight,
|
||||
Corner::BottomLeft => Corner::TopLeft,
|
||||
Corner::BottomRight => Corner::TopRight,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,12 +345,8 @@ impl Render for ColorPicker {
|
||||
div()
|
||||
.occlude()
|
||||
.map(|this| match self.anchor {
|
||||
AnchorCorner::TopLeft | AnchorCorner::TopRight => {
|
||||
this.mt_1p5()
|
||||
}
|
||||
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => {
|
||||
this.mb_1p5()
|
||||
}
|
||||
Corner::TopLeft | Corner::TopRight => this.mt_1p5(),
|
||||
Corner::BottomLeft | Corner::BottomRight => this.mb_1p5(),
|
||||
})
|
||||
.w_72()
|
||||
.overflow_hidden()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
anchored, deferred, div, prelude::FluentBuilder, px, relative, AnchorCorner, AnyElement,
|
||||
anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, Corner,
|
||||
DismissEvent, DispatchPhase, Element, ElementId, Focusable, GlobalElementId,
|
||||
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
Position, Stateful, Style, View, ViewContext, WindowContext,
|
||||
@@ -27,7 +27,7 @@ type Menu<M> = Option<Box<dyn Fn(PopupMenu, &mut ViewContext<M>) -> PopupMenu +
|
||||
pub struct ContextMenu {
|
||||
id: ElementId,
|
||||
menu: Menu<PopupMenu>,
|
||||
anchor: AnchorCorner,
|
||||
anchor: Corner,
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
@@ -35,7 +35,7 @@ impl ContextMenu {
|
||||
Self {
|
||||
id: id.into(),
|
||||
menu: None,
|
||||
anchor: AnchorCorner::TopLeft,
|
||||
anchor: Corner::TopLeft,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, px, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent,
|
||||
div, prelude::FluentBuilder, px, rems, AppContext, Corner, DefiniteLength, DismissEvent,
|
||||
DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _,
|
||||
@@ -367,7 +367,7 @@ impl TabPanel {
|
||||
this.separator().menu("Close", Box::new(ClosePanel))
|
||||
})
|
||||
})
|
||||
.anchor(AnchorCorner::TopRight),
|
||||
.anchor(Corner::TopRight),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ pub mod switch;
|
||||
pub mod tab;
|
||||
pub mod theme;
|
||||
pub mod tooltip;
|
||||
pub mod virtual_list;
|
||||
|
||||
pub use crate::Disableable;
|
||||
pub use event::InteractiveElementExt;
|
||||
@@ -51,6 +52,7 @@ pub use focusable::FocusableCycle;
|
||||
pub use root::{ContextModal, Root};
|
||||
pub use styled::*;
|
||||
pub use title_bar::*;
|
||||
pub use virtual_list::{h_virtual_list, v_virtual_list, VirtualList};
|
||||
|
||||
pub use colors::*;
|
||||
pub use icon::*;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnchorCorner, AnyElement,
|
||||
AppContext, Bounds, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle,
|
||||
actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, AppContext,
|
||||
Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle,
|
||||
FocusableView, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
|
||||
Style, StyleRefinement, Styled, View, ViewContext, VisualContext, WindowContext,
|
||||
@@ -69,7 +69,7 @@ type ViewContent<M> = Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>
|
||||
|
||||
pub struct Popover<M: ManagedView> {
|
||||
id: ElementId,
|
||||
anchor: AnchorCorner,
|
||||
anchor: Corner,
|
||||
trigger: Trigger,
|
||||
content: ViewContent<M>,
|
||||
/// Style for trigger element.
|
||||
@@ -87,7 +87,7 @@ where
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
anchor: AnchorCorner::TopLeft,
|
||||
anchor: Corner::TopLeft,
|
||||
trigger: None,
|
||||
trigger_style: None,
|
||||
content: None,
|
||||
@@ -96,7 +96,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||
pub fn anchor(mut self, anchor: Corner) -> Self {
|
||||
self.anchor = anchor;
|
||||
self
|
||||
}
|
||||
@@ -153,13 +153,12 @@ where
|
||||
}
|
||||
|
||||
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||
match self.anchor {
|
||||
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||
}
|
||||
.corner(bounds)
|
||||
bounds.corner(match self.anchor {
|
||||
Corner::TopLeft => Corner::BottomLeft,
|
||||
Corner::TopRight => Corner::BottomRight,
|
||||
Corner::BottomLeft => Corner::TopLeft,
|
||||
Corner::BottomRight => Corner::TopRight,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_element_state<R>(
|
||||
@@ -273,12 +272,8 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||
.occlude()
|
||||
.when(!no_style, |this| this.popover_style(cx))
|
||||
.map(|this| match anchor {
|
||||
AnchorCorner::TopLeft | AnchorCorner::TopRight => {
|
||||
this.top_1p5()
|
||||
}
|
||||
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => {
|
||||
this.bottom_1p5()
|
||||
}
|
||||
Corner::TopLeft | Corner::TopRight => this.top_1p5(),
|
||||
Corner::BottomLeft | Corner::BottomRight => this.bottom_1p5(),
|
||||
})
|
||||
.child(content_view.clone())
|
||||
.when(!no_style, |this| {
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{
|
||||
SharedString, View, ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
use gpui::{
|
||||
anchored, canvas, rems, AnchorCorner, AnyElement, Bounds, Edges, FocusableView, Keystroke,
|
||||
anchored, canvas, rems, AnyElement, Bounds, Corner, Edges, FocusableView, Keystroke,
|
||||
ScrollHandle, StatefulInteractiveElement, Styled, WeakView,
|
||||
};
|
||||
|
||||
@@ -37,13 +37,13 @@ pub trait PopupMenuExt: Styled + Selectable + IntoElement + 'static {
|
||||
self,
|
||||
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Popover<PopupMenu> {
|
||||
self.popup_menu_with_anchor(AnchorCorner::TopLeft, f)
|
||||
self.popup_menu_with_anchor(Corner::TopLeft, f)
|
||||
}
|
||||
|
||||
/// Create a popup menu with the given items, anchored to the given corner
|
||||
fn popup_menu_with_anchor(
|
||||
mut self,
|
||||
anchor: impl Into<AnchorCorner>,
|
||||
anchor: impl Into<Corner>,
|
||||
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Popover<PopupMenu> {
|
||||
let style = self.style().clone();
|
||||
@@ -646,13 +646,10 @@ impl Render for PopupMenu {
|
||||
- bounds.origin.x
|
||||
< max_width
|
||||
{
|
||||
(
|
||||
AnchorCorner::TopRight,
|
||||
-px(15.),
|
||||
)
|
||||
(Corner::TopRight, -px(15.))
|
||||
} else {
|
||||
(
|
||||
AnchorCorner::TopLeft,
|
||||
Corner::TopLeft,
|
||||
bounds.size.width
|
||||
- px(10.),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
||||
use gpui::{
|
||||
canvas, div, relative, AnyElement, Div, Element, ElementId, EntityId, GlobalElementId,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, Position, ScrollHandle, SharedString,
|
||||
Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
||||
|
||||
/// A scroll view is a container that allows the user to scroll through a large amount of content.
|
||||
pub struct Scrollable<E> {
|
||||
@@ -121,6 +121,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> StatefulInteractiveElement for Scrollable<E> where E: Element + StatefulInteractiveElement {}
|
||||
|
||||
impl<E> IntoElement for Scrollable<E>
|
||||
@@ -202,8 +203,8 @@ where
|
||||
),
|
||||
)
|
||||
.into_any_element();
|
||||
let element_id = element.request_layout(cx);
|
||||
|
||||
let element_id = element.request_layout(cx);
|
||||
let layout_id = cx.request_layout(style, vec![element_id]);
|
||||
|
||||
(layout_id, element)
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use gpui::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cell::Cell, rc::Rc, time::Instant};
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
use gpui::{
|
||||
fill, point, px, relative, AppContext, Bounds, ContentMask, CursorStyle, Edges, Element,
|
||||
EntityId, Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Style, UniformListScrollHandle,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Scrollbar show mode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
|
||||
@@ -22,6 +18,7 @@ impl ScrollbarShow {
|
||||
}
|
||||
}
|
||||
|
||||
const BORDER_WIDTH: Pixels = px(0.);
|
||||
const MIN_THUMB_SIZE: f32 = 80.;
|
||||
const THUMB_RADIUS: Pixels = Pixels(3.0);
|
||||
const THUMB_INSET: Pixels = Pixels(4.);
|
||||
@@ -357,11 +354,12 @@ pub struct AxisPrepaintState {
|
||||
axis: ScrollbarAxis,
|
||||
bar_hitbox: Hitbox,
|
||||
bounds: Bounds<Pixels>,
|
||||
border_width: Pixels,
|
||||
radius: Pixels,
|
||||
bg: Hsla,
|
||||
border: Hsla,
|
||||
thumb_bounds: Bounds<Pixels>,
|
||||
// Bounds of thumb to be rendered.
|
||||
thumb_fill_bounds: Bounds<Pixels>,
|
||||
thumb_bg: Hsla,
|
||||
scroll_size: Pixels,
|
||||
container_size: Pixels,
|
||||
@@ -387,7 +385,7 @@ impl Element for Scrollbar {
|
||||
position: Position::Absolute,
|
||||
flex_grow: 1.0,
|
||||
flex_shrink: 1.0,
|
||||
size: gpui::Size {
|
||||
size: Size {
|
||||
width: relative(1.).into(),
|
||||
height: relative(1.).into(),
|
||||
},
|
||||
@@ -517,11 +515,21 @@ impl Element for Scrollbar {
|
||||
idle_state
|
||||
};
|
||||
|
||||
let border_width = px(0.);
|
||||
let thumb_bounds = if is_vertical {
|
||||
Bounds::from_corners(
|
||||
point(bounds.origin.x, bounds.origin.y + thumb_start),
|
||||
point(bounds.origin.x + self.width, bounds.origin.y + thumb_end),
|
||||
)
|
||||
} else {
|
||||
Bounds::from_corners(
|
||||
point(bounds.origin.x + thumb_start, bounds.origin.y),
|
||||
point(bounds.origin.x + thumb_end, bounds.origin.y + self.width),
|
||||
)
|
||||
};
|
||||
let thumb_fill_bounds = if is_vertical {
|
||||
Bounds::from_corners(
|
||||
point(
|
||||
bounds.origin.x + inset + border_width,
|
||||
bounds.origin.x + inset + BORDER_WIDTH,
|
||||
bounds.origin.y + thumb_start + inset,
|
||||
),
|
||||
point(
|
||||
@@ -533,7 +541,7 @@ impl Element for Scrollbar {
|
||||
Bounds::from_corners(
|
||||
point(
|
||||
bounds.origin.x + thumb_start + inset,
|
||||
bounds.origin.y + inset + border_width,
|
||||
bounds.origin.y + inset + BORDER_WIDTH,
|
||||
),
|
||||
point(
|
||||
bounds.origin.x + thumb_end - inset,
|
||||
@@ -550,11 +558,11 @@ impl Element for Scrollbar {
|
||||
axis,
|
||||
bar_hitbox,
|
||||
bounds,
|
||||
border_width,
|
||||
radius,
|
||||
bg: bar_bg,
|
||||
border: bar_border,
|
||||
thumb_bounds,
|
||||
thumb_fill_bounds,
|
||||
thumb_bg,
|
||||
scroll_size: scroll_area_size,
|
||||
container_size,
|
||||
@@ -603,11 +611,11 @@ impl Element for Scrollbar {
|
||||
top: px(0.),
|
||||
right: px(0.),
|
||||
bottom: px(0.),
|
||||
left: state.border_width,
|
||||
left: BORDER_WIDTH,
|
||||
}
|
||||
} else {
|
||||
Edges {
|
||||
top: state.border_width,
|
||||
top: BORDER_WIDTH,
|
||||
right: px(0.),
|
||||
bottom: px(0.),
|
||||
left: px(0.),
|
||||
@@ -616,7 +624,7 @@ impl Element for Scrollbar {
|
||||
border_color: state.border,
|
||||
});
|
||||
|
||||
cx.paint_quad(fill(thumb_bounds, state.thumb_bg).corner_radii(radius));
|
||||
cx.paint_quad(fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius));
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
|
||||
@@ -263,7 +263,7 @@ impl ThemeColor {
|
||||
scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9),
|
||||
scrollbar_thumb_hover: hsl(0., 0., 59.),
|
||||
secondary: hsl(240.0, 5.9, 96.9),
|
||||
secondary_active: hsl(240.0, 5.9, 93.),
|
||||
secondary_active: hsl(240.0, 5.9, 90.),
|
||||
secondary_foreground: hsl(240.0, 59.0, 10.),
|
||||
secondary_hover: hsl(240.0, 5.9, 98.),
|
||||
selection: hsl(211.0, 97.0, 85.0),
|
||||
|
||||
447
crates/ui/src/virtual_list.rs
Normal file
447
crates/ui/src/virtual_list.rs
Normal file
@@ -0,0 +1,447 @@
|
||||
//! Vistual List for render a large number of differently sized rows/columns.
|
||||
//!
|
||||
//! > NOTE: This must ensure each column width or row height.
|
||||
//!
|
||||
//! Only visible range are rendered for performance reasons.
|
||||
//!
|
||||
//! Inspired by `gpui::uniform_list`.
|
||||
//! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs
|
||||
//!
|
||||
//! Unlike the `uniform_list`, the each item can have different size.
|
||||
//!
|
||||
//! This is useful for more complex layout, for example, a table with different row height.
|
||||
use std::{cmp, ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
div, point, px, size, AnyElement, AvailableSpace, Axis, Bounds, ContentMask, Div, Element,
|
||||
ElementId, GlobalElementId, Hitbox, InteractiveElement, IntoElement, IsZero as _, Pixels,
|
||||
Render, ScrollHandle, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||
View, ViewContext, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Create a virtual list in Vertical direction.
|
||||
///
|
||||
/// This is like `uniform_list` in GPUI, but support two axis.
|
||||
///
|
||||
/// The `item_sizes` is the size of each column.
|
||||
pub fn v_virtual_list<R, V>(
|
||||
view: View<V>,
|
||||
id: impl Into<ElementId>,
|
||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, Size<Pixels>, &mut ViewContext<V>) -> Vec<R>,
|
||||
) -> VirtualList
|
||||
where
|
||||
R: IntoElement,
|
||||
V: Render,
|
||||
{
|
||||
virtual_list(view, id, Axis::Vertical, item_sizes, f)
|
||||
}
|
||||
|
||||
/// Create a virtual list in Horizontal direction.
|
||||
pub fn h_virtual_list<R, V>(
|
||||
view: View<V>,
|
||||
id: impl Into<ElementId>,
|
||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, Size<Pixels>, &mut ViewContext<V>) -> Vec<R>,
|
||||
) -> VirtualList
|
||||
where
|
||||
R: IntoElement,
|
||||
V: Render,
|
||||
{
|
||||
virtual_list(view, id, Axis::Horizontal, item_sizes, f)
|
||||
}
|
||||
|
||||
pub(crate) fn virtual_list<R, V>(
|
||||
view: View<V>,
|
||||
id: impl Into<ElementId>,
|
||||
axis: Axis,
|
||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, Size<Pixels>, &mut ViewContext<V>) -> Vec<R>,
|
||||
) -> VirtualList
|
||||
where
|
||||
R: IntoElement,
|
||||
V: Render,
|
||||
{
|
||||
let id: ElementId = id.into();
|
||||
let scroll_handle = ScrollHandle::default();
|
||||
let render_range = move |visible_range, content_size, cx: &mut WindowContext| {
|
||||
view.update(cx, |this, cx| {
|
||||
f(this, visible_range, content_size, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.into_any_element())
|
||||
.collect()
|
||||
})
|
||||
};
|
||||
|
||||
VirtualList {
|
||||
id: id.clone(),
|
||||
axis,
|
||||
base: div()
|
||||
.id(id)
|
||||
.size_full()
|
||||
.overflow_scroll()
|
||||
.track_scroll(&scroll_handle),
|
||||
scroll_handle,
|
||||
items_count: item_sizes.len(),
|
||||
item_sizes,
|
||||
render_items: Box::new(render_range),
|
||||
}
|
||||
}
|
||||
|
||||
type RenderItems = Box<
|
||||
dyn for<'a> Fn(Range<usize>, Size<Pixels>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>,
|
||||
>;
|
||||
|
||||
/// VirtualItem component for rendering a large number of differently sized columns.
|
||||
pub struct VirtualList {
|
||||
id: ElementId,
|
||||
axis: Axis,
|
||||
base: Stateful<Div>,
|
||||
scroll_handle: ScrollHandle,
|
||||
// scroll_handle: ScrollHandle,
|
||||
items_count: usize,
|
||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||
render_items: RenderItems,
|
||||
}
|
||||
|
||||
impl Styled for VirtualList {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualList {
|
||||
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||
self.base = self.base.track_scroll(scroll_handle);
|
||||
self.scroll_handle = scroll_handle.clone();
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify for table
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn with_scroll_handle(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||
self.base = div().id(self.id.clone()).size_full();
|
||||
self.scroll_handle = scroll_handle.clone();
|
||||
self
|
||||
}
|
||||
|
||||
/// Measure first item to get the size.
|
||||
fn measure_item(&self, cx: &mut WindowContext) -> Size<Pixels> {
|
||||
if self.items_count == 0 {
|
||||
return Size::default();
|
||||
}
|
||||
|
||||
let item_ix = 0;
|
||||
let mut items = (self.render_items)(item_ix..item_ix + 1, Size::default(), cx);
|
||||
let Some(mut item_to_measure) = items.pop() else {
|
||||
return Size::default();
|
||||
};
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
item_to_measure.layout_as_root(available_space, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame state used by the [VirtualItem].
|
||||
pub struct VirtualListFrameState {
|
||||
/// Visible items to be painted.
|
||||
items: SmallVec<[AnyElement; 32]>,
|
||||
item_sizes: Vec<Pixels>,
|
||||
item_origins: Vec<Pixels>,
|
||||
}
|
||||
|
||||
impl IntoElement for VirtualList {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for VirtualList {
|
||||
type RequestLayoutState = VirtualListFrameState;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let style = self.base.interactivity().compute_style(global_id, None, cx);
|
||||
let font_size = cx.text_style().font_size.to_pixels(cx.rem_size());
|
||||
|
||||
// Including the gap between items for calculate the item size
|
||||
let gap = match self.axis {
|
||||
Axis::Horizontal => style.gap.width,
|
||||
Axis::Vertical => style.gap.height,
|
||||
}
|
||||
.to_pixels(font_size.into(), cx.rem_size());
|
||||
|
||||
// TODO: To cache the item_sizes, item_origins
|
||||
// If there have 500,000 items, this method will speed about 500~600µs
|
||||
// let start = std::time::Instant::now();
|
||||
// Prepare each item's size by axis
|
||||
let item_sizes = match self.axis {
|
||||
Axis::Horizontal => self
|
||||
.item_sizes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, size)| {
|
||||
if i == self.items_count - 1 {
|
||||
size.width
|
||||
} else {
|
||||
size.width + gap
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Axis::Vertical => self
|
||||
.item_sizes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, size)| {
|
||||
if i == self.items_count - 1 {
|
||||
size.height
|
||||
} else {
|
||||
size.height + gap
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
|
||||
// Prepare each item's origin by axis
|
||||
let item_origins = match self.axis {
|
||||
Axis::Horizontal => item_sizes
|
||||
.iter()
|
||||
.scan(px(0.), |cumulative_x, size| {
|
||||
let x = *cumulative_x;
|
||||
*cumulative_x += *size;
|
||||
Some(x)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Axis::Vertical => item_sizes
|
||||
.iter()
|
||||
.scan(px(0.), |cumulative_y, size| {
|
||||
let y = *cumulative_y;
|
||||
*cumulative_y += *size;
|
||||
Some(y)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
// println!("layout: {} {:?}", item_sizes.len(), start.elapsed());
|
||||
|
||||
let (layout_id, _) = self.base.request_layout(global_id, cx);
|
||||
|
||||
(
|
||||
layout_id,
|
||||
VirtualListFrameState {
|
||||
items: SmallVec::new(),
|
||||
item_sizes,
|
||||
item_origins,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self::PrepaintState {
|
||||
let style = self.base.interactivity().compute_style(global_id, None, cx);
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
let first_item_size = self.measure_item(cx);
|
||||
|
||||
let padded_bounds = Bounds::from_corners(
|
||||
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||
bounds.bottom_right()
|
||||
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||
);
|
||||
|
||||
// Get border + padding pixel size
|
||||
let padding_size = match self.axis {
|
||||
Axis::Horizontal => border.left + padding.left + border.right + padding.right,
|
||||
Axis::Vertical => border.top + padding.top + border.bottom + padding.bottom,
|
||||
};
|
||||
|
||||
let item_sizes = &layout.item_sizes;
|
||||
let item_origins = &layout.item_origins;
|
||||
|
||||
let content_size = match self.axis {
|
||||
Axis::Horizontal => Size {
|
||||
width: px(item_sizes.iter().map(|size| size.0).sum::<f32>()) + padding_size,
|
||||
height: (first_item_size.height + padding_size).max(padded_bounds.size.height),
|
||||
},
|
||||
Axis::Vertical => Size {
|
||||
width: (first_item_size.width + padding_size).max(padded_bounds.size.width),
|
||||
height: px(item_sizes.iter().map(|size| size.0).sum::<f32>()) + padding_size,
|
||||
},
|
||||
};
|
||||
|
||||
self.base.interactivity().prepaint(
|
||||
global_id,
|
||||
bounds,
|
||||
content_size,
|
||||
cx,
|
||||
|style, _, hitbox, cx| {
|
||||
let mut scroll_offset = self.scroll_handle.offset();
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
let padded_bounds = Bounds::from_corners(
|
||||
bounds.origin + point(border.left + padding.left, border.top),
|
||||
bounds.bottom_right() - point(border.right + padding.right, border.bottom),
|
||||
);
|
||||
|
||||
if self.items_count > 0 {
|
||||
let is_scrolled = match self.axis {
|
||||
Axis::Horizontal => !scroll_offset.x.is_zero(),
|
||||
Axis::Vertical => !scroll_offset.y.is_zero(),
|
||||
};
|
||||
|
||||
let min_scroll_offset = match self.axis {
|
||||
Axis::Horizontal => padded_bounds.size.width - content_size.width,
|
||||
Axis::Vertical => padded_bounds.size.height - content_size.height,
|
||||
};
|
||||
|
||||
if is_scrolled {
|
||||
match self.axis {
|
||||
Axis::Horizontal if scroll_offset.x < min_scroll_offset => {
|
||||
scroll_offset.x = min_scroll_offset;
|
||||
}
|
||||
Axis::Vertical if scroll_offset.y < min_scroll_offset => {
|
||||
scroll_offset.y = min_scroll_offset;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let (first_visible_element_ix, last_visible_element_ix) = match self.axis {
|
||||
Axis::Horizontal => {
|
||||
let mut cumulative_size = px(0.);
|
||||
let mut first_visible_element_ix = 0;
|
||||
for (i, &size) in item_sizes.iter().enumerate() {
|
||||
cumulative_size += size;
|
||||
if cumulative_size > -(scroll_offset.x + padding.left) {
|
||||
first_visible_element_ix = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cumulative_size = px(0.);
|
||||
let mut last_visible_element_ix = 0;
|
||||
for (i, &size) in item_sizes.iter().enumerate() {
|
||||
cumulative_size += size;
|
||||
if cumulative_size > (-scroll_offset.x + padded_bounds.size.width) {
|
||||
last_visible_element_ix = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if last_visible_element_ix == 0 {
|
||||
last_visible_element_ix = self.items_count;
|
||||
} else {
|
||||
last_visible_element_ix += 1;
|
||||
}
|
||||
(first_visible_element_ix, last_visible_element_ix)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let mut cumulative_size = px(0.);
|
||||
let mut first_visible_element_ix = 0;
|
||||
for (i, &size) in item_sizes.iter().enumerate() {
|
||||
cumulative_size += size;
|
||||
if cumulative_size > -(scroll_offset.y + padding.top) {
|
||||
first_visible_element_ix = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cumulative_size = px(0.);
|
||||
let mut last_visible_element_ix = 0;
|
||||
for (i, &size) in item_sizes.iter().enumerate() {
|
||||
cumulative_size += size;
|
||||
if cumulative_size > (-scroll_offset.y + padded_bounds.size.height)
|
||||
{
|
||||
last_visible_element_ix = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if last_visible_element_ix == 0 {
|
||||
last_visible_element_ix = self.items_count;
|
||||
} else {
|
||||
last_visible_element_ix += 1;
|
||||
}
|
||||
(first_visible_element_ix, last_visible_element_ix)
|
||||
}
|
||||
};
|
||||
|
||||
let visible_range = first_visible_element_ix
|
||||
..cmp::min(last_visible_element_ix, self.items_count);
|
||||
|
||||
let items = (self.render_items)(visible_range.clone(), content_size, cx);
|
||||
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
|
||||
let item_origin = match self.axis {
|
||||
Axis::Horizontal => {
|
||||
padded_bounds.origin
|
||||
+ point(
|
||||
item_origins[ix] + scroll_offset.x,
|
||||
padding.top + scroll_offset.y,
|
||||
)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
padded_bounds.origin
|
||||
+ point(
|
||||
scroll_offset.x,
|
||||
padding.top + item_origins[ix] + scroll_offset.y,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let available_space = match self.axis {
|
||||
Axis::Horizontal => size(
|
||||
AvailableSpace::Definite(item_sizes[ix]),
|
||||
AvailableSpace::Definite(padded_bounds.size.height),
|
||||
),
|
||||
Axis::Vertical => size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_sizes[ix]),
|
||||
),
|
||||
};
|
||||
|
||||
item.layout_as_root(available_space, cx);
|
||||
item.prepaint_at(item_origin, cx);
|
||||
layout.items.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hitbox
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.base
|
||||
.interactivity()
|
||||
.paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| {
|
||||
for item in &mut layout.items {
|
||||
item.paint(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user