T44d Web · Hidden demos keep still, wide ones scroll whole, pastes read the web's clipboard
This commit is contained in:
26 files changed
+211
-101
No files matched your search
@@ -9,14 +9,14 @@
|
||||
html.dark, html.dark body { background: #131110; }
|
||||
</style>
|
||||
<script>
|
||||
// Ely's own background under the canvas until the first frame, in the mode asked for.
|
||||
// Ely's ground under the canvas until the first frame.
|
||||
if (new URLSearchParams(location.search).get("theme") === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import init, { set_theme } from "./gallery.js";
|
||||
// The host sets the mode with gallery.setTheme(mode) or a message { ely: "theme", theme }, early or late.
|
||||
// The host sets the mode by setTheme or a message.
|
||||
let early = null;
|
||||
let apply = (theme) => { early = theme; };
|
||||
const setTheme = (theme) => {
|
||||
@@ -30,7 +30,7 @@
|
||||
await init();
|
||||
apply = set_theme;
|
||||
if (early) set_theme(early);
|
||||
// A host that waits for this knows the mode it sends lands.
|
||||
// Tells the host that a mode it sends now lands.
|
||||
window.parent.postMessage({ ely: "ready" }, "*");
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -12,7 +12,7 @@ macro_rules! asset {
|
||||
};
|
||||
}
|
||||
|
||||
/// On the web, the address the site serves it at: the build names the folder.
|
||||
/// On the web, the build names the pictures' address.
|
||||
#[cfg(target_family = "wasm")]
|
||||
macro_rules! asset {
|
||||
($name:literal) => {
|
||||
@@ -51,7 +51,7 @@ use gpui::{
|
||||
|
||||
actions!(gallery, [Quit, About, LightMode, DarkMode, NewWindow]);
|
||||
|
||||
/// The gallery's menu bar: NativeMenu at work, and what MenuBar draws.
|
||||
/// The gallery's menu bar, drawn natively and by MenuBar.
|
||||
pub fn app_menus() -> Vec<Menu> {
|
||||
vec![
|
||||
Menu {
|
||||
@@ -74,7 +74,7 @@ pub fn app_menus() -> Vec<Menu> {
|
||||
]
|
||||
}
|
||||
|
||||
/// The menu bar and Dock menu (JumpList). Set before the window opens, whose first frame may draw them.
|
||||
/// Menu bar and Dock menu, set before the window opens.
|
||||
fn menus(cx: &mut App) {
|
||||
cx.set_menus(app_menus());
|
||||
cx.set_dock_menu(vec![MenuItem::action("New Window", NewWindow)]);
|
||||
@@ -104,7 +104,7 @@ pub struct Start {
|
||||
pub choice: shell::Choice,
|
||||
/// Narrow check width, if any.
|
||||
pub narrow: Option<f32>,
|
||||
/// Why the start could not be read, shown in place of a page.
|
||||
/// Why the start could not be read, shown instead.
|
||||
pub failure: Option<SharedString>,
|
||||
}
|
||||
|
||||
@@ -195,7 +195,8 @@ fn main() -> Result<()> {
|
||||
page,
|
||||
capture,
|
||||
} = parse_args()?;
|
||||
let scripted = start.narrow.is_none();
|
||||
// A script aims across its page; cards and stories hold part.
|
||||
let scripted = start.narrow.is_none() && start.story.is_none();
|
||||
gpui_platform::application()
|
||||
.with_assets(Assets)
|
||||
.run(move |cx: &mut App| {
|
||||
|
||||
@@ -12,7 +12,7 @@ use gpui::{App, ClipboardItem, IntoElement, ParentElement, SharedString, Styled,
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, noise, row, section, set},
|
||||
ui::{keep, live, noise, row, section, set},
|
||||
};
|
||||
|
||||
const MONTHS: [&str; 12] = [
|
||||
@@ -176,7 +176,7 @@ pub fn realtime(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
cx,
|
||||
);
|
||||
let ticking = keep("stream-tick", || false, window, cx);
|
||||
if !*ticking.read(cx) && !cx.theme().reduced_motion {
|
||||
if !*ticking.read(cx) && !cx.theme().reduced_motion && live("RealtimeChart", cx) {
|
||||
set(&ticking, true, cx);
|
||||
let stream = stream.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
|
||||
@@ -13,7 +13,7 @@ use gpui::{
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{code, row, section, web_note},
|
||||
ui::{code, row, section, set},
|
||||
};
|
||||
|
||||
pub fn boxes(cx: &App) -> impl IntoElement + use<> {
|
||||
@@ -442,13 +442,9 @@ pub fn clipboard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
||||
let muted = cx.theme().colors.fg_muted;
|
||||
section(
|
||||
"Clipboard",
|
||||
"write_to_clipboard and read_from_clipboard on App.",
|
||||
"write_to_clipboard and read_from_clipboard_async on App; a browser asks before it hands the clipboard over.",
|
||||
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()
|
||||
@@ -464,14 +460,18 @@ pub fn clipboard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
||||
.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();
|
||||
let (read, pasted) = (cx.read_from_clipboard_async(), pasted.clone());
|
||||
cx.spawn(async move |cx| {
|
||||
let said: SharedString = match read.await {
|
||||
Ok(item) => match item.and_then(|item| item.text()) {
|
||||
Some(text) => text.into(),
|
||||
None => "The clipboard holds no text.".into(),
|
||||
},
|
||||
Err(error) => format!("The clipboard stayed shut: {error:?}").into(),
|
||||
};
|
||||
cx.update(|cx| set(&pasted, Some(said), cx));
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
)
|
||||
.when_some(shown, |row, text| {
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, section, set, web_note},
|
||||
ui::{keep, section, set},
|
||||
};
|
||||
|
||||
type Cells = HashMap<(usize, usize), SharedString>;
|
||||
@@ -35,10 +35,6 @@ pub fn data_grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
||||
"Cells to edit like a spreadsheet's. Arrows walk, Shift stretches a range, typing or a double press edits, Delete clears, and Cmd-C and Cmd-V copy and paste.",
|
||||
cx,
|
||||
)
|
||||
.children(web_note(
|
||||
"A browser reads its clipboard only as it pastes into a text field, so Cmd-V pastes no cells here.",
|
||||
cx,
|
||||
))
|
||||
.child(probe(
|
||||
"grid",
|
||||
div().child(
|
||||
|
||||
@@ -37,7 +37,7 @@ const SCRIPT: &[Step] = &[
|
||||
Step::Rest,
|
||||
];
|
||||
|
||||
/// A live shell natively; a browser runs no processes, so there it says so.
|
||||
/// A live shell natively; on the web, why not.
|
||||
fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let section = section(
|
||||
"Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink",
|
||||
|
||||
@@ -7,7 +7,7 @@ use ely_gpui_component::{
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, Task, Window, div};
|
||||
|
||||
use crate::ui::{row, section, specimen, specimens};
|
||||
use crate::ui::{live, row, section, specimen, specimens};
|
||||
|
||||
const STEP: Duration = Duration::from_millis(1600);
|
||||
const VALUES: [f64; 5] = [1_284.0, 1_917.0, 12_406.0, 9_998.0, 10_031.0];
|
||||
@@ -18,24 +18,12 @@ struct Ticker {
|
||||
}
|
||||
|
||||
pub fn animated_number(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let ticker = window.use_keyed_state("type-ticker", cx, |window, cx| Ticker {
|
||||
index: 0,
|
||||
_task: cx.spawn_in(window, async move |ticker, cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(STEP).await;
|
||||
let advanced = cx.update(|_, cx| {
|
||||
ticker.update(cx, |ticker: &mut Ticker, cx| {
|
||||
ticker.index = (ticker.index + 1) % VALUES.len();
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
if advanced.and_then(|inner| inner).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
});
|
||||
let value = VALUES[ticker.read(cx).index];
|
||||
let index = if live("AnimatedNumber", cx) {
|
||||
ticker(window, cx).read(cx).index
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let value = VALUES[index];
|
||||
section(
|
||||
"AnimatedNumber",
|
||||
"Digits roll by place; or the value counts up.",
|
||||
@@ -55,6 +43,27 @@ pub fn animated_number(window: &mut Window, cx: &mut App) -> impl IntoElement +
|
||||
)
|
||||
}
|
||||
|
||||
/// Steps through the values on a clock of its own.
|
||||
fn ticker(window: &mut Window, cx: &mut App) -> gpui::Entity<Ticker> {
|
||||
window.use_keyed_state("type-ticker", cx, |window, cx| Ticker {
|
||||
index: 0,
|
||||
_task: cx.spawn_in(window, async move |ticker, cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(STEP).await;
|
||||
let advanced = cx.update(|_, cx| {
|
||||
ticker.update(cx, |ticker: &mut Ticker, cx| {
|
||||
ticker.index = (ticker.index + 1) % VALUES.len();
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
if advanced.and_then(|inner| inner).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn typewriter(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let take = window.use_keyed_state("type-take", cx, |_, _| 0usize);
|
||||
let lines = [
|
||||
|
||||
@@ -228,7 +228,7 @@ impl Gallery {
|
||||
}
|
||||
}
|
||||
|
||||
/// The window's root: the focus scope, over Ely's ground, ink and type.
|
||||
/// The window's root: the focus scope over Ely's ground.
|
||||
fn root(&self, cx: &App) -> FocusScope {
|
||||
let theme = cx.theme();
|
||||
FocusScope::new(&self.focus)
|
||||
@@ -241,7 +241,7 @@ impl Gallery {
|
||||
.text_size(theme.text_size(TextSize::Base))
|
||||
}
|
||||
|
||||
/// The page's demos, or the story's alone; why not, once the start or the story failed.
|
||||
/// The page's demos, the story's alone, or why not.
|
||||
fn body(
|
||||
&mut self,
|
||||
page: &pages::Page,
|
||||
@@ -290,8 +290,17 @@ impl Render for Gallery {
|
||||
.size_full()
|
||||
.overflow_scroll()
|
||||
.track_scroll(&self.scroll)
|
||||
// The page column's 848 plus padding; narrower, the story scrolls sideways.
|
||||
.child(div().min_w(px(896.0)).p_6().child(body)),
|
||||
// The widest demo, 900, plus padding; narrower, it scrolls sideways.
|
||||
.child(
|
||||
div()
|
||||
.min_w(px(948.0))
|
||||
.p_6()
|
||||
// Natively the traffic lights take the top 56.
|
||||
.when(cfg!(not(target_family = "wasm")), |story| {
|
||||
story.pt(px(56.0))
|
||||
})
|
||||
.child(body),
|
||||
),
|
||||
);
|
||||
}
|
||||
let sidebar = self.sidebar(cx);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pub enum Step {
|
||||
Rest,
|
||||
Hover(&'static str),
|
||||
/// Moves the pointer to an offset from the probe's top-left corner.
|
||||
/// Moves the pointer to an offset in the probe.
|
||||
HoverAt(&'static str, f32, f32),
|
||||
Down(&'static str),
|
||||
Up(&'static str),
|
||||
@@ -13,13 +13,13 @@ pub enum Step {
|
||||
DownAt(&'static str, f32, f32),
|
||||
/// Right-clicks at an offset from the probe's top-left corner.
|
||||
RightAt(&'static str, f32, f32),
|
||||
/// Drags from the last point to an offset, in small steps.
|
||||
/// Drags from the last point to an offset, in steps.
|
||||
DragTo(&'static str, f32, f32),
|
||||
/// Swipes two fingers sideways across the probe's middle, as a trackpad does: begin, move, end.
|
||||
/// Swipes two fingers sideways across the probe's middle.
|
||||
Swipe(&'static str, f32),
|
||||
UpAt(&'static str, f32, f32),
|
||||
Key(&'static str),
|
||||
/// Types text into whatever holds focus, one key at a time.
|
||||
/// Types text into the focus, one key at a time.
|
||||
Type(&'static str),
|
||||
Wait(u64),
|
||||
Shot(&'static str),
|
||||
@@ -28,14 +28,14 @@ pub enum Step {
|
||||
CloseWindow(&'static str),
|
||||
/// Sends a keystroke to a window a demo opened.
|
||||
KeyWindow(&'static str, &'static str),
|
||||
/// Asks AppKit to close a window, as its close button would.
|
||||
/// Asks AppKit to close a window, as its button would.
|
||||
CloseNative(&'static str),
|
||||
/// Fails unless the window is gone.
|
||||
ExpectClosed(&'static str),
|
||||
/// Photographs this app's frontmost popup, such as the share picker.
|
||||
ShotPopup(&'static str),
|
||||
/// Posts a key down to AppKit itself; `escape` and `5` are known.
|
||||
/// Posts a key down to AppKit: `escape` or `5`.
|
||||
NativeKey(&'static str),
|
||||
/// Cancels the open file panel, whose content runs out of process.
|
||||
/// Cancels the open file panel, which runs out of process.
|
||||
CancelPanel,
|
||||
}
|
||||
+23
-10
@@ -6,10 +6,11 @@ use gpui::{
|
||||
Resource, SharedString, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
/// The one section a story draws, while a page renders; the rest draw nothing.
|
||||
/// The one section a story draws while its page renders.
|
||||
pub struct Story {
|
||||
wanted: SharedString,
|
||||
met: RefCell<Vec<SharedString>>,
|
||||
live: RefCell<Vec<SharedString>>,
|
||||
}
|
||||
|
||||
impl Global for Story {}
|
||||
@@ -19,6 +20,7 @@ impl Story {
|
||||
Self {
|
||||
wanted,
|
||||
met: RefCell::default(),
|
||||
live: RefCell::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +28,12 @@ impl Story {
|
||||
*self.wanted == *title || *self.wanted == slug(title)
|
||||
}
|
||||
|
||||
/// Why the page drew no single section by this name, if it did not.
|
||||
/// Why no single section answered this story, if none did.
|
||||
pub fn missing(&self, page: &str) -> Option<String> {
|
||||
let met = self.met.borrow();
|
||||
if let Some(stray) = self.live.borrow().iter().find(|title| !met.contains(title)) {
|
||||
return Some(format!("ui::live names {stray}, no section on {page}."));
|
||||
}
|
||||
match met.iter().filter(|title| self.answers(title)).count() {
|
||||
1 => None,
|
||||
0 => Some(format!(
|
||||
@@ -41,7 +46,15 @@ impl Story {
|
||||
}
|
||||
}
|
||||
|
||||
/// A title as a story's address: lowercase letters and digits, dashes between.
|
||||
/// Whether a demo's clock runs: on its page or story.
|
||||
pub fn live(title: &str, cx: &App) -> bool {
|
||||
cx.try_global::<Story>().is_none_or(|story| {
|
||||
story.live.borrow_mut().push(title.to_string().into());
|
||||
story.answers(title)
|
||||
})
|
||||
}
|
||||
|
||||
/// A title as a story's address: lowercase, dashes between.
|
||||
pub fn slug(title: &str) -> String {
|
||||
let mut slug = String::new();
|
||||
for c in title.chars() {
|
||||
@@ -54,7 +67,7 @@ pub fn slug(title: &str) -> String {
|
||||
slug.trim_end_matches('-').to_string()
|
||||
}
|
||||
|
||||
/// Titled block of demos. Under a story, only the one asked for draws, without its title.
|
||||
/// Titled block of demos; a story draws one, untitled.
|
||||
pub fn section(title: impl Into<SharedString>, note: impl Into<SharedString>, cx: &App) -> Section {
|
||||
let title = title.into();
|
||||
let theme = cx.theme();
|
||||
@@ -95,7 +108,7 @@ pub fn section(title: impl Into<SharedString>, note: impl Into<SharedString>, cx
|
||||
)
|
||||
}
|
||||
|
||||
/// A section's box. One a story passes over drops its demos, so none lays out, loads or moves.
|
||||
/// A section's box; one a story passes drops its demos.
|
||||
pub struct Section {
|
||||
div: Div,
|
||||
shown: bool,
|
||||
@@ -117,7 +130,7 @@ impl IntoElement for Section {
|
||||
}
|
||||
}
|
||||
|
||||
/// A picture under the gallery's assets: a file natively, an address on the web.
|
||||
/// A gallery picture: a file, or a web address.
|
||||
pub fn resource(path: impl Into<SharedString>) -> Resource {
|
||||
let path = path.into();
|
||||
if cfg!(target_family = "wasm") {
|
||||
@@ -195,15 +208,15 @@ pub const UNREAD: &str = if cfg!(target_family = "wasm") {
|
||||
"gpui builds an AccessKit tree from elements with an id and a role, and no screen reader here reads it"
|
||||
};
|
||||
|
||||
/// Why a demo that takes files takes none on the web.
|
||||
/// Why a demo takes no files on the web.
|
||||
pub const NO_FILES: &str = "A browser hands gpui no files: on the web the file dialog fails and dropped files never arrive, so nothing is taken here.";
|
||||
|
||||
/// What a browser cannot do here, and why: a note on the web, nothing natively.
|
||||
/// On the web, what a browser cannot do.
|
||||
pub fn web_note(text: &'static str, cx: &App) -> Option<AnyElement> {
|
||||
cfg!(target_family = "wasm").then(|| blocked(text, cx).into_any_element())
|
||||
}
|
||||
|
||||
/// A demo a browser cannot run: on the web its note stands in.
|
||||
/// A demo browsers cannot run; there, its note instead.
|
||||
pub fn native_only(element: impl IntoElement, note: &'static str, cx: &App) -> AnyElement {
|
||||
match web_note(note, cx) {
|
||||
Some(note) => note,
|
||||
@@ -211,7 +224,7 @@ pub fn native_only(element: impl IntoElement, note: &'static str, cx: &App) -> A
|
||||
}
|
||||
}
|
||||
|
||||
/// A button that opens a window of its own, which a browser page cannot.
|
||||
/// A button opening its own window, which browsers cannot.
|
||||
pub fn own_window(button: impl IntoElement, cx: &App) -> AnyElement {
|
||||
native_only(
|
||||
button,
|
||||
|
||||
@@ -17,13 +17,13 @@ use crate::{
|
||||
shell::{Choice, Gallery},
|
||||
};
|
||||
|
||||
/// The i18n page's Hebrew: gpui_web asks the browser for emoji and CJK alone.
|
||||
/// Hebrew for i18n; browsers lend only emoji and CJK.
|
||||
const HEBREW: &[u8] = include_bytes!("fonts/NotoSansHebrew-Regular.ttf");
|
||||
|
||||
thread_local! {
|
||||
/// The running app and its window, for the host page's calls.
|
||||
/// The running app and its window, for the host's calls.
|
||||
static HOST: RefCell<Option<(AsyncApp, WindowHandle<Gallery>)>> = const { RefCell::new(None) };
|
||||
/// A mode the host page chose before the window opened, which it opens in.
|
||||
/// A mode the host chose before the window opened.
|
||||
static EARLY: Cell<Option<Choice>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ fn read(search: &str) -> Result<Start> {
|
||||
Ok(start)
|
||||
}
|
||||
|
||||
/// The gallery in a browser: one window over the page's canvas.
|
||||
/// The gallery in a browser: one window on a canvas.
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
@@ -88,7 +88,7 @@ pub fn start() {
|
||||
..Start::default()
|
||||
}
|
||||
});
|
||||
// One thread: a static site sends no cross-origin isolation, so there is no shared memory.
|
||||
// One thread: a static site gets no shared memory.
|
||||
let platform = Rc::new(WebPlatform::new_with_backend_and_font_fallback(
|
||||
false,
|
||||
WebBackendPreference::Auto,
|
||||
@@ -110,7 +110,7 @@ pub fn start() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Switches Ely's mode from the host page: `light` or `dark`, kept until the window opens.
|
||||
/// Sets Ely's mode from the host, even before opening.
|
||||
#[wasm_bindgen]
|
||||
pub fn set_theme(theme: &str) -> Result<(), JsError> {
|
||||
let choice = choice(theme).map_err(|error| JsError::new(&format!("{error:#}")))?;
|
||||
|
||||
Reference in new issue
Block a user