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
@@ -21,7 +21,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
|
||||
## Commands
|
||||
|
||||
- `cargo run --example gallery` opens the gallery. `-- --page <slug>` starts on a page; `--story <title or slug>` with it draws that section alone.
|
||||
- `cargo run --example gallery` opens the gallery. `-- --page <slug>` starts on a page; `--story <title or slug>` with it draws that section alone, and `--capture` then shoots it without the page's script.
|
||||
- `cargo run --example gallery -- --capture <dir>` writes PNGs of every page, light and dark, top to bottom, then each page's scripted states, including windows the demos open. macOS only.
|
||||
- `cargo run --example gallery -- --narrow 280 --capture <dir>` lays each page, header and body, at that width in a padded card and shoots it without its script, whose aims are measured at full width. Demos that set their own width run past the card.
|
||||
- `RUSTC_BOOTSTRAP=1 cargo check --lib --target wasm32-unknown-unknown` checks the web build; gpui_web's `wasm_thread` needs the bootstrap, as Zed's web examples do.
|
||||
@@ -180,9 +180,9 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- Time reads `web_time::Instant`: std's compiles for wasm32 and panics in the browser. `scripts/check.sh` refuses std's in `src` and the gallery.
|
||||
- What cannot build for wasm32 is absent there through `cfg(not(target_family = "wasm"))`, never faked: the live terminal, `Terminal` and `Launch` with their grid, keys and search, and the gallery's capture harness.
|
||||
- A gallery demo a browser cannot run says why on the web through `ui::web_note`, in the blocked style: a second window (`own_window`), files from the dialog or a drop (`NO_FILES`), the menu bar, window moves, a clipboard read.
|
||||
- A story is a gallery section: its title is a literal, one per page. Under `ui::Story` the one asked for draws without its title; the rest drop their demos (`ui::Section`), so nothing of theirs lays out, loads or moves. A story the page lacks fails in the page.
|
||||
- A story is a gallery section: its title is a literal, one per page. Under `ui::Story` the one asked for draws without its title; the rest drop their demos (`ui::Section`), so nothing of theirs lays out, loads or moves. Their builders still run, so a demo that keeps its own clock starts it through `ui::live`, which a story grants its own section alone and fails on a title no section has. A story the page lacks fails in the page.
|
||||
- The gallery's pictures go through `asset!` and `ui::picture` or `ui::resource`: a file natively, on the web an address under `ELY_GALLERY_ASSETS`, since gpui reads no files there.
|
||||
- On the web a paste reaches the focused field as the browser's paste event, through `EntityInputHandler::paste`: gpui_web reads no clipboard on demand, so Paste stays unbound there (`TextInput`, `CodeEditor`).
|
||||
- On the web a paste reaches the focused field as the browser's paste event, through `EntityInputHandler::paste`, so Paste stays unbound there (`TextInput`, `CodeEditor`). Code that reads the clipboard itself awaits `read_from_clipboard_async`: gpui_web's `read_from_clipboard` answers at once and finds nothing (`tables::DataGrid`'s Cmd-V).
|
||||
- The first frame wears the host's mode through `Theme::set_mode_now`, a mode sent before the window opens included; `set_mode` fades from the colors on screen.
|
||||
- The system time zone comes from `typography::format::system_zone`, which fails loud; jiff's `TimeZone::system()` falls back to UTC in silence.
|
||||
- A Select with nothing to pick stays shut: disabled, it shows its placeholder; open, it fails (`media::DeviceSelector` says no device was found).
|
||||
|
||||
@@ -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:#}")))?;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ OUT = ROOT / "examples/gallery/stories.json"
|
||||
CALL = re.compile(r"(?<![\w.])(?:(\w+)::)?(\w+)\s*\(")
|
||||
CHAIN = re.compile(r"\s*\.\s*\w+\s*(?:::<[^>]*>\s*)?\(")
|
||||
|
||||
# Components a story draws beyond its title's, by page and the title's first name.
|
||||
# What a story draws beyond its title's names.
|
||||
SHOWN = {
|
||||
("forms", "Form"): ["FormLabel", "FormError"],
|
||||
("tables", "DataTable"): ["Sparkline"],
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Builds the browser gallery into <out>; ELY_GALLERY_ASSETS is the address of <out>/assets/.
|
||||
# Builds the browser gallery; ELY_GALLERY_ASSETS addresses <out>/assets/.
|
||||
set -eu
|
||||
out=$(mkdir -p "${1:?usage: scripts/web.sh <out dir>}" && cd "$1" && pwd)
|
||||
: "${ELY_GALLERY_ASSETS:?set ELY_GALLERY_ASSETS to the address of <out>/assets/, ending in /}"
|
||||
@@ -10,12 +10,12 @@ wasm-bindgen --version | grep -qx "wasm-bindgen $version" || {
|
||||
exit 1
|
||||
}
|
||||
command -v wasm-opt >/dev/null || { echo "wasm-opt needed: brew install binaryen, or npm i -g binaryen" >&2; exit 1; }
|
||||
# gpui_web's wasm_thread asks for an unstable feature even single-threaded, as Zed's own web build does.
|
||||
# gpui_web's wasm_thread needs an unstable feature, as in Zed.
|
||||
RUSTC_BOOTSTRAP=1 cargo build --example gallery --target wasm32-unknown-unknown --profile web
|
||||
wasm="${CARGO_TARGET_DIR:-target}/wasm32-unknown-unknown/web/examples/gallery.wasm"
|
||||
wasm-bindgen "$wasm" --out-dir "$out" --out-name gallery --target web --no-typescript \
|
||||
--remove-name-section --remove-producers-section
|
||||
# Cloudflare serves no file over 25 MiB; -Oz takes a seventh off, though not off the gzip.
|
||||
# Cloudflare serves no file over 25 MiB; -Oz leaves room.
|
||||
wasm-opt -Oz --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext \
|
||||
--enable-mutable-globals --enable-reference-types --enable-multivalue \
|
||||
"$out/gallery_bg.wasm" -o "$out/gallery_bg.wasm"
|
||||
|
||||
@@ -161,6 +161,8 @@ impl CodeEditor {
|
||||
div()
|
||||
.w(advance * digits as f32)
|
||||
.text_right()
|
||||
// Snapped to device pixels, the box can run short.
|
||||
.whitespace_nowrap()
|
||||
.when(current, |number| number.text_color(colors.fg))
|
||||
.child(number),
|
||||
)
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ pub(crate) fn bind_keys(cx: &mut App) {
|
||||
KeyBinding::new("secondary-alt-[", Fold, context),
|
||||
KeyBinding::new("secondary-alt-]", Unfold, context),
|
||||
];
|
||||
// The browser's paste event carries the clipboard to the input handler.
|
||||
// The browser's paste event reaches the input handler.
|
||||
if !cfg!(target_family = "wasm") {
|
||||
bindings.push(KeyBinding::new("secondary-v", Paste, context));
|
||||
}
|
||||
|
||||
+9
-2
@@ -112,6 +112,8 @@ pub struct CodeEditor {
|
||||
pub(crate) metrics: Metrics,
|
||||
pub(crate) dragging: bool,
|
||||
pub(crate) caret_on: bool,
|
||||
/// Blink runs only while focused, or unseen cursors redraw forever.
|
||||
focused: bool,
|
||||
pub(crate) chat_field: gpui::Entity<TextInput>,
|
||||
pub(crate) frame: Frame,
|
||||
blink_epoch: u64,
|
||||
@@ -133,8 +135,12 @@ impl CodeEditor {
|
||||
let chat_field =
|
||||
cx.new(|cx| TextInput::new(window, cx).placeholder("Ask to change this code"));
|
||||
let subscriptions = vec![
|
||||
cx.on_focus(&focus, window, |editor, _, cx| editor.restart_blink(cx)),
|
||||
cx.on_focus(&focus, window, |editor, _, cx| {
|
||||
editor.focused = true;
|
||||
editor.restart_blink(cx);
|
||||
}),
|
||||
cx.on_blur(&focus, window, |editor, _, cx| {
|
||||
editor.focused = false;
|
||||
editor.dragging = false;
|
||||
editor._blink = None;
|
||||
cx.notify();
|
||||
@@ -178,6 +184,7 @@ impl CodeEditor {
|
||||
metrics: Metrics::default(),
|
||||
dragging: false,
|
||||
caret_on: true,
|
||||
focused: false,
|
||||
chat_field,
|
||||
frame: Frame::default(),
|
||||
blink_epoch: 0,
|
||||
@@ -462,7 +469,7 @@ impl CodeEditor {
|
||||
self.blink_epoch += 1;
|
||||
let epoch = self.blink_epoch;
|
||||
let still = cx.theme().reduced_motion;
|
||||
self._blink = (!still).then(|| {
|
||||
self._blink = (!still && self.focused).then(|| {
|
||||
cx.spawn(async move |editor, cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(BLINK).await;
|
||||
|
||||
+28
-1
@@ -1,4 +1,4 @@
|
||||
use std::time::Duration;
|
||||
use std::{cell::Cell, rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
ClipboardItem, Entity, EntityInputHandler, KeyUpEvent, Keystroke, TestAppContext,
|
||||
@@ -256,3 +256,30 @@ fn a_browser_paste_gives_each_cursor_its_line(cx: &mut TestAppContext) {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn only_a_focused_editor_blinks(cx: &mut TestAppContext) {
|
||||
let (editor, cx) = editor("a\nb", cx);
|
||||
let redraws = Rc::new(Cell::new(0));
|
||||
let counted = redraws.clone();
|
||||
let _count = cx.update(|_, cx| cx.observe(&editor, move |_, _| counted.set(counted.get() + 1)));
|
||||
editor.update(cx, |editor, cx| {
|
||||
let second = 2..2;
|
||||
editor.select([second], cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
redraws.set(0);
|
||||
cx.executor().advance_clock(Duration::from_secs(3));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(redraws.get(), 0, "an unfocused editor keeps still");
|
||||
cx.update(|window, cx| {
|
||||
window.activate_window();
|
||||
let focus = editor.read(cx).focus.clone();
|
||||
focus.focus(window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
redraws.set(0);
|
||||
cx.executor().advance_clock(Duration::from_secs(3));
|
||||
cx.run_until_parked();
|
||||
assert!(redraws.get() > 2, "a focused one blinks");
|
||||
}
|
||||
@@ -89,7 +89,7 @@ pub(crate) fn bind_keys(cx: &mut App) {
|
||||
KeyBinding::new("secondary-enter", Submit, Some(CONTEXT)),
|
||||
KeyBinding::new("secondary-enter", Submit, Some(FORM_CONTEXT)),
|
||||
];
|
||||
// The browser's paste event carries the clipboard to the input handler.
|
||||
// The browser's paste event reaches the input handler.
|
||||
if !cfg!(target_family = "wasm") {
|
||||
bindings.push(KeyBinding::new("secondary-v", Paste, Some(CONTEXT)));
|
||||
}
|
||||
|
||||
+22
-10
@@ -1,6 +1,9 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, ClipboardItem, Entity, KeyDownEvent, Pixels, Point, SharedString, Window, point};
|
||||
use anyhow::anyhow;
|
||||
use gpui::{
|
||||
App, ClipboardItem, Entity, KeyDownEvent, Pixels, Point, SharedString, TaskExt, Window, point,
|
||||
};
|
||||
|
||||
use super::{
|
||||
formula::filled,
|
||||
@@ -283,15 +286,24 @@ pub(crate) fn keys(
|
||||
sheet.read(cx),
|
||||
))),
|
||||
("v", true) => {
|
||||
let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else {
|
||||
log::info!("grid: the clipboard holds no text to paste");
|
||||
return;
|
||||
};
|
||||
let edits = pasted(&text, (row, col), (rows, cols));
|
||||
log::info!("grid: pasted {} cells", edits.len());
|
||||
if let Some(on_change) = &face.on_change {
|
||||
on_change(&edits, window, cx);
|
||||
}
|
||||
// The web reads its clipboard only through the async reader.
|
||||
let read = cx.read_from_clipboard_async();
|
||||
let on_change = face.on_change.clone();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = read.await.map_err(|error| anyhow!("{error:?}"))?;
|
||||
let Some(text) = item.and_then(|item| item.text()) else {
|
||||
log::info!("grid: the clipboard holds no text to paste");
|
||||
return Ok(());
|
||||
};
|
||||
let edits = pasted(&text, (row, col), (rows, cols));
|
||||
log::info!("grid: pasted {} cells", edits.len());
|
||||
if let Some(on_change) = on_change {
|
||||
cx.update(|window, cx| on_change(&edits, window, cx))?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
_ => {
|
||||
let typed = stroke.key_char.clone().filter(|typed| {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{ClipboardItem, Modifiers, TestAppContext, point, px};
|
||||
|
||||
use super::{Grid, settle};
|
||||
use crate::theme::Theme;
|
||||
|
||||
#[gpui::test]
|
||||
fn a_paste_fills_cells_from_the_cursor(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
crate::forms::bind_keys(cx);
|
||||
});
|
||||
let heard = Rc::new(RefCell::new(Vec::new()));
|
||||
let seen = heard.clone();
|
||||
let (_, cx) = cx.add_window_view(|_, _| Grid(seen));
|
||||
settle(cx);
|
||||
cx.simulate_click(point(px(48.0), px(42.0)), Modifiers::none());
|
||||
settle(cx);
|
||||
cx.update(|_, cx| cx.write_to_clipboard(ClipboardItem::new_string("x\ty\nz\tw".into())));
|
||||
cx.simulate_keystrokes("cmd-v");
|
||||
settle(cx);
|
||||
let cells = |row, col, text: &str| (row, col, text.to_string());
|
||||
assert_eq!(
|
||||
*heard.borrow(),
|
||||
[
|
||||
cells(0, 0, "x"),
|
||||
cells(0, 1, "y"),
|
||||
cells(1, 0, "z"),
|
||||
cells(1, 1, "w")
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use gpui::{
|
||||
Styled, TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
|
||||
mod clipboard;
|
||||
mod columns;
|
||||
|
||||
use super::{Column, DataTable, FilterBuilder, FilterRule, Row};
|
||||
|
||||
@@ -90,7 +90,7 @@ pub(crate) fn printed(source: &str) -> (String, Vec<(Range<usize>, Pen)>) {
|
||||
(printed.text, printed.spans)
|
||||
}
|
||||
|
||||
/// Plain text sets no colors with OSC 4, 10 or 11.
|
||||
/// Plain text sets no colors through OSC 4, 10, 11.
|
||||
struct Unset;
|
||||
|
||||
impl Index<usize> for Unset {
|
||||
|
||||
@@ -93,7 +93,7 @@ impl Ink {
|
||||
}
|
||||
}
|
||||
|
||||
/// Colors a program set with OSC 4, 10 or 11, by index.
|
||||
/// Colors a program set through OSC 4, 10, 11.
|
||||
pub(crate) trait Set: Index<usize, Output = Option<Rgb>> {}
|
||||
|
||||
impl<T: Index<usize, Output = Option<Rgb>>> Set for T {}
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ impl Theme {
|
||||
Self::fade_to_target(cx);
|
||||
}
|
||||
|
||||
/// Sets `mode` without the fade, as before a window's first frame; a fade under way stops.
|
||||
/// Sets `mode` at once, stopping any fade under way.
|
||||
pub fn set_mode_now(mode: Mode, cx: &mut App) {
|
||||
log::info!("theme: mode -> {mode:?}, at once");
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ fn an_owners_palette_serves_its_mode_alone(cx: &mut TestAppContext) {
|
||||
assert_eq!(cx.read(|cx| cx.theme().palette()), Palette::light(false));
|
||||
}
|
||||
|
||||
/// A mode set at once wears its palette at once, and a fade under way cannot pull it back.
|
||||
/// A mode set at once keeps its palette through fades.
|
||||
#[gpui::test]
|
||||
fn a_mode_set_at_once_skips_the_fade(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
|
||||
Reference in new issue
Block a user