Demos that open a second window, take files, move the window, read the clipboard or set native menus show a note on the web in the blocked style. IBM Plex Sans Regular joins the assets, unmodified, where gpui's svg renderer loads it: gpui_web answers .SystemUIFont with it. The gallery's web entry registers a Hebrew cut of Noto Sans Hebrew. scripts/web.sh builds the bundle with wasm-bindgen and wasm-opt -Oz, 20.0 MB, and fails past 25 MiB.
483 lines
16 KiB
Rust
483 lines
16 KiB
Rust
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, web_note},
|
|
};
|
|
|
|
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,
|
|
)
|
|
.children(web_note(
|
|
"A browser reads its clipboard only as it pastes, into a field: on the web gpui's read_from_clipboard finds nothing, so Paste reports no text. Copy works.",
|
|
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)),
|
|
)
|
|
}
|