T03 layout: stacks, grids, surfaces, scroll, splits, sheets, dock, panes, workspace, viewport
This commit is contained in:
37 files changed
+5283
-50
No files matched your search
@@ -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")),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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.")),
|
||||
)
|
||||
}
|
||||
@@ -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)),
|
||||
))
|
||||
}
|
||||
@@ -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),
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user