Jason Lee 2024-07-12 15:24:54 +08:00 committed by GitHub
parent 9e935bed56
commit 2401d5d5a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 488 additions and 37 deletions

5
Cargo.lock generated
View file

@ -5427,6 +5427,7 @@ dependencies = [
"taffy",
"unicode-segmentation",
"usvg",
"uuid",
"windows",
"wry",
]
@ -5577,9 +5578,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.8.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom 0.2.15",
"serde",

View file

@ -56,7 +56,7 @@ This is an example of build app by using GPUI.
- [x] Floating Popover
- [x] Child window Popover
- [x] Dockpanel
- [ ] Splitter
- [x] Resizable
- [ ] Progress
- [ ] ProgressBar
- [ ] Loading

View file

@ -2,7 +2,8 @@ use gpui::*;
use prelude::FluentBuilder as _;
use story::{
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory,
PopoverStory, ProgressStory, StoryContainer, SwitchStory, TableStory, TooltipStory,
PopoverStory, ProgressStory, ResizableStory, StoryContainer, SwitchStory, TableStory,
TooltipStory,
};
use workspace::{dock::DockPosition, TitleBar, Workspace};
@ -54,7 +55,7 @@ impl StoryWorkspace {
InputStory::view(cx).into(),
workspace.clone(),
DockPosition::Right,
px(500.0),
px(350.0),
cx,
);
@ -62,7 +63,7 @@ impl StoryWorkspace {
CheckboxStory::view(cx).into(),
workspace.clone(),
DockPosition::Bottom,
px(300.),
px(200.),
cx,
);
@ -115,7 +116,7 @@ impl StoryWorkspace {
ListStory::view(cx).into(),
workspace.clone(),
DockPosition::Left,
px(360.),
px(300.),
cx,
);
@ -146,6 +147,15 @@ impl StoryWorkspace {
)
.detach();
StoryContainer::add_pane(
"Resizable",
"Accessible resizable panel groups and layouts with keyboard support.",
ResizableStory::view(cx).into(),
workspace.clone(),
cx,
)
.detach();
Self { workspace }
}

View file

@ -7,6 +7,7 @@ mod list_story;
mod picker_story;
mod popover_story;
mod progress_story;
mod resizable_story;
mod switch_story;
mod table_story;
mod tooltip_story;
@ -20,6 +21,7 @@ pub use list_story::ListStory;
pub use picker_story::PickerStory;
pub use popover_story::PopoverStory;
pub use progress_story::ProgressStory;
pub use resizable_story::ResizableStory;
pub use switch_story::SwitchStory;
pub use table_story::TableStory;
pub use tooltip_story::TooltipStory;
@ -206,7 +208,9 @@ impl StoryContainer {
impl Render for StoryContainer {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.id("story-container")
.size_full()
.overflow_scroll()
.child(
div()
.flex()

View file

@ -36,7 +36,7 @@ impl ListDelegate for ListItemDeletegate {
.items
.iter()
.filter(|item| item.to_lowercase().contains(&query.to_lowercase()))
.map(|s| s.clone())
.cloned()
.collect();
cx.notify();
}

View file

@ -0,0 +1,109 @@
use gpui::{
div, px, AnyElement, IntoElement, ParentElement as _, Render, SharedString, Styled, View,
ViewContext, VisualContext, WindowContext,
};
use ui::theme::ActiveTheme;
use ui::{
resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanelGroup},
v_flex,
};
pub struct ResizableStory {
group1: View<ResizablePanelGroup>,
group2: View<ResizablePanelGroup>,
}
impl ResizableStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.new_view(|cx| Self::new(cx))
}
fn new(cx: &mut WindowContext) -> Self {
fn panel_box(content: impl Into<SharedString>, cx: &WindowContext) -> AnyElement {
div()
.p_4()
.border_1()
.border_color(cx.theme().border)
.size_full()
.child(content.into())
.into_any_element()
}
let group1 = cx.new_view(|cx| {
v_resizable()
.group(
h_resizable()
.size(px(150.))
.child(
resizable_panel()
.size(px(300.))
.min_size(px(120.))
.content(|cx| panel_box("Left 1 (Min 120px)", cx)),
cx,
)
.child(
resizable_panel()
.size(px(400.))
.min_size(px(100.))
.content(|cx| panel_box("Center 1", cx)),
cx,
)
.child(
resizable_panel()
.size(px(300.))
.min_size(px(100.))
.grow()
.content(|cx| panel_box("Right (Grow)", cx)),
cx,
),
cx,
)
.child(
resizable_panel()
.size(px(150.))
.max_size(px(550.))
.min_size(px(100.))
.grow()
.content(|cx| panel_box("Center (Grow)", cx)),
cx,
)
.child(
resizable_panel()
.size(px(210.))
.min_size(px(100.))
.content(|cx| panel_box("Bottom", cx)),
cx,
)
});
let group2 = cx.new_view(|cx| {
h_resizable()
.child(
resizable_panel()
.size(px(300.))
.min_size(px(100.))
.content(|cx| panel_box("Left 2", cx)),
cx,
)
.child(
resizable_panel()
.size(px(400.))
.max_size(px(550.))
.min_size(px(100.))
.grow()
.content(|cx| panel_box("Right (Grow)", cx)),
cx,
)
});
Self { group1, group2 }
}
}
impl Render for ResizableStory {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.gap_6()
.child(self.group1.clone())
.child(self.group2.clone())
}
}

View file

@ -24,6 +24,7 @@ usvg = { version = "0.41.0", default-features = false }
taffy = "0.4.3"
paste = "1"
once_cell = "1.19.0"
uuid = "1.10.0"
[lints]
workspace = true

View file

@ -22,6 +22,7 @@ pub mod popover;
pub mod popup_menu;
pub mod prelude;
pub mod progress;
pub mod resizable;
pub mod switch;
pub mod tab;
pub mod table;

View file

@ -0,0 +1,16 @@
use gpui::Axis;
mod panel;
pub use panel::*;
pub fn h_resizable() -> ResizablePanelGroup {
ResizablePanelGroup::new().axis(Axis::Horizontal)
}
pub fn v_resizable() -> ResizablePanelGroup {
ResizablePanelGroup::new().axis(Axis::Vertical)
}
pub fn resizable_panel() -> ResizablePanel {
ResizablePanel::new()
}

View file

@ -0,0 +1,278 @@
use std::rc::Rc;
use gpui::{
canvas, deferred, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, Axis, Bounds,
DragMoveEvent, EntityId, InteractiveElement as _, IntoElement, ParentElement, Pixels, Render,
StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, WindowContext,
};
use crate::{h_flex, theme::ActiveTheme, v_flex};
#[derive(Clone, Render)]
pub struct DragPanel(pub (EntityId, usize, Axis));
#[derive(Clone)]
pub struct ResizablePanelGroup {
panels: Vec<View<ResizablePanel>>,
sizes: Vec<Pixels>,
axis: Axis,
handle_size: Pixels,
size: Pixels,
}
impl ResizablePanelGroup {
pub(super) fn new() -> Self {
Self {
axis: Axis::Horizontal,
sizes: Vec::new(),
panels: Vec::new(),
handle_size: px(3.),
size: px(20.),
}
}
/// Set the axis of the resizable panel group, default is horizontal.
pub fn axis(mut self, axis: Axis) -> Self {
self.axis = axis;
self
}
/// Set the size of the resize handle, default is 3px.
///
/// The handle size will inherit the parent group handle size, if you insert a group into another group.
pub fn handle_size(mut self, size: Pixels) -> Self {
self.handle_size = size;
self
}
/// Add a resizable panel to the group.
pub fn child(mut self, panel: ResizablePanel, cx: &mut WindowContext) -> Self {
let mut panel = panel;
panel.axis = self.axis;
self.sizes.push(panel.size);
self.panels.push(cx.new_view(|_| panel));
self
}
/// Add a ResizablePanelGroup as a child to the group.
pub fn group(self, group: ResizablePanelGroup, cx: &mut WindowContext) -> Self {
let mut group: ResizablePanelGroup = group;
group.handle_size = self.handle_size;
let size = group.size;
let panel = ResizablePanel::new()
.content_view(cx.new_view(|_| group).into())
.size(size);
self.child(panel, cx)
}
/// Set size of the resizable panel group
///
/// - When the axis is horizontal, the size is the height of the group.
/// - When the axis is vertical, the size is the width of the group.
pub fn size(mut self, size: Pixels) -> Self {
self.size = size;
self
}
fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
let axis = self.axis;
let handle_size = self.handle_size;
deferred(
div()
.id(("resizable-handle", ix))
.occlude()
.hover(|this| this.bg(cx.theme().drag_border))
.on_drag_move(cx.listener(move |view, e: &DragMoveEvent<DragPanel>, cx| {
match e.drag(cx) {
DragPanel((entity_id, ix, axis)) => {
let ix = *ix;
if cx.entity_id() != *entity_id {
return;
}
let panel = view
.panels
.get(ix)
.expect("BUG: invalid panel index")
.read(cx);
match axis {
Axis::Horizontal => {
let size = e.event.position.x - panel.bounds.left();
view.resize_panels(ix, size, cx)
}
Axis::Vertical => {
let size = e.event.position.y - panel.bounds.top();
view.resize_panels(ix, size, cx);
}
}
}
}
}))
.when(self.axis == Axis::Horizontal, |this| {
this.cursor_col_resize().top_0().w(handle_size).h_full()
})
.when(self.axis == Axis::Vertical, |this| {
this.cursor_row_resize().left_0().w_full().h(handle_size)
})
.on_drag(DragPanel((cx.entity_id(), ix, axis)), |drag_panel, cx| {
cx.stop_propagation();
cx.new_view(|_| drag_panel.clone())
}),
)
}
/// The `ix`` is the index of the panel to resize,
/// and the `size` is the new size for the panel.
fn resize_panels(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext<Self>) {
// Only resize the middle panels.
if ix == self.panels.len() - 1 {
return;
}
let old_size = self.sizes[ix];
let size = self.panels[ix].read(cx).limit_size(size);
let changed_size = size - old_size;
// If change size is less than 1px, do nothing.
if changed_size > px(-1.0) && changed_size < px(1.0) {
return;
}
self.sizes[ix] = size;
let next_size = self.sizes[ix + 1];
self.sizes[ix + 1] = self.panels[ix + 1]
.read(cx)
.limit_size(next_size - changed_size);
for (i, panel) in self.panels.iter_mut().enumerate() {
let size = self.sizes[i];
panel.update(cx, |this, _| this.size = size);
}
cx.notify();
}
}
impl Render for ResizablePanelGroup {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let mut children: Vec<AnyElement> = vec![];
for (ix, panel) in self.panels.iter().enumerate() {
children.push(panel.clone().into_any_element());
if ix < self.panels.len() - 1 {
children.push(self.render_resize_handle(ix, cx).into_any_element());
}
}
let container = if self.axis == Axis::Horizontal {
h_flex()
} else {
v_flex()
};
container.size_full().children(children)
}
}
pub struct ResizablePanel {
size: Pixels,
max_size: Option<Pixels>,
min_size: Option<Pixels>,
axis: Axis,
content_builder: Option<Rc<dyn Fn(&mut WindowContext) -> AnyElement>>,
content_view: Option<AnyView>,
/// The bounds of the resizable panel, when render the bounds will be updated.
bounds: Bounds<Pixels>,
grow: bool,
}
impl ResizablePanel {
pub(super) fn new() -> Self {
Self {
size: px(20.),
axis: Axis::Horizontal,
max_size: None,
min_size: None,
content_builder: None,
content_view: None,
bounds: Bounds::default(),
grow: false,
}
}
pub fn content<F>(mut self, content: F) -> Self
where
F: Fn(&mut WindowContext) -> AnyElement + 'static,
{
self.content_builder = Some(Rc::new(content));
self
}
pub fn content_view(mut self, content: AnyView) -> Self {
self.content_view = Some(content);
self
}
pub fn size(mut self, size: Pixels) -> Self {
self.size = size;
self
}
pub fn max_size(mut self, max_size: Pixels) -> Self {
self.max_size = Some(max_size);
self
}
pub fn min_size(mut self, min_size: Pixels) -> Self {
self.min_size = Some(min_size);
self
}
fn limit_size(&self, size: Pixels) -> Pixels {
if let Some(max_size) = self.max_size {
if size > max_size {
return max_size;
}
}
if let Some(min_size) = self.min_size {
if size < min_size {
return min_size;
}
}
size
}
/// Set the panel to grow to fill the remaining space.
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
}
impl Render for ResizablePanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let size = self.limit_size(self.size);
div()
.size_full()
.relative()
.when(self.grow, |this| this.flex_grow())
.when(self.axis == Axis::Vertical, |this| this.h(size))
.when(self.axis == Axis::Horizontal, |this| this.w(size))
.overflow_hidden()
.child({
let view = cx.view().clone();
canvas(
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
})
.when_some(self.content_builder.clone(), |this, c| this.child(c(cx)))
.when_some(self.content_view.clone(), |this, c| this.child(c))
}
}

View file

@ -127,15 +127,8 @@ pub trait StyledExt: Styled + Sized {
}
/// Render a border with a width of 1px, color ring color
///
/// Please ensure this after the shadow
fn outline(self, cx: &WindowContext) -> Self {
self.shadow(smallvec![BoxShadow {
color: cx.theme().ring,
offset: point(px(0.), px(0.)),
blur_radius: px(0.1),
spread_radius: px(1.),
}])
self.border_color(cx.theme().ring)
}
}

View file

@ -1,10 +1,9 @@
use crate::selectable::Selectable;
use crate::theme::{ActiveTheme, Colorize};
use gpui::prelude::FluentBuilder as _;
use gpui::InteractiveElement;
use gpui::{
div, px, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Stateful,
StatefulInteractiveElement, Styled, WindowContext,
div, AnyElement, Div, ElementId, InteractiveElement, IntoElement, ParentElement as _,
RenderOnce, Stateful, StatefulInteractiveElement, Styled, WindowContext,
};
#[derive(IntoElement)]
@ -20,7 +19,7 @@ pub struct Tab {
impl Tab {
pub fn new(id: impl Into<ElementId>, label: impl Into<AnyElement>) -> Self {
Self {
base: div().id(id.into()).gap_1().py_1().px_3(),
base: div().id(id.into()).gap_1().py_1p5().px_3().h_8(),
label: label.into(),
disabled: false,
selected: false,
@ -66,20 +65,23 @@ impl Styled for Tab {
impl RenderOnce for Tab {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (text_color, bg_color) = match (self.selected, self.disabled) {
(true, _) => (cx.theme().foreground, cx.theme().background),
(false, true) => (cx.theme().foreground.opacity(0.5), cx.theme().muted),
(false, false) => (cx.theme().muted_foreground, cx.theme().muted),
(true, _) => (cx.theme().tab_active_foreground, cx.theme().tab_active),
(false, true) => (cx.theme().tab_foreground.opacity(0.5), cx.theme().tab),
(false, false) => (cx.theme().muted_foreground, cx.theme().tab),
};
self.base
.flex()
.items_center()
.h_full()
.flex_shrink_0()
.cursor_pointer()
.text_color(text_color)
.bg(bg_color)
.when(self.selected, |this| this.rounded(px(6.)))
.border_x_1()
.border_color(bg_color)
.border_color(cx.theme().transparent)
.when(self.selected, |this| this.border_color(cx.theme().border))
.text_sm()
.when(self.disabled, |this| this)
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)

View file

@ -1,10 +1,10 @@
use crate::stack::h_flex;
use crate::theme::ActiveTheme;
use gpui::InteractiveElement;
use gpui::{
div, AnyElement, Div, IntoElement, ParentElement, RenderOnce, ScrollHandle, SharedString,
StatefulInteractiveElement as _, Styled, WindowContext,
};
use gpui::{px, InteractiveElement};
use smallvec::SmallVec;
#[derive(IntoElement)]
@ -18,7 +18,7 @@ pub struct TabBar {
impl TabBar {
pub fn new(id: impl Into<SharedString>) -> Self {
Self {
base: div().h_10().p_1(),
base: div().h_8().px(px(-1.)),
id: id.into(),
children: SmallVec::new(),
scroll_handle: ScrollHandle::new(),
@ -54,8 +54,10 @@ impl RenderOnce for TabBar {
.flex()
.flex_none()
.items_center()
.bg(theme.muted)
.text_color(theme.muted_foreground)
.bg(theme.tab_bar)
.border_b_1()
.border_color(cx.theme().border)
.text_color(theme.tab_foreground)
// The child will append to this level
.child(
h_flex()

View file

@ -1,4 +1,4 @@
use gpui::{hsla, AppContext, Global, Hsla, WindowAppearance};
use gpui::{hsla, point, AppContext, BoxShadow, Global, Hsla, Pixels, WindowAppearance};
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
@ -19,6 +19,27 @@ pub fn hsl(h: f32, s: f32, l: f32) -> Hsla {
hsla(h / 360., s / 100.0, l / 100.0, 1.0)
}
/// Make a BoxShadow like CSS
///
/// e.g:
///
/// If CSS is `box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);`
///
/// Then the equivalent in Rust is `box_shadow(0., 0., 10., 0., hsla(0., 0., 0., 0.1))`
pub fn box_shadow(
x: impl Into<Pixels>,
y: impl Into<Pixels>,
blur: impl Into<Pixels>,
spread: impl Into<Pixels>,
color: Hsla,
) -> BoxShadow {
BoxShadow {
offset: point(x.into(), y.into()),
blur_radius: blur.into(),
spread_radius: spread.into(),
color,
}
}
pub trait Colorize {
fn opacity(&self, opacity: f32) -> Hsla;
fn divide(&self, divisor: f32) -> Hsla;
@ -112,7 +133,7 @@ struct Colors {
pub scrollbar: Hsla,
pub scrollbar_thumb: Hsla,
pub panel: Hsla,
pub drop_target: Hsla,
pub tab_bar: Hsla,
}
impl Colors {
@ -172,7 +193,7 @@ impl Colors {
scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 5.9, 85.0).opacity(0.7),
panel: hsl(0.0, 0.0, 100.0),
drop_target: hsl(240.0, 65., 44.0).opacity(0.15),
tab_bar: hsl(240.0, 4.8, 95.9),
}
}
@ -231,7 +252,7 @@ impl Colors {
scrollbar: Hsla::transparent_black(),
scrollbar_thumb: hsl(240.0, 3.7, 15.9).opacity(0.7),
panel: hsl(299.0, 2., 9.),
drop_target: hsl(240.0, 89., 67.0).opacity(0.8),
tab_bar: hsl(299.0, 2., 9.),
}
}
}
@ -272,8 +293,14 @@ pub struct Theme {
pub scrollbar: Hsla,
pub scrollbar_thumb: Hsla,
pub panel: Hsla,
pub drag_border: Hsla,
pub drop_target: Hsla,
pub radius: f32,
pub tab_bar: Hsla,
pub tab: Hsla,
pub tab_active: Hsla,
pub tab_foreground: Hsla,
pub tab_active_foreground: Hsla,
}
impl Global for Theme {}
@ -321,7 +348,13 @@ impl From<Colors> for Theme {
scrollbar_thumb: colors.scrollbar_thumb,
panel: colors.panel,
selection: colors.selection,
drop_target: colors.drop_target,
drag_border: crate::blue_500(),
drop_target: hsl(240.0, 65., 44.0).opacity(0.15),
tab_bar: colors.tab_bar,
tab: gpui::transparent_black(),
tab_active: colors.background,
tab_foreground: colors.foreground,
tab_active_foreground: colors.foreground,
}
}
}

View file

@ -696,9 +696,10 @@ impl Pane {
|tab, cx| cx.new_view(|_| tab.clone()),
)
.drag_over::<DraggedTab>(|tab, _, cx| {
tab.border_l_3()
.rounded_l_none()
.border_color(cx.theme().drop_target)
tab.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
})
.drag_over::<DraggedSelection>(|tab, _, cx| tab.bg(cx.theme().drop_target))
.when_some(self.can_drop_predicate.clone(), |this, p| {