T03 layout: stacks, grids, surfaces, scroll, splits, sheets, dock, panes, workspace, viewport

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 02:17:37 -04:00
1 parent b35530c80d
commit 2bdf902bf4
37 files changed
+5283 -50

No files matched your search

+256
View File
@@ -0,0 +1,256 @@
use ely_gpui_component::{
layout::{AspectRatio, Container, Masonry, SimpleGrid, ZStack, h_stack, v_stack},
theme::{ActiveTheme, ContainerSize, Radius, TextSize},
typography::Caption,
};
use gpui::{App, Div, Hsla, IntoElement, ParentElement, SharedString, Styled, div, px};
use crate::ui::{code, section, specimen, specimens};
pub fn tile(label: impl Into<SharedString>, cx: &App) -> Div {
let theme = cx.theme();
div()
.flex()
.items_center()
.justify_center()
.px_3()
.py_2()
.rounded(theme.radius(Radius::Md))
.bg(theme.colors.sunken)
.border_1()
.border_color(theme.colors.border)
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child(label.into())
}
fn outline(cx: &App) -> Div {
let theme = cx.theme();
div()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
}
pub fn stacks(cx: &App) -> impl IntoElement + use<> {
section(
"Stack / HStack / VStack",
"h_stack() and v_stack(): flex rows and columns.",
cx,
)
.child(
specimens()
.child(specimen(
"h_stack",
h_stack()
.gap_2()
.child(tile("A", cx))
.child(tile("B", cx))
.child(tile("C", cx)),
cx,
))
.child(specimen(
"v_stack",
v_stack().gap_2().child(tile("A", cx)).child(tile("B", cx)),
cx,
)),
)
}
pub fn flex(cx: &App) -> impl IntoElement + use<> {
section("Flex", "gpui's .flex(), with grow and justify.", cx).child(
outline(cx)
.p_2()
.w(px(420.0))
.flex()
.gap_2()
.child(tile("fixed", cx))
.child(tile("flex_1", cx).flex_1())
.child(tile("fixed", cx)),
)
}
pub fn grid(cx: &App) -> impl IntoElement + use<> {
section("Grid / GridItem", "gpui's .grid() and .col_span().", cx).child(
div()
.w(px(420.0))
.grid()
.grid_cols(3)
.gap_2()
.child(tile("span 2", cx).col_span(2))
.child(tile("1", cx))
.child(tile("1", cx))
.child(tile("span 2", cx).col_span(2)),
)
}
pub fn simple_grid(cx: &App) -> impl IntoElement + use<> {
section(
"SimpleGrid",
"As many equal columns as fit at a minimum width.",
cx,
)
.child(
SimpleGrid::new("simple-grid", px(140.0), px(12.0))
.max_w(px(720.0))
.children((1..=7).map(|ix| tile(format!("Item {ix}"), cx).h(px(56.0)))),
)
}
pub fn masonry(cx: &App) -> impl IntoElement + use<> {
let heights = [72.0, 120.0, 56.0, 96.0, 140.0, 64.0, 88.0, 110.0];
section("Masonry", "Each item joins the shortest column.", cx).child(
div().w(px(560.0)).child(
Masonry::new("masonry", 3, px(10.0)).children(
heights
.iter()
.enumerate()
.map(|(ix, height)| tile(format!("{}", ix + 1), cx).h(px(*height))),
),
),
)
}
pub fn center(cx: &App) -> impl IntoElement + use<> {
section("Center", "flex, items_center, justify_center.", cx).child(
outline(cx)
.w(px(280.0))
.h(px(96.0))
.flex()
.items_center()
.justify_center()
.child(tile("centered", cx)),
)
}
pub fn container(cx: &App) -> impl IntoElement + use<> {
section(
"Container",
"A centered column capped by a width token.",
cx,
)
.child(
outline(cx).w_full().py_3().child(
Container::new(ContainerSize::Sm).child(
div()
.h(px(40.0))
.rounded(cx.theme().radius(Radius::Md))
.bg(cx.theme().colors.sunken),
),
),
)
.child(code("Container::new(ContainerSize::Sm)", cx))
}
pub fn aspect_ratio(cx: &App) -> impl IntoElement + use<> {
let fill = |ratio: &str, cx: &App| {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.bg(cx.theme().colors.sunken)
.text_color(cx.theme().colors.fg_subtle)
.child(SharedString::from(ratio.to_string()))
};
section("AspectRatio", "Width fills; height follows the ratio.", cx).child(
specimens()
.child(
div().w(px(240.0)).child(
AspectRatio::new(16.0 / 9.0)
.rounded(cx.theme().radius(Radius::Lg))
.child(fill("16 : 9", cx)),
),
)
.child(
div().w(px(120.0)).child(
AspectRatio::new(1.0)
.rounded(cx.theme().radius(Radius::Lg))
.child(fill("1 : 1", cx)),
),
),
)
}
pub fn inset(cx: &App) -> impl IntoElement + use<> {
section("Inset / Padding", "gpui's .p_*() on the rem scale.", cx).child(
specimens()
.child(specimen(
"p_2",
outline(cx).p_2().child(tile("content", cx)),
cx,
))
.child(specimen(
"p_6",
outline(cx).p_6().child(tile("content", cx)),
cx,
)),
)
}
pub fn wrap(cx: &App) -> impl IntoElement + use<> {
let tags = [
"design", "motion", "type", "color", "layout", "grid", "rhythm", "space",
];
section(
"Wrap",
"gpui's .flex_wrap() moves overflow to the next line.",
cx,
)
.child(
outline(cx)
.p_3()
.w(px(320.0))
.flex()
.flex_wrap()
.gap_2()
.children(tags.map(|tag| tile(tag, cx))),
)
}
pub fn absolute(cx: &App) -> impl IntoElement + use<> {
section(
"Absolute / Positioned",
"gpui's .absolute() with edge offsets.",
cx,
)
.child(
outline(cx)
.relative()
.w(px(280.0))
.h(px(112.0))
.child(tile("top left", cx).absolute().top_2().left_2())
.child(tile("bottom right", cx).absolute().bottom_2().right_2()),
)
}
pub fn z_stack(cx: &App) -> impl IntoElement + use<> {
let colors = &cx.theme().colors;
let layer = |color: Hsla, inset: f32| {
div()
.size_full()
.p(px(inset))
.child(div().size_full().rounded_md().bg(color))
};
section(
"Layer / ZStack",
"The first child sets the size; the rest cover it.",
cx,
)
.child(
ZStack::new()
.w(px(200.0))
.h(px(120.0))
.child(div().size_full().rounded_lg().bg(colors.sunken))
.child(layer(colors.hover, 16.0))
.child(layer(colors.active, 32.0))
.child(
div()
.size_full()
.flex()
.items_center()
.justify_center()
.child(Caption::new("three layers")),
),
)
}
+128
View File
@@ -0,0 +1,128 @@
mod basics;
mod regions;
mod scrolling;
mod shells;
mod systems;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 3,
slug: "layout",
title: "Layout",
summary: "Stacks, grids, splits, docks. Structure that stays out of the way.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::Hover("scroll-area"),
Step::Wait(300),
Step::Shot("scroll-hover"),
Step::DownAt("scrollbar", 313.0, 12.0),
Step::DragTo("scrollbar", 313.0, 110.0),
Step::UpAt("scrollbar", 313.0, 110.0),
Step::Shot("scrollbar-drag"),
Step::DownAt("scrollbar", 313.0, 110.0),
Step::DragTo("scrollbar", 313.0, 12.0),
Step::UpAt("scrollbar", 313.0, 12.0),
Step::DownAt("sticky", 313.0, 12.0),
Step::DragTo("sticky", 313.0, 76.0),
Step::UpAt("sticky", 313.0, 76.0),
Step::Shot("sticky-pushed"),
Step::DownAt("sticky", 313.0, 76.0),
Step::DragTo("sticky", 313.0, 12.0),
Step::UpAt("sticky", 313.0, 12.0),
Step::Click("collapse-toggle"),
Step::Wait(400),
Step::Shot("collapsed"),
Step::Click("collapse-toggle"),
Step::Click("sidebar-toggle"),
Step::Wait(400),
Step::Shot("sidebar-collapsed"),
Step::Click("sidebar-toggle"),
Step::Rest,
Step::Click("sheet-open"),
Step::Wait(400),
Step::Shot("sheet"),
Step::Click("sheet-open"),
Step::Click("drawer-open"),
Step::Wait(400),
Step::Shot("drawer"),
Step::DownAt("drawer-body", 10.0, -28.0),
Step::DragTo("drawer-body", 10.0, 172.0),
Step::UpAt("drawer-body", 10.0, 172.0),
Step::Wait(400),
Step::Shot("drawer-pulled"),
Step::Click("floating-open"),
Step::Wait(300),
Step::Shot("floating"),
Step::Click("floating-open"),
Step::DownAt("dock", 300.0, 240.0),
Step::DragTo("dock", 380.0, 60.0),
Step::Wait(200),
Step::Shot("dock-zones"),
Step::UpAt("dock", 380.0, 60.0),
Step::Wait(200),
Step::Shot("dock-top"),
Step::DownAt("dock", 300.0, 20.0),
Step::DragTo("dock", 380.0, 330.0),
Step::UpAt("dock", 380.0, 330.0),
Step::Click("workspace-save"),
Step::DownAt("panes", 380.0, 150.0),
Step::DragTo("panes", 560.0, 150.0),
Step::UpAt("panes", 560.0, 150.0),
Step::Shot("panes-dragged"),
Step::Click("workspace-restore"),
Step::Wait(300),
Step::Shot("panes-restored"),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(basics::stacks(cx))
.child(basics::flex(cx))
.child(basics::grid(cx))
.child(basics::simple_grid(cx))
.child(basics::masonry(cx))
.child(basics::center(cx))
.child(basics::container(cx))
.child(basics::aspect_ratio(cx))
.child(basics::inset(cx))
.child(basics::wrap(cx))
.child(basics::absolute(cx))
.child(basics::z_stack(cx))
.child(scrolling::scroll_area(cx))
.child(scrolling::scrollbar(window, cx))
.child(scrolling::scroll_shadow(window, cx))
.child(scrolling::scroll_to_top(window, cx))
.child(scrolling::sticky_header(window, cx))
.child(regions::split_pane(cx))
.child(regions::resize_handles(cx))
.child(regions::collapsible(window, cx))
.child(regions::accordion(cx))
.child(regions::card(cx))
.child(regions::panel(cx))
.child(regions::section_demo(cx))
.child(regions::fieldset(cx))
.child(regions::frame_demo(cx))
.child(regions::well(cx))
.child(shells::sidebar(window, cx))
.child(shells::drawer(window, cx))
.child(shells::sheet(window, cx))
.child(shells::page(cx))
.child(shells::app_shell(cx))
.child(shells::master_detail(cx))
.child(systems::dock(window, cx))
.child(systems::dock_zones(cx))
.child(systems::floating(window, cx))
.child(systems::panes(window, cx))
.child(systems::workspace(window, cx))
.child(systems::viewport(cx))
.into_any_element()
}
+252
View File
@@ -0,0 +1,252 @@
use ely_gpui_component::{
buttons::{Button, ButtonVariant, IconButton},
layout::{
Accordion, AccordionItem, Card, CardHeader, Collapsible, Fieldset, Frame, Panel, Section,
SplitPane, Well, resize_handle,
},
primitives::IconName,
theme::{ActiveTheme, ControlSize, Radius},
typography::{Caption, Label, Paragraph},
};
use gpui::{App, Axis, IntoElement, ParentElement, Styled, Window, div, px};
use super::basics::tile;
use crate::{
probe::probe,
ui::{code, row, section},
};
fn pane(label: &'static str, cx: &App) -> gpui::Div {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.bg(cx.theme().colors.sunken)
.text_color(cx.theme().colors.fg_subtle)
.child(label)
}
fn frame(cx: &App) -> gpui::Div {
let theme = cx.theme();
div()
.w(px(560.0))
.h(px(220.0))
.overflow_hidden()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
}
pub fn split_pane(cx: &App) -> impl IntoElement + use<> {
section(
"SplitPane",
"Drag a divider. Panes nest across and down.",
cx,
)
.child(probe(
"split",
frame(cx).child(
SplitPane::new("split-outer", Axis::Horizontal, px(96.0))
.sizes(&[0.3, 0.7])
.pane(pane("files", cx))
.pane(
SplitPane::new("split-inner", Axis::Vertical, px(48.0))
.pane(pane("editor", cx))
.pane(pane("terminal", cx)),
),
),
))
.child(code(
"ResizablePanel and ThreeColumnLayout are SplitPane.",
cx,
))
}
pub fn resize_handles(cx: &App) -> impl IntoElement + use<> {
section(
"ResizeHandle",
"A hairline with a wide grab. Hover lights it.",
cx,
)
.child(
row()
.gap_8()
.child(
div()
.h(px(80.0))
.child(resize_handle("handle-x", Axis::Horizontal, cx)),
)
.child(
div()
.w(px(160.0))
.child(resize_handle("handle-y", Axis::Vertical, cx)),
),
)
}
pub fn collapsible(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let open = window.use_keyed_state("collapse-open", cx, |_, _| true);
let is_open = *open.read(cx);
section(
"Collapsible",
"Height animates to the measured content.",
cx,
)
.child(
div()
.w(px(420.0))
.flex()
.flex_col()
.gap_2()
.child(probe(
"collapse-toggle",
Button::new(
"collapse-toggle",
if is_open { "Collapse" } else { "Expand" },
)
.variant(ButtonVariant::Outline)
.trailing_icon(if is_open {
IconName::ChevronUp
} else {
IconName::ChevronDown
})
.on_click(move |_, _, cx| {
open.update(cx, |open, cx| {
*open = !*open;
cx.notify();
})
}),
))
.child(
Collapsible::new("collapse", is_open).child(
Paragraph::new(
"Content keeps its natural height. The frame grows to it, then lets go.",
)
.pt_2(),
),
),
)
}
pub fn accordion(cx: &App) -> impl IntoElement + use<> {
section(
"Accordion / AccordionItem",
"One open at a time. The chevron turns with it.",
cx,
)
.child(probe(
"accordion",
div().w(px(480.0)).child(
Accordion::new("faq")
.first_open()
.item(
AccordionItem::new("What is Ely?")
.child("A component library for gpui, in light and dark."),
)
.item(
AccordionItem::new("Does it animate?")
.child("Where motion explains a change, and nowhere else."),
)
.item(
AccordionItem::new("Can I theme it?")
.child("Every color, size and radius comes from one token set."),
),
),
))
}
pub fn card(cx: &App) -> impl IntoElement + use<> {
section(
"Card / CardHeader / CardBody / CardFooter",
"Flat: a hairline, no shadow.",
cx,
)
.child(
Card::new()
.w(px(380.0))
.header(
CardHeader::new("Storage")
.description("Across all workspaces")
.action(IconButton::new("card-more", IconName::Ellipsis).size(ControlSize::Sm)),
)
.child(Label::new("42.8 GB of 100 GB"))
.child(Caption::new("Media takes the most room."))
.footer(
div()
.flex()
.gap_2()
.child(Button::new("card-cancel", "Later").variant(ButtonVariant::Ghost))
.child(Button::new("card-go", "Manage").primary()),
),
)
}
pub fn panel(cx: &App) -> impl IntoElement + use<> {
section(
"Panel / PanelHeader",
"An app region: a quiet header bar and a body.",
cx,
)
.child(
Panel::new("Outline")
.w(px(320.0))
.h(px(160.0))
.action(IconButton::new("panel-filter", IconName::Filter).size(ControlSize::Sm))
.action(IconButton::new("panel-more", IconName::Ellipsis).size(ControlSize::Sm))
.child(
div()
.p_3()
.flex()
.flex_col()
.gap_2()
.child(tile("fn render", cx))
.child(tile("struct Theme", cx)),
),
)
}
pub fn section_demo(cx: &App) -> impl IntoElement + use<> {
section(
"Section / SectionHeader",
"A page block: title, note, action.",
cx,
)
.child(
Section::new("Members")
.w(px(520.0))
.description("People who can open this workspace.")
.action(Button::new("invite", "Invite").icon(IconName::Plus))
.child(
row()
.child(tile("Ada", cx))
.child(tile("Grace", cx))
.child(tile("Linus", cx)),
),
)
}
pub fn fieldset(cx: &App) -> impl IntoElement + use<> {
section("Group / Fieldset", "Related fields under one name.", cx).child(
Fieldset::new("Shipping address")
.w(px(420.0))
.child(tile("Street", cx))
.child(row().child(tile("City", cx)).child(tile("Postcode", cx))),
)
}
pub fn frame_demo(cx: &App) -> impl IntoElement + use<> {
section("Frame", "A bordered region with no fill.", cx).child(
Frame::new()
.w(px(320.0))
.child(Caption::new("Framed content sits on the page tone.")),
)
}
pub fn well(cx: &App) -> impl IntoElement + use<> {
section("Well", "A recessed region on the sunken tone.", cx).child(
Well::new()
.w(px(320.0))
.child(Caption::new("Wells hold secondary material.")),
)
}
+164
View File
@@ -0,0 +1,164 @@
use ely_gpui_component::{
layout::{ScrollArea, ScrollShadow, ScrollToTop, Scrollbar, StickyHeader},
theme::{ActiveTheme, Radius, TextSize},
};
use gpui::{
App, Axis, FontWeight, InteractiveElement, IntoElement, ParentElement, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, Window, div, px,
};
use crate::{probe::probe, ui::section};
fn lines(count: usize, cx: &App) -> impl Iterator<Item = gpui::Div> + '_ {
let theme = cx.theme();
(1..=count).map(move |ix| {
div()
.px_3()
.py_2()
.border_b_1()
.border_color(theme.colors.border)
.text_size(theme.text_size(TextSize::Sm))
.child(SharedString::from(format!(
"Line {ix:02} · quiet content that scrolls"
)))
})
}
fn well(cx: &App) -> gpui::Div {
let theme = cx.theme();
div()
.relative()
.w(px(320.0))
.h(px(200.0))
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.overflow_hidden()
}
pub fn scroll_area(cx: &App) -> impl IntoElement + use<> {
section(
"ScrollArea",
"Overlay scrollbars appear while scrolling or hovered.",
cx,
)
.child(probe(
"scroll-area",
well(cx).child(
ScrollArea::new("scroll-area")
.size_full()
.children(lines(24, cx)),
),
))
}
fn handle(window: &mut Window, cx: &mut App, key: &'static str) -> ScrollHandle {
window
.use_keyed_state(key, cx, |_, _| ScrollHandle::new())
.read(cx)
.clone()
}
pub fn scrollbar(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let scroll = handle(window, cx, "bar-handle");
section(
"Scrollbar",
"Any ScrollHandle can wear one. Drag the thumb.",
cx,
)
.child(probe(
"scrollbar",
well(cx)
.child(
div()
.id("bar-body")
.size_full()
.overflow_y_scroll()
.track_scroll(&scroll)
.children(lines(30, cx)),
)
.child(Scrollbar::new("bar", &scroll, Axis::Vertical)),
))
}
pub fn scroll_shadow(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let scroll = handle(window, cx, "shadow-handle");
section(
"ScrollShadow",
"A soft edge wherever more content waits.",
cx,
)
.child(
well(cx)
.child(
div()
.id("shadow-body")
.size_full()
.overflow_y_scroll()
.track_scroll(&scroll)
.children(lines(30, cx)),
)
.child(ScrollShadow::new(&scroll)),
)
}
pub fn scroll_to_top(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let scroll = handle(window, cx, "top-handle");
section(
"ScrollToTop",
"Past half a screen, a button eases back up.",
cx,
)
.child(
well(cx)
.child(
div()
.id("top-body")
.size_full()
.overflow_y_scroll()
.track_scroll(&scroll)
.children(lines(40, cx)),
)
.child(ScrollToTop::new("to-top", &scroll)),
)
}
pub fn sticky_header(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let scroll = handle(window, cx, "sticky-handle");
let groups = ["Today", "Yesterday", "Earlier"];
section(
"StickyHeader",
"Each group's header pins until the next pushes it off.",
cx,
)
.child(probe(
"sticky",
well(cx)
.child(
div()
.id("sticky-body")
.size_full()
.overflow_y_scroll()
.track_scroll(&scroll)
.children(groups.iter().enumerate().map(|(ix, group)| {
let title = SharedString::from(*group);
StickyHeader::new(("sticky", ix), &scroll, move |_, cx| {
let theme = cx.theme();
div()
.px_3()
.py_1p5()
.bg(theme.colors.sunken)
.border_b_1()
.border_color(theme.colors.border)
.text_size(theme.text_size(TextSize::Xs))
.font_weight(FontWeight::SEMIBOLD)
.text_color(theme.colors.fg_muted)
.child(title.clone())
.into_any_element()
})
.children(lines(8, cx))
})),
)
.child(Scrollbar::new("sticky-bar", &scroll, Axis::Vertical)),
))
}
+233
View File
@@ -0,0 +1,233 @@
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
layout::{AppShell, Drawer, MasterDetail, Page, Sheet, Sidebar, h_stack},
primitives::{Icon, IconName, Place},
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
typography::{Caption, Paragraph},
};
use gpui::{App, Entity, IntoElement, ParentElement, Styled, Window, div, prelude::*, px};
use super::basics::tile;
use crate::{
probe::probe,
ui::{row, section},
};
pub fn frame(width: f32, height: f32, cx: &App) -> gpui::Div {
let theme = cx.theme();
div()
.w(px(width))
.h(px(height))
.overflow_hidden()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
}
fn toggle(window: &mut Window, cx: &mut App, key: &'static str) -> (Entity<bool>, bool) {
let state = window.use_keyed_state(key, cx, |_, _| false);
let value = *state.read(cx);
(state, value)
}
fn flip(state: &Entity<bool>, cx: &mut App) {
state.update(cx, |value, cx| {
*value = !*value;
cx.notify();
})
}
fn nav(collapsed: bool, cx: &App) -> impl IntoElement + use<> {
let items = [
(IconName::House, "Home"),
(IconName::Inbox, "Inbox"),
(IconName::Settings, "Settings"),
];
let theme = cx.theme();
div()
.flex()
.flex_col()
.gap_0p5()
.p_2()
.children(items.map(|(icon, label)| {
h_stack()
.gap_3()
.h(theme.control_height(ControlSize::Md))
.px_2()
.rounded(theme.radius(Radius::Md))
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child(
Icon::new(icon)
.size(IconSize::Sm)
.color(theme.colors.fg_muted),
)
.when(!collapsed, |item| item.child(label))
}))
}
pub fn sidebar(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (state, collapsed) = toggle(window, cx, "sidebar-collapsed");
section("Sidebar", "Folds to an icon rail; the width eases.", cx).child(
frame(560.0, 200.0, cx)
.flex()
.child(Sidebar::new("demo-sidebar", collapsed).child(nav(collapsed, cx)))
.child(
div().flex_1().p_4().flex().items_start().child(probe(
"sidebar-toggle",
Button::new(
"sidebar-toggle",
if collapsed { "Expand" } else { "Collapse" },
)
.variant(ButtonVariant::Outline)
.icon(IconName::PanelLeft)
.on_click(move |_, _, cx| flip(&state, cx)),
)),
),
)
}
pub fn drawer(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (state, open) = toggle(window, cx, "drawer-open");
let close = state.clone();
section(
"Drawer",
"Rises from the bottom. Pull the handle down to dismiss.",
cx,
)
.child(
row()
.child(probe(
"drawer-open",
Button::new("drawer-open", "Open drawer")
.on_click(move |_, _, cx| flip(&state, cx)),
))
.when(open, |row| {
row.child(
Drawer::new("demo-drawer", move |_, cx| flip(&close, cx))
.child(probe(
"drawer-body",
Paragraph::new(
"Drawers hold short tasks close to where the hand already is.",
),
))
.child(Caption::new("Drag the handle down, or click the scrim.")),
)
}),
)
}
pub fn sheet(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (state, open) = toggle(window, cx, "sheet-open");
let close = state.clone();
section("Sheet", "Slides in from an edge over a scrim.", cx).child(
row()
.child(probe(
"sheet-open",
Button::new("sheet-open", "Open sheet").on_click(move |_, _, cx| flip(&state, cx)),
))
.when(open, |row| {
row.child(
Sheet::new("demo-sheet", Place::Right, "Filters", move |_, cx| {
flip(&close, cx)
})
.child(Paragraph::new(
"Sheets keep context visible while a side task runs.",
))
.child(tile("Status: open", cx))
.child(tile("Owner: anyone", cx)),
)
}),
)
}
pub fn page(cx: &App) -> impl IntoElement + use<> {
section(
"Page / PageHeader / PageContent",
"Title, note and actions over the body.",
cx,
)
.child(
frame(640.0, 240.0, cx).child(
Page::new("Projects")
.subtitle("Everything your team is shipping.")
.action(
Button::new("page-new", "New project")
.primary()
.icon(IconName::Plus),
)
.child(
row()
.child(tile("Atlas", cx))
.child(tile("Beacon", cx))
.child(tile("Compass", cx)),
),
),
)
}
pub fn app_shell(cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
let bar = |label: &'static str, cx: &App| {
div()
.h(cx.theme().control_height(ControlSize::Md))
.px_3()
.flex()
.items_center()
.text_size(cx.theme().text_size(TextSize::Xs))
.text_color(cx.theme().colors.fg_subtle)
.child(label)
};
section("AppShell", "Title bar, sidebar, content, status bar.", cx).child(
frame(640.0, 260.0, cx).child(
AppShell::new()
.title_bar(
bar("Ely — Projects", cx)
.border_b_1()
.border_color(theme.colors.border),
)
.sidebar(Sidebar::new("shell-sidebar", false).child(nav(false, cx)))
.status_bar(
bar("main · 3 changes · UTF-8", cx)
.border_t_1()
.border_color(theme.colors.border),
)
.child(div().p_4().child(Paragraph::new(
"The shell holds the frame. Your content fills the middle.",
))),
),
)
}
pub fn master_detail(cx: &App) -> impl IntoElement + use<> {
let items = [
"Quarterly review",
"Design sync",
"Launch plan",
"Hiring loop",
];
section(
"MasterDetail",
"A list beside the chosen item. The divider drags.",
cx,
)
.child(
frame(640.0, 220.0, cx).child(MasterDetail::new(
"master-detail",
div()
.p_2()
.flex()
.flex_col()
.gap_1()
.children(items.map(|item| tile(item, cx))),
div()
.p_5()
.flex()
.flex_col()
.gap_2()
.child(Paragraph::new("Quarterly review"))
.child(Caption::new("Numbers, notes and the next three moves.")),
px(160.0),
)),
)
}
+271
View File
@@ -0,0 +1,271 @@
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
layout::{Dock, DockPanel, DockSide, FloatingPanel, PaneGroup, Transform, Viewport, Workspace},
primitives::IconName,
theme::{ActiveTheme, Radius, TextSize},
typography::{Caption, Paragraph},
};
use gpui::{
AnyElement, App, Axis, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div,
point, prelude::*, px,
};
use super::{basics::tile, shells::frame};
use crate::{
probe::probe,
ui::{code, row, section},
};
fn demo_dock(cx: &mut App) -> Dock {
let panel = |id: &str, title: &str, icon| DockPanel {
id: SharedString::from(id.to_string()),
title: SharedString::from(title.to_string()),
icon,
};
let mut dock = Dock::new(
|_, cx| {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.text_color(cx.theme().colors.fg_subtle)
.child("Center. Drag a tab onto a zone.")
.into_any_element()
},
|id, _, cx| {
div()
.p_3()
.text_size(cx.theme().text_size(TextSize::Sm))
.text_color(cx.theme().colors.fg_muted)
.child(SharedString::from(format!("{id} panel")))
.into_any_element()
},
cx,
);
dock.add(panel("files", "Files", IconName::Folder), DockSide::Left);
dock.add(
panel("outline", "Outline", IconName::Layers),
DockSide::Left,
);
dock.add(panel("search", "Search", IconName::Search), DockSide::Right);
dock.add(
panel("terminal", "Terminal", IconName::Terminal),
DockSide::Bottom,
);
dock
}
fn demo_panes(cx: &mut Context<PaneGroup>) -> PaneGroup {
let mut group = PaneGroup::new(|id, _, cx| {
div()
.size_full()
.p_3()
.text_color(cx.theme().colors.fg_subtle)
.child(SharedString::from(format!("buffer {id}")))
.into_any_element()
});
group.split(1, Axis::Horizontal, cx);
group
}
/// The dock and pane group shared by the demos and the workspace section.
fn systems(window: &mut Window, cx: &mut App) -> (Entity<Dock>, Entity<PaneGroup>) {
let dock = window.use_keyed_state("demo-dock", cx, |_, cx| demo_dock(cx));
let panes = window.use_keyed_state("demo-panes", cx, |_, cx| demo_panes(cx));
(dock, panes)
}
pub fn dock(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (dock, _) = systems(window, cx);
section(
"Dock / DockPanel",
"Panels dock on any edge, tab together, and float.",
cx,
)
.child(probe("dock", frame(760.0, 380.0, cx).child(dock)))
}
pub fn dock_zones(cx: &App) -> impl IntoElement + use<> {
section(
"DockZone / DropIndicator",
"While a tab drags, the drop zones appear and the one below lights.",
cx,
)
.child(Caption::new(
"Drag a tab across the dock above to see them.",
))
}
pub fn floating(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let open = window.use_keyed_state("floating-open", cx, |_, _| false);
let shown = *open.read(cx);
section(
"FloatingPanel",
"Drag the title bar. It stays inside the window.",
cx,
)
.child(
row()
.child(probe(
"floating-open",
Button::new(
"floating-open",
if shown { "Hide panel" } else { "Show panel" },
)
.on_click(move |_, _, cx| {
open.update(cx, |open, cx| {
*open = !*open;
cx.notify();
})
}),
))
.when(shown, |row| {
row.child(
FloatingPanel::new("demo-float", "Inspector", point(px(720.0), px(160.0)))
.w(px(240.0))
.child(Caption::new("Floating panels sit above the page."))
.child(tile("Opacity 100%", cx)),
)
}),
)
}
pub fn panes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (_, panes) = systems(window, cx);
section(
"Pane / PaneGroup",
"Split right or down from any pane; close all but one.",
cx,
)
.child(probe("panes", frame(760.0, 300.0, cx).child(panes)))
}
pub fn workspace(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (dock, panes) = systems(window, cx);
let saved = window.use_keyed_state("workspace-json", cx, |_, _| None::<String>);
let report = window.use_keyed_state("workspace-report", cx, |_, _| SharedString::from(""));
let preview: Option<String> = saved.read(cx).clone();
let note = report.read(cx).clone();
let (save_dock, save_panes, save_to) = (dock.clone(), panes.clone(), saved.clone());
let (load_from, load_note) = (saved, report.clone());
section(
"Workspace",
"Layout to versioned JSON and back, all or nothing.",
cx,
)
.child(
row()
.child(probe(
"workspace-save",
Button::new("workspace-save", "Save layout")
.icon(IconName::Save)
.on_click(move |_, _, cx| {
let json =
Workspace::capture(save_dock.read(cx), save_panes.read(cx)).to_json();
save_to.update(cx, |saved, cx| {
*saved = Some(json);
cx.notify();
})
}),
))
.child(probe(
"workspace-restore",
Button::new("workspace-restore", "Restore")
.variant(ButtonVariant::Ghost)
.icon(IconName::RotateCcw)
.disabled(preview.is_none())
.on_click(move |_, _, cx| {
let json = load_from
.read(cx)
.clone()
.expect("restore is disabled until a layout is saved");
let outcome = Workspace::from_json(&json)
.and_then(|layout| layout.apply(&dock, &panes, cx));
let text = match outcome {
Ok(()) => "Restored.".to_string(),
Err(error) => format!("Refused: {error:#}"),
};
load_note.update(cx, |note, cx| {
*note = SharedString::from(text);
cx.notify();
})
}),
))
.child(Caption::new(note)),
)
.when_some(preview, |block, json| {
let excerpt: String = json.lines().take(12).collect::<Vec<_>>().join("\n");
block.child(code(excerpt, cx))
})
}
fn blob(
at: gpui::Point<gpui::Pixels>,
size: f32,
transform: Transform,
label: &'static str,
cx: &App,
) -> AnyElement {
let theme = cx.theme();
let local = transform.apply(at);
div()
.absolute()
.left(local.x)
.top(local.y)
.w(px(size) * transform.scale)
.h(px(size * 0.6) * transform.scale)
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border_strong)
.bg(theme.colors.surface)
.flex()
.items_center()
.justify_center()
.text_color(theme.colors.fg_muted)
.child(label)
.into_any_element()
}
pub fn viewport(cx: &App) -> impl IntoElement + use<> {
section(
"Viewport",
"Drag to pan. Hold ⌘ or Ctrl and scroll to zoom at the pointer.",
cx,
)
.child(probe(
"viewport",
frame(640.0, 260.0, cx)
.bg(cx.theme().colors.sunken)
.child(Viewport::new("demo-viewport", |transform, _, cx| {
div()
.size_full()
.child(blob(
point(px(60.0), px(50.0)),
140.0,
transform,
"Idea",
cx,
))
.child(blob(
point(px(260.0), px(110.0)),
140.0,
transform,
"Draft",
cx,
))
.child(blob(
point(px(460.0), px(40.0)),
120.0,
transform,
"Ship",
cx,
))
.into_any_element()
})),
))
.child(
Paragraph::new("Content places itself with the transform; zoom stays between 0.1 and 8.")
.max_w(px(560.0)),
)
}
+2
View File
@@ -1,4 +1,5 @@
mod buttons;
mod layout;
mod primitives;
mod theme;
mod typography;
@@ -20,6 +21,7 @@ pub struct Page {
pub const ALL: &[Page] = &[
primitives::PAGE,
typography::PAGE,
layout::PAGE,
buttons::PAGE,
theme::PAGE,
];
+33
View File
@@ -14,17 +14,24 @@ pub enum Step {
Click(&'static str),
/// Clicks just inside the probe's right edge.
ClickEnd(&'static str),
/// Presses at an offset from the probe's top-left corner.
DownAt(&'static str, f32, f32),
/// Drags from the last point to an offset, in small steps.
DragTo(&'static str, f32, f32),
UpAt(&'static str, f32, f32),
Key(&'static str),
Wait(u64),
Shot(&'static str),
}
const FRAME: Duration = Duration::from_millis(120);
const DRAG_STEPS: u32 = 10;
#[derive(Clone, Copy)]
enum Mouse {
Move,
Down,
Drag,
Up,
}
@@ -39,6 +46,7 @@ pub async fn play(
path: impl Fn(&str) -> PathBuf,
cx: &mut AsyncApp,
) -> Result<()> {
let mut last = point(px(0.0), px(0.0));
for step in steps {
match *step {
Step::Rest => park(window, cx)?,
@@ -68,6 +76,30 @@ pub async fn play(
send(window, Mouse::Down, at, cx)?;
send(window, Mouse::Up, at, cx)?;
}
Step::DownAt(key, x, y) => {
last = target_bounds(window, key, cx).await?.origin + point(px(x), px(y));
send(window, Mouse::Move, last, cx)?;
send(window, Mouse::Down, last, cx)?;
}
Step::DragTo(key, x, y) => {
let goal = target_bounds(window, key, cx).await?.origin + point(px(x), px(y));
for step in 1..=DRAG_STEPS {
let t = step as f32 / DRAG_STEPS as f32;
let at = point(
last.x + (goal.x - last.x) * t,
last.y + (goal.y - last.y) * t,
);
send(window, Mouse::Drag, at, cx)?;
cx.background_executor()
.timer(Duration::from_millis(16))
.await;
}
last = goal;
}
Step::UpAt(key, x, y) => {
last = target_bounds(window, key, cx).await?.origin + point(px(x), px(y));
send(window, Mouse::Up, last, cx)?;
}
Step::Key(stroke) => {
let stroke =
Keystroke::parse(stroke).with_context(|| format!("bad keystroke {stroke}"))?;
@@ -113,6 +145,7 @@ fn post(kind: Mouse, x: f64, y: f64) -> Result<()> {
let kind = match kind {
Mouse::Move => NSEventType::NSMouseMoved,
Mouse::Down => NSEventType::NSLeftMouseDown,
Mouse::Drag => NSEventType::NSLeftMouseDragged,
Mouse::Up => NSEventType::NSLeftMouseUp,
};
unsafe {