T01 primitives: image, divider, backdrop, focus, pressable, measure, tooltip
Ely-built primitives plus gallery demos for every chapter-one entry, including gpui natives. The gallery now scripts real input by posting NSEvents to its own queue and shoots each interaction in both modes. Codex gpt-6-astra (max): three rounds; the last finding, a two-line doc comment, was fixed after the three-review cap.
This commit is contained in:
38 files changed
+2165
-157
No files matched your search
@@ -0,0 +1,80 @@
|
||||
mod native;
|
||||
mod parts;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 1,
|
||||
slug: "primitives",
|
||||
title: "Primitives",
|
||||
summary: "The smallest parts. Most are gpui itself; Ely adds the rest.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[
|
||||
Step::Rest,
|
||||
Step::Hover("tip-plain"),
|
||||
Step::Wait(700),
|
||||
Step::Shot("tooltip"),
|
||||
Step::Hover("tip-meta"),
|
||||
Step::Wait(700),
|
||||
Step::Shot("tooltip-meta"),
|
||||
Step::Hover("tip-rich"),
|
||||
Step::Wait(700),
|
||||
Step::Shot("tooltip-rich"),
|
||||
Step::Hover("tip-follow"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("tooltip-follow"),
|
||||
Step::Rest,
|
||||
Step::Down("prim-press"),
|
||||
Step::Wait(700),
|
||||
Step::Shot("pressed"),
|
||||
Step::Up("prim-press"),
|
||||
Step::Rest,
|
||||
Step::Click("portal-toggle"),
|
||||
Step::Shot("portal"),
|
||||
Step::Click("portal-toggle"),
|
||||
Step::Click("backdrop-open"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("backdrop"),
|
||||
Step::Click("backdrop-open"),
|
||||
Step::Click("trap-c"),
|
||||
Step::Key("tab"),
|
||||
Step::Shot("trap-wrap"),
|
||||
Step::Rest,
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(native::boxes(cx))
|
||||
.child(native::text(cx))
|
||||
.child(parts::icon(cx))
|
||||
.child(parts::icon_set(cx))
|
||||
.child(native::svg_demo(cx))
|
||||
.child(parts::image(cx))
|
||||
.child(native::canvas_demo(cx))
|
||||
.child(parts::divider(cx))
|
||||
.child(native::spacer(cx))
|
||||
.child(native::portal(window, cx))
|
||||
.child(parts::backdrop(window, cx))
|
||||
.child(native::slot(cx))
|
||||
.child(native::show(window, cx))
|
||||
.child(native::each(cx))
|
||||
.child(native::fragment(cx))
|
||||
.child(parts::visually_hidden(cx))
|
||||
.child(parts::focus_ring(cx))
|
||||
.child(parts::focus_trap(window, cx))
|
||||
.child(native::click_outside(window, cx))
|
||||
.child(native::hover_area(window, cx))
|
||||
.child(parts::pressable(window, cx))
|
||||
.child(native::keyboard(window, cx))
|
||||
.child(parts::measure(window, cx))
|
||||
.child(parts::intersection(window, cx))
|
||||
.child(native::clipboard(window, cx))
|
||||
.child(parts::tooltip(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
primitives::{FocusRing, Icon, IconName},
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, ClipboardItem, FontWeight, HighlightStyle, InteractiveElement, IntoElement,
|
||||
ParentElement, PathBuilder, SharedString, StatefulInteractiveElement, Styled, StyledText,
|
||||
Transformation, Window, anchored, canvas, deferred, div, point, prelude::*, px, radians, svg,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{code, row, section},
|
||||
};
|
||||
|
||||
pub fn boxes(cx: &App) -> impl IntoElement + use<> {
|
||||
let colors = &cx.theme().colors;
|
||||
section(
|
||||
"Box / View",
|
||||
"gpui's div() is the box. Ely adds nothing.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.size_16()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.surface),
|
||||
)
|
||||
.child(div().w_32().h_16().rounded_lg().bg(colors.sunken))
|
||||
.child(code("div()", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn text(cx: &App) -> impl IntoElement + use<> {
|
||||
let bold = HighlightStyle {
|
||||
font_weight: Some(FontWeight::SEMIBOLD),
|
||||
..Default::default()
|
||||
};
|
||||
section("Text", "A string is an element. StyledText adds runs.", cx).child(
|
||||
row()
|
||||
.gap_6()
|
||||
.child("Plain text, as a child.")
|
||||
.child(StyledText::new("One accent, used once.").with_highlights([(4..10, bold)]))
|
||||
.child(code("StyledText::new(…)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn svg_demo(cx: &App) -> impl IntoElement + use<> {
|
||||
let color = cx.theme().colors.fg_muted;
|
||||
section(
|
||||
"Svg",
|
||||
"gpui's svg(). Any path from the asset source, tinted.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_6()
|
||||
.child(
|
||||
svg()
|
||||
.path(IconName::Compass.path())
|
||||
.size_12()
|
||||
.text_color(color),
|
||||
)
|
||||
.child(
|
||||
svg()
|
||||
.path(IconName::Compass.path())
|
||||
.size_12()
|
||||
.text_color(color)
|
||||
.with_transformation(Transformation::rotate(radians(0.6))),
|
||||
)
|
||||
.child(code("svg().path(…)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn canvas_demo(cx: &App) -> impl IntoElement + use<> {
|
||||
let color = cx.theme().colors.focus;
|
||||
section("Canvas", "gpui's canvas(). Paint with paths.", cx).child(
|
||||
row()
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(320.0)).h(px(80.0)).child(
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |bounds, _, window, _| {
|
||||
let mut wave = PathBuilder::stroke(px(1.5));
|
||||
for step in 0..=96 {
|
||||
let t = step as f32 / 96.0;
|
||||
let x = bounds.origin.x + bounds.size.width * t;
|
||||
let lift = 0.5 - 0.38 * (t * TAU * 1.5).sin() * (1.0 - t * 0.5);
|
||||
let y = bounds.origin.y + bounds.size.height * lift;
|
||||
if step == 0 {
|
||||
wave.move_to(point(x, y));
|
||||
} else {
|
||||
wave.line_to(point(x, y));
|
||||
}
|
||||
}
|
||||
window.paint_path(wave.build().expect("wave path"), color);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
),
|
||||
)
|
||||
.child(code("canvas(prepaint, paint)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn spacer(cx: &App) -> impl IntoElement + use<> {
|
||||
let colors = &cx.theme().colors;
|
||||
section(
|
||||
"Spacer",
|
||||
"flex_1() takes the slack. No component needed.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.w(px(420.0))
|
||||
.h_10()
|
||||
.px_3()
|
||||
.rounded(cx.theme().radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.child(Icon::new(IconName::Menu).color(colors.fg_muted))
|
||||
.child(div().flex_1())
|
||||
.child(code("div().flex_1()", cx))
|
||||
.child(div().flex_1())
|
||||
.child(Icon::new(IconName::Settings).color(colors.fg_muted)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn portal(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let open = window.use_keyed_state("prim-portal", cx, |_, _| false);
|
||||
let shown = *open.read(cx);
|
||||
let theme = cx.theme();
|
||||
let note = div()
|
||||
.w(px(260.0))
|
||||
.p_3()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.bg(theme.colors.overlay)
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.shadow(theme.elevation(ely_gpui_component::theme::Elevation::Floating))
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child("Painted last, above the clipped box.");
|
||||
section(
|
||||
"Portal",
|
||||
"deferred(anchored()) escapes clipping and paints on top.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.w(px(220.0))
|
||||
.h_16()
|
||||
.p_3()
|
||||
.overflow_hidden()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.child(probe(
|
||||
"portal-toggle",
|
||||
Button::new(
|
||||
"portal-toggle",
|
||||
if shown { "Hide note" } else { "Show note" },
|
||||
)
|
||||
.on_click(move |_, _, cx| {
|
||||
open.update(cx, |open, cx| {
|
||||
*open = !*open;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
))
|
||||
.when(shown, |clip| {
|
||||
clip.child(deferred(
|
||||
anchored().offset(point(px(0.0), px(8.0))).child(note),
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(code("deferred(anchored())", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn framed(title: &'static str, body: impl IntoElement, cx: &App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.p_3()
|
||||
.w(px(180.0))
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.child(
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(theme.colors.fg_subtle)
|
||||
.child(title),
|
||||
)
|
||||
.child(body)
|
||||
}
|
||||
|
||||
pub fn slot(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"Slot",
|
||||
"Take impl IntoElement. The caller fills the hole.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_4()
|
||||
.child(framed("slot: icon", Icon::new(IconName::Sparkles), cx))
|
||||
.child(framed(
|
||||
"slot: button",
|
||||
Button::new("slot-button", "Save").primary(),
|
||||
cx,
|
||||
))
|
||||
.child(code("fn framed(body: impl IntoElement)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn show(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let on = window.use_keyed_state("prim-show", cx, |_, _| true);
|
||||
let shown = *on.read(cx);
|
||||
let muted = cx.theme().colors.fg_muted;
|
||||
section(
|
||||
"Show / When",
|
||||
".when() and .when_some() branch inside a builder.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_4()
|
||||
.child(
|
||||
Button::new("show-toggle", if shown { "Hide" } else { "Show" }).on_click(
|
||||
move |_, _, cx| {
|
||||
on.update(cx, |on, cx| {
|
||||
*on = !*on;
|
||||
cx.notify();
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.when(shown, |row| {
|
||||
row.child(div().text_color(muted).child("Here while true."))
|
||||
})
|
||||
.child(code(".when(cond, |el| …)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn each(cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
let steps = ["Draft", "Review", "Ship"];
|
||||
section("For / Each", ".children() takes any iterator.", cx).child(
|
||||
row()
|
||||
.gap_6()
|
||||
.children(steps.iter().enumerate().map(|(ix, step)| {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(theme.colors.fg_subtle)
|
||||
.child(SharedString::from(format!("{:02}", ix + 1))),
|
||||
)
|
||||
.child(*step)
|
||||
}))
|
||||
.child(code(".children(iter)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn pair(cx: &App) -> [AnyElement; 2] {
|
||||
let muted = cx.theme().colors.fg_muted;
|
||||
[
|
||||
Icon::new(IconName::Layers).color(muted).into_any_element(),
|
||||
div()
|
||||
.text_color(muted)
|
||||
.child("two siblings, no wrapper")
|
||||
.into_any_element(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn fragment(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"Fragment",
|
||||
"Return several elements; the parent adopts them.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_2()
|
||||
.children(pair(cx))
|
||||
.child(div().w_6())
|
||||
.child(code(".children([a, b])", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn click_outside(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let open = window.use_keyed_state("prim-outside", cx, |_, _| false);
|
||||
let shown = *open.read(cx);
|
||||
let theme = cx.theme();
|
||||
let close = open.clone();
|
||||
section(
|
||||
"ClickOutside",
|
||||
"on_mouse_down_out closes what the pointer left.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_4()
|
||||
.child(
|
||||
Button::new("outside-open", "Open panel")
|
||||
.disabled(shown)
|
||||
.on_click(move |_, _, cx| {
|
||||
open.update(cx, |open, cx| {
|
||||
*open = true;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when(shown, |row| {
|
||||
row.child(
|
||||
div()
|
||||
.id("outside-panel")
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(theme.colors.sunken)
|
||||
.child("Click anywhere else.")
|
||||
.on_mouse_down_out(move |_, _, cx| {
|
||||
close.update(cx, |open, cx| {
|
||||
*open = false;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(code(".on_mouse_down_out(…)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn hover_area(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let inside = window.use_keyed_state("prim-hover", cx, |_, _| false);
|
||||
let hovered = *inside.read(cx);
|
||||
let theme = cx.theme();
|
||||
section(
|
||||
"HoverArea",
|
||||
"on_hover reports enter and leave. .hover() restyles.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.id("hover-area")
|
||||
.w(px(220.0))
|
||||
.h_16()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.hover(|style| style.bg(theme.colors.hover))
|
||||
.child(if hovered {
|
||||
"Pointer inside"
|
||||
} else {
|
||||
"Pointer outside"
|
||||
})
|
||||
.on_hover(move |hovered, _, cx| {
|
||||
inside.update(cx, |inside, cx| {
|
||||
*inside = *hovered;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(code(".on_hover(…)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn keyboard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let focus = window.use_keyed_state("prim-keys-focus", cx, |_, cx| cx.focus_handle());
|
||||
let last = window.use_keyed_state("prim-keys", cx, |_, _| SharedString::from("—"));
|
||||
let handle = focus.read(cx).clone();
|
||||
let shown = last.read(cx).clone();
|
||||
let theme = cx.theme();
|
||||
section(
|
||||
"KeyboardHandler",
|
||||
"on_key_down on a focused element. Click, then type.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.id("keys")
|
||||
.track_focus(&handle)
|
||||
.w(px(220.0))
|
||||
.h_16()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.font_family(theme.mono_family.clone())
|
||||
.focus_ring(cx)
|
||||
.child(shown)
|
||||
.on_key_down(move |event, _, cx| {
|
||||
let stroke = SharedString::from(event.keystroke.to_string());
|
||||
last.update(cx, |last, cx| {
|
||||
*last = stroke;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(code(".on_key_down(…)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clipboard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let pasted = window.use_keyed_state("prim-clip", cx, |_, _| None::<SharedString>);
|
||||
let shown = pasted.read(cx).clone();
|
||||
let muted = cx.theme().colors.fg_muted;
|
||||
section(
|
||||
"Clipboard",
|
||||
"write_to_clipboard and read_from_clipboard on App.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("clip-copy", "Copy “Ely”")
|
||||
.icon(IconName::Copy)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string("Ely".into()))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("clip-paste", "Paste")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(IconName::Clipboard)
|
||||
.on_click(move |_, _, cx| {
|
||||
let text = cx.read_from_clipboard().and_then(|item| item.text());
|
||||
pasted.update(cx, |pasted, cx| {
|
||||
*pasted = Some(match text {
|
||||
Some(text) => text.into(),
|
||||
None => "The clipboard holds no text.".into(),
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when_some(shown, |row, text| {
|
||||
row.child(div().text_color(muted).child(text))
|
||||
})
|
||||
.child(code("cx.write_to_clipboard(…)", cx)),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, IconButton},
|
||||
primitives::{
|
||||
Backdrop, Divider, FocusRing, FocusScope, Icon, IconName, Image, IntersectionObserver,
|
||||
Measure, Pressable, Tooltip, TooltipTrigger,
|
||||
},
|
||||
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
||||
};
|
||||
use gpui::{
|
||||
App, FontWeight, InteractiveElement, IntoElement, ParentElement, Pixels, SharedString, Size,
|
||||
StatefulInteractiveElement, Styled, Window, div, prelude::*, px,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{blocked, code, row, section, specimen, specimens},
|
||||
};
|
||||
|
||||
const ATRIUM: &str = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/atrium.jpg"
|
||||
);
|
||||
|
||||
fn counter(window: &mut Window, cx: &mut App, key: &'static str) -> gpui::Entity<u32> {
|
||||
window.use_keyed_state(key, cx, |_, _| 0u32)
|
||||
}
|
||||
|
||||
pub fn icon(cx: &App) -> impl IntoElement + use<> {
|
||||
let colors = &cx.theme().colors;
|
||||
let sizes = [
|
||||
(IconSize::Xs, "12"),
|
||||
(IconSize::Sm, "14"),
|
||||
(IconSize::Md, "16"),
|
||||
(IconSize::Lg, "20"),
|
||||
(IconSize::Xl, "24"),
|
||||
(IconSize::Xxl, "32"),
|
||||
];
|
||||
let tones = [
|
||||
(colors.fg, "fg"),
|
||||
(colors.fg_muted, "muted"),
|
||||
(colors.focus, "focus"),
|
||||
(colors.success, "success"),
|
||||
(colors.warning, "warning"),
|
||||
(colors.danger, "danger"),
|
||||
];
|
||||
section(
|
||||
"Icon",
|
||||
"Lucide, drawn as a mask. Color comes from the theme.",
|
||||
cx,
|
||||
)
|
||||
.child(specimens().children(
|
||||
sizes.map(|(size, name)| specimen(name, Icon::new(IconName::Sparkles).size(size), cx)),
|
||||
))
|
||||
.child(
|
||||
specimens().children(tones.map(|(color, name)| {
|
||||
specimen(name, Icon::new(IconName::CircleCheck).color(color), cx)
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn icon_set(cx: &App) -> impl IntoElement + use<> {
|
||||
let colors = &cx.theme().colors;
|
||||
section(
|
||||
"IconSet / IconRegistry",
|
||||
"IconName lists every bundled icon. Hover for its name.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.children(IconName::ALL.iter().map(|&name| {
|
||||
div()
|
||||
.id(name.name())
|
||||
.size_9()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.hover(|style| style.bg(colors.hover))
|
||||
.tooltip(Tooltip::text(name.name()))
|
||||
.child(Icon::new(name).color(colors.fg_muted))
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn tile(image: Image, cx: &App) -> Image {
|
||||
image
|
||||
.w(px(198.0))
|
||||
.h(px(132.0))
|
||||
.rounded(cx.theme().radius(Radius::Lg))
|
||||
}
|
||||
|
||||
pub fn image(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"Image",
|
||||
"Loading pulses. Failure and emptiness show a quiet mark.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.child(specimen(
|
||||
"loaded",
|
||||
tile(Image::new("img-atrium", Path::new(ATRIUM)), cx),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"failed",
|
||||
tile(Image::new("img-missing", Path::new("/missing/ely.png")), cx),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"empty",
|
||||
tile(Image::placeholder("img-empty"), cx),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn divider(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"Divider / Separator",
|
||||
"One hairline. Across, down, or holding a word.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.w(px(420.0))
|
||||
.child(Divider::horizontal())
|
||||
.child(Divider::horizontal().label("or"))
|
||||
.child(
|
||||
row()
|
||||
.h_6()
|
||||
.child("Left")
|
||||
.child(Divider::vertical())
|
||||
.child("Right"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn backdrop(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let open = window.use_keyed_state("prim-backdrop", cx, |_, _| false);
|
||||
let shown = *open.read(cx);
|
||||
let theme = cx.theme();
|
||||
let (show, dismiss, close) = (open.clone(), open.clone(), open);
|
||||
let set = |state: &gpui::Entity<bool>, value: bool, cx: &mut App| {
|
||||
state.update(cx, |open, cx| {
|
||||
*open = value;
|
||||
cx.notify();
|
||||
})
|
||||
};
|
||||
let card = div()
|
||||
.w(px(320.0))
|
||||
.p_5()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.rounded(theme.radius(Radius::Xl))
|
||||
.bg(theme.colors.overlay)
|
||||
.shadow(theme.elevation(ely_gpui_component::theme::Elevation::Modal))
|
||||
.child(div().font_weight(FontWeight::SEMIBOLD).child("Backdrop"))
|
||||
.child(
|
||||
div()
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child("Click outside to close."),
|
||||
)
|
||||
.child(div().flex().justify_end().child(
|
||||
Button::new("backdrop-close", "Close").on_click(move |_, _, cx| set(&close, false, cx)),
|
||||
));
|
||||
section(
|
||||
"Overlay / Backdrop",
|
||||
"A scrim that fades in, blocks the page, and hears outside clicks.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.child(probe(
|
||||
"backdrop-open",
|
||||
Button::new("backdrop-open", "Show backdrop")
|
||||
.on_click(move |_, _, cx| set(&show, true, cx)),
|
||||
))
|
||||
.when(shown, |row| {
|
||||
row.child(
|
||||
Backdrop::new("prim-backdrop-layer")
|
||||
.on_dismiss(move |_, cx| set(&dismiss, false, cx))
|
||||
.child(card),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn visually_hidden(cx: &App) -> impl IntoElement + use<> {
|
||||
section("VisuallyHidden", "Text for screen readers only.", cx).child(blocked(
|
||||
"Blocked: gpui 0.2.2 has no accessibility tree to speak to.",
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn chip(id: &'static str, label: &'static str, cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.id(id)
|
||||
.tab_index(0)
|
||||
.px_3()
|
||||
.py_1p5()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.focus_ring(cx)
|
||||
.child(label)
|
||||
}
|
||||
|
||||
pub fn focus_ring(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"FocusRing",
|
||||
"Any focusable element. Press Tab to walk these.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.child(chip("ring-a", "First", cx))
|
||||
.child(chip("ring-b", "Second", cx))
|
||||
.child(chip("ring-c", "Third", cx))
|
||||
.child(code(".focus_ring(cx)", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn focus_trap(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let handle = window
|
||||
.use_keyed_state("prim-trap", cx, |_, cx| cx.focus_handle())
|
||||
.read(cx)
|
||||
.clone();
|
||||
let theme = cx.theme();
|
||||
section(
|
||||
"FocusScope / FocusTrap",
|
||||
"Tab into the box. Tab then circles inside it.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
FocusScope::new(&handle).trap().flex().child(
|
||||
row()
|
||||
.p_3()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border_strong)
|
||||
.child(chip("trap-a", "One", cx))
|
||||
.child(chip("trap-b", "Two", cx))
|
||||
.child(probe("trap-c", chip("trap-c", "Three", cx))),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pressable(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let taps = counter(window, cx, "prim-taps");
|
||||
let holds = counter(window, cx, "prim-holds");
|
||||
let (tap_count, hold_count) = (*taps.read(cx), *holds.read(cx));
|
||||
let bump = |count: &gpui::Entity<u32>, cx: &mut App| {
|
||||
count.update(cx, |count, cx| {
|
||||
*count += 1;
|
||||
cx.notify();
|
||||
})
|
||||
};
|
||||
section(
|
||||
"Pressable",
|
||||
"Knows when it is held. Hold half a second for a long press.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_6()
|
||||
.child(probe(
|
||||
"prim-press",
|
||||
Pressable::new("prim-press", |pressed, _, cx| {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.w(px(160.0))
|
||||
.h_16()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.bg(if pressed {
|
||||
theme.colors.active
|
||||
} else {
|
||||
theme.colors.sunken
|
||||
})
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.child(if pressed { "Holding" } else { "Press" })
|
||||
.into_any_element()
|
||||
})
|
||||
.on_press(move |_, cx| bump(&taps, cx))
|
||||
.on_long_press(move |_, cx| bump(&holds, cx)),
|
||||
))
|
||||
.child(code(format!("taps {tap_count} · holds {hold_count}"), cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn measure(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let size = window.use_keyed_state("prim-size", cx, |_, _| None::<Size<Pixels>>);
|
||||
let wide = window.use_keyed_state("prim-wide", cx, |_, _| false);
|
||||
let (measured, is_wide) = (*size.read(cx), *wide.read(cx));
|
||||
let theme = cx.theme();
|
||||
let readout = match measured {
|
||||
Some(size) => format!(
|
||||
"{:.0} × {:.0}",
|
||||
f32::from(size.width),
|
||||
f32::from(size.height)
|
||||
),
|
||||
None => "measuring…".to_string(),
|
||||
};
|
||||
section(
|
||||
"Measure",
|
||||
"Reports bounds when they change. This is also the ResizeObserver.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.child(
|
||||
Button::new("measure-toggle", if is_wide { "Narrow" } else { "Widen" }).on_click(
|
||||
move |_, _, cx| {
|
||||
wide.update(cx, |wide, cx| {
|
||||
*wide = !*wide;
|
||||
cx.notify();
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(code(readout, cx)),
|
||||
)
|
||||
.child(
|
||||
Measure::new("prim-measured", move |bounds, _, cx| {
|
||||
size.update(cx, |size, cx| {
|
||||
*size = Some(bounds.size);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.w(px(if is_wide { 420.0 } else { 220.0 }))
|
||||
.p_3()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.bg(theme.colors.sunken)
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child("Text wraps to the width it gets. The box reports each new size once."),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn intersection(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let seen = window.use_keyed_state("prim-seen", cx, |_, _| false);
|
||||
let visible = *seen.read(cx);
|
||||
let theme = cx.theme();
|
||||
let spacer = || div().h(px(140.0));
|
||||
let status = if visible { "in view" } else { "out of view" };
|
||||
section(
|
||||
"IntersectionObserver",
|
||||
"Scroll the box. The marker reports entering and leaving.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.id("prim-scroller")
|
||||
.w(px(260.0))
|
||||
.h(px(120.0))
|
||||
.overflow_y_scroll()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.child(spacer())
|
||||
.child(
|
||||
IntersectionObserver::new("prim-marker", move |visible, _, cx| {
|
||||
seen.update(cx, |seen, cx| {
|
||||
*seen = visible;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.mx_3()
|
||||
.p_2()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(theme.colors.info_subtle)
|
||||
.text_color(theme.colors.info)
|
||||
.child("Marker"),
|
||||
)
|
||||
.child(spacer()),
|
||||
)
|
||||
.child(code(status, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn tooltip(cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
let muted = theme.colors.tooltip_fg.opacity(0.7);
|
||||
let lane = div()
|
||||
.w(px(260.0))
|
||||
.h_12()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child("Move across me");
|
||||
section(
|
||||
"Tooltip Trigger",
|
||||
"gpui's trigger with Ely's body, or Ely's own trigger for delay and tracking.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.gap_2()
|
||||
.child(probe(
|
||||
"tip-plain",
|
||||
div()
|
||||
.id("tip-plain")
|
||||
.child(IconButton::new("tip-a", IconName::Copy))
|
||||
.tooltip(Tooltip::text("Copy")),
|
||||
))
|
||||
.child(probe(
|
||||
"tip-meta",
|
||||
div()
|
||||
.id("tip-meta")
|
||||
.child(IconButton::new("tip-b", IconName::Search))
|
||||
.tooltip(Tooltip::with_meta("Search", "⌘K")),
|
||||
))
|
||||
.child(probe(
|
||||
"tip-rich",
|
||||
div()
|
||||
.id("tip-rich")
|
||||
.child(IconButton::new("tip-c", IconName::Info))
|
||||
.tooltip(Tooltip::rich(move |_, _| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_0p5()
|
||||
.child(div().font_weight(FontWeight::MEDIUM).child("Rich content"))
|
||||
.child(div().text_color(muted).child("Any element fits here."))
|
||||
.into_any_element()
|
||||
})),
|
||||
))
|
||||
.child(div().w_4())
|
||||
.child(probe(
|
||||
"tip-follow",
|
||||
TooltipTrigger::new("tip-follow", lane, |_, _| {
|
||||
SharedString::from("No delay. I follow.").into_any_element()
|
||||
})
|
||||
.delay(Duration::ZERO)
|
||||
.follow_mouse(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(theme.colors.fg_subtle)
|
||||
.child("Ely's trigger waits 400ms by default; gpui's waits 500ms."),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user