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
|
## 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 -- --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.
|
- `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.
|
- `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.
|
- 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.
|
- 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 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.
|
- 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 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.
|
- 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).
|
- 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; }
|
html.dark, html.dark body { background: #131110; }
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<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") {
|
if (new URLSearchParams(location.search).get("theme") === "dark") {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add("dark");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import init, { set_theme } from "./gallery.js";
|
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 early = null;
|
||||||
let apply = (theme) => { early = theme; };
|
let apply = (theme) => { early = theme; };
|
||||||
const setTheme = (theme) => {
|
const setTheme = (theme) => {
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
await init();
|
await init();
|
||||||
apply = set_theme;
|
apply = set_theme;
|
||||||
if (early) set_theme(early);
|
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" }, "*");
|
window.parent.postMessage({ ely: "ready" }, "*");
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</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")]
|
#[cfg(target_family = "wasm")]
|
||||||
macro_rules! asset {
|
macro_rules! asset {
|
||||||
($name:literal) => {
|
($name:literal) => {
|
||||||
@@ -51,7 +51,7 @@ use gpui::{
|
|||||||
|
|
||||||
actions!(gallery, [Quit, About, LightMode, DarkMode, NewWindow]);
|
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> {
|
pub fn app_menus() -> Vec<Menu> {
|
||||||
vec![
|
vec![
|
||||||
Menu {
|
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) {
|
fn menus(cx: &mut App) {
|
||||||
cx.set_menus(app_menus());
|
cx.set_menus(app_menus());
|
||||||
cx.set_dock_menu(vec![MenuItem::action("New Window", NewWindow)]);
|
cx.set_dock_menu(vec![MenuItem::action("New Window", NewWindow)]);
|
||||||
@@ -104,7 +104,7 @@ pub struct Start {
|
|||||||
pub choice: shell::Choice,
|
pub choice: shell::Choice,
|
||||||
/// Narrow check width, if any.
|
/// Narrow check width, if any.
|
||||||
pub narrow: Option<f32>,
|
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>,
|
pub failure: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +195,8 @@ fn main() -> Result<()> {
|
|||||||
page,
|
page,
|
||||||
capture,
|
capture,
|
||||||
} = parse_args()?;
|
} = 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()
|
gpui_platform::application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.run(move |cx: &mut App| {
|
.run(move |cx: &mut App| {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use gpui::{App, ClipboardItem, IntoElement, ParentElement, SharedString, Styled,
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
probe::probe,
|
probe::probe,
|
||||||
ui::{keep, noise, row, section, set},
|
ui::{keep, live, noise, row, section, set},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MONTHS: [&str; 12] = [
|
const MONTHS: [&str; 12] = [
|
||||||
@@ -176,7 +176,7 @@ pub fn realtime(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let ticking = keep("stream-tick", || false, window, 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);
|
set(&ticking, true, cx);
|
||||||
let stream = stream.clone();
|
let stream = stream.clone();
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use gpui::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
probe::probe,
|
probe::probe,
|
||||||
ui::{code, row, section, web_note},
|
ui::{code, row, section, set},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn boxes(cx: &App) -> impl IntoElement + use<> {
|
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;
|
let muted = cx.theme().colors.fg_muted;
|
||||||
section(
|
section(
|
||||||
"Clipboard",
|
"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,
|
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(
|
.child(
|
||||||
row()
|
row()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
@@ -464,14 +460,18 @@ pub fn clipboard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.icon(IconName::Clipboard)
|
.icon(IconName::Clipboard)
|
||||||
.on_click(move |_, _, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
let text = cx.read_from_clipboard().and_then(|item| item.text());
|
let (read, pasted) = (cx.read_from_clipboard_async(), pasted.clone());
|
||||||
pasted.update(cx, |pasted, cx| {
|
cx.spawn(async move |cx| {
|
||||||
*pasted = Some(match text {
|
let said: SharedString = match read.await {
|
||||||
Some(text) => text.into(),
|
Ok(item) => match item.and_then(|item| item.text()) {
|
||||||
None => "The clipboard holds no text.".into(),
|
Some(text) => text.into(),
|
||||||
});
|
None => "The clipboard holds no text.".into(),
|
||||||
cx.notify();
|
},
|
||||||
|
Err(error) => format!("The clipboard stayed shut: {error:?}").into(),
|
||||||
|
};
|
||||||
|
cx.update(|cx| set(&pasted, Some(said), cx));
|
||||||
})
|
})
|
||||||
|
.detach();
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when_some(shown, |row, text| {
|
.when_some(shown, |row, text| {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
probe::probe,
|
probe::probe,
|
||||||
ui::{keep, section, set, web_note},
|
ui::{keep, section, set},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Cells = HashMap<(usize, usize), SharedString>;
|
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.",
|
"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,
|
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(
|
.child(probe(
|
||||||
"grid",
|
"grid",
|
||||||
div().child(
|
div().child(
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const SCRIPT: &[Step] = &[
|
|||||||
Step::Rest,
|
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<> {
|
fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let section = section(
|
let section = section(
|
||||||
"Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink",
|
"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 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 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];
|
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<> {
|
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 {
|
let index = if live("AnimatedNumber", cx) {
|
||||||
index: 0,
|
ticker(window, cx).read(cx).index
|
||||||
_task: cx.spawn_in(window, async move |ticker, cx| {
|
} else {
|
||||||
loop {
|
0
|
||||||
cx.background_executor().timer(STEP).await;
|
};
|
||||||
let advanced = cx.update(|_, cx| {
|
let value = VALUES[index];
|
||||||
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];
|
|
||||||
section(
|
section(
|
||||||
"AnimatedNumber",
|
"AnimatedNumber",
|
||||||
"Digits roll by place; or the value counts up.",
|
"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<> {
|
pub fn typewriter(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
let take = window.use_keyed_state("type-take", cx, |_, _| 0usize);
|
let take = window.use_keyed_state("type-take", cx, |_, _| 0usize);
|
||||||
let lines = [
|
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 {
|
fn root(&self, cx: &App) -> FocusScope {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
FocusScope::new(&self.focus)
|
FocusScope::new(&self.focus)
|
||||||
@@ -241,7 +241,7 @@ impl Gallery {
|
|||||||
.text_size(theme.text_size(TextSize::Base))
|
.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(
|
fn body(
|
||||||
&mut self,
|
&mut self,
|
||||||
page: &pages::Page,
|
page: &pages::Page,
|
||||||
@@ -290,8 +290,17 @@ impl Render for Gallery {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.overflow_scroll()
|
.overflow_scroll()
|
||||||
.track_scroll(&self.scroll)
|
.track_scroll(&self.scroll)
|
||||||
// The page column's 848 plus padding; narrower, the story scrolls sideways.
|
// The widest demo, 900, plus padding; narrower, it scrolls sideways.
|
||||||
.child(div().min_w(px(896.0)).p_6().child(body)),
|
.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);
|
let sidebar = self.sidebar(cx);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
pub enum Step {
|
pub enum Step {
|
||||||
Rest,
|
Rest,
|
||||||
Hover(&'static str),
|
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),
|
HoverAt(&'static str, f32, f32),
|
||||||
Down(&'static str),
|
Down(&'static str),
|
||||||
Up(&'static str),
|
Up(&'static str),
|
||||||
@@ -13,13 +13,13 @@ pub enum Step {
|
|||||||
DownAt(&'static str, f32, f32),
|
DownAt(&'static str, f32, f32),
|
||||||
/// Right-clicks at an offset from the probe's top-left corner.
|
/// Right-clicks at an offset from the probe's top-left corner.
|
||||||
RightAt(&'static str, f32, f32),
|
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),
|
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),
|
Swipe(&'static str, f32),
|
||||||
UpAt(&'static str, f32, f32),
|
UpAt(&'static str, f32, f32),
|
||||||
Key(&'static str),
|
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),
|
Type(&'static str),
|
||||||
Wait(u64),
|
Wait(u64),
|
||||||
Shot(&'static str),
|
Shot(&'static str),
|
||||||
@@ -28,14 +28,14 @@ pub enum Step {
|
|||||||
CloseWindow(&'static str),
|
CloseWindow(&'static str),
|
||||||
/// Sends a keystroke to a window a demo opened.
|
/// Sends a keystroke to a window a demo opened.
|
||||||
KeyWindow(&'static str, &'static str),
|
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),
|
CloseNative(&'static str),
|
||||||
/// Fails unless the window is gone.
|
/// Fails unless the window is gone.
|
||||||
ExpectClosed(&'static str),
|
ExpectClosed(&'static str),
|
||||||
/// Photographs this app's frontmost popup, such as the share picker.
|
/// Photographs this app's frontmost popup, such as the share picker.
|
||||||
ShotPopup(&'static str),
|
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),
|
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,
|
CancelPanel,
|
||||||
}
|
}
|
||||||
+23
-10
@@ -6,10 +6,11 @@ use gpui::{
|
|||||||
Resource, SharedString, Styled, Window, div, prelude::*,
|
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 {
|
pub struct Story {
|
||||||
wanted: SharedString,
|
wanted: SharedString,
|
||||||
met: RefCell<Vec<SharedString>>,
|
met: RefCell<Vec<SharedString>>,
|
||||||
|
live: RefCell<Vec<SharedString>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Global for Story {}
|
impl Global for Story {}
|
||||||
@@ -19,6 +20,7 @@ impl Story {
|
|||||||
Self {
|
Self {
|
||||||
wanted,
|
wanted,
|
||||||
met: RefCell::default(),
|
met: RefCell::default(),
|
||||||
|
live: RefCell::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +28,12 @@ impl Story {
|
|||||||
*self.wanted == *title || *self.wanted == slug(title)
|
*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> {
|
pub fn missing(&self, page: &str) -> Option<String> {
|
||||||
let met = self.met.borrow();
|
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() {
|
match met.iter().filter(|title| self.answers(title)).count() {
|
||||||
1 => None,
|
1 => None,
|
||||||
0 => Some(format!(
|
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 {
|
pub fn slug(title: &str) -> String {
|
||||||
let mut slug = String::new();
|
let mut slug = String::new();
|
||||||
for c in title.chars() {
|
for c in title.chars() {
|
||||||
@@ -54,7 +67,7 @@ pub fn slug(title: &str) -> String {
|
|||||||
slug.trim_end_matches('-').to_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 {
|
pub fn section(title: impl Into<SharedString>, note: impl Into<SharedString>, cx: &App) -> Section {
|
||||||
let title = title.into();
|
let title = title.into();
|
||||||
let theme = cx.theme();
|
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 {
|
pub struct Section {
|
||||||
div: Div,
|
div: Div,
|
||||||
shown: bool,
|
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 {
|
pub fn resource(path: impl Into<SharedString>) -> Resource {
|
||||||
let path = path.into();
|
let path = path.into();
|
||||||
if cfg!(target_family = "wasm") {
|
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"
|
"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.";
|
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> {
|
pub fn web_note(text: &'static str, cx: &App) -> Option<AnyElement> {
|
||||||
cfg!(target_family = "wasm").then(|| blocked(text, cx).into_any_element())
|
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 {
|
pub fn native_only(element: impl IntoElement, note: &'static str, cx: &App) -> AnyElement {
|
||||||
match web_note(note, cx) {
|
match web_note(note, cx) {
|
||||||
Some(note) => note,
|
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 {
|
pub fn own_window(button: impl IntoElement, cx: &App) -> AnyElement {
|
||||||
native_only(
|
native_only(
|
||||||
button,
|
button,
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ use crate::{
|
|||||||
shell::{Choice, Gallery},
|
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");
|
const HEBREW: &[u8] = include_bytes!("fonts/NotoSansHebrew-Regular.ttf");
|
||||||
|
|
||||||
thread_local! {
|
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) };
|
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) };
|
static EARLY: Cell<Option<Choice>> = const { Cell::new(None) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ fn read(search: &str) -> Result<Start> {
|
|||||||
Ok(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)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn start() {
|
pub fn start() {
|
||||||
gpui_platform::web_init();
|
gpui_platform::web_init();
|
||||||
@@ -88,7 +88,7 @@ pub fn start() {
|
|||||||
..Start::default()
|
..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(
|
let platform = Rc::new(WebPlatform::new_with_backend_and_font_fallback(
|
||||||
false,
|
false,
|
||||||
WebBackendPreference::Auto,
|
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]
|
#[wasm_bindgen]
|
||||||
pub fn set_theme(theme: &str) -> Result<(), JsError> {
|
pub fn set_theme(theme: &str) -> Result<(), JsError> {
|
||||||
let choice = choice(theme).map_err(|error| JsError::new(&format!("{error:#}")))?;
|
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*\(")
|
CALL = re.compile(r"(?<![\w.])(?:(\w+)::)?(\w+)\s*\(")
|
||||||
CHAIN = re.compile(r"\s*\.\s*\w+\s*(?:::<[^>]*>\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 = {
|
SHOWN = {
|
||||||
("forms", "Form"): ["FormLabel", "FormError"],
|
("forms", "Form"): ["FormLabel", "FormError"],
|
||||||
("tables", "DataTable"): ["Sparkline"],
|
("tables", "DataTable"): ["Sparkline"],
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/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
|
set -eu
|
||||||
out=$(mkdir -p "${1:?usage: scripts/web.sh <out dir>}" && cd "$1" && pwd)
|
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 /}"
|
: "${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
|
exit 1
|
||||||
}
|
}
|
||||||
command -v wasm-opt >/dev/null || { echo "wasm-opt needed: brew install binaryen, or npm i -g binaryen" >&2; 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
|
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="${CARGO_TARGET_DIR:-target}/wasm32-unknown-unknown/web/examples/gallery.wasm"
|
||||||
wasm-bindgen "$wasm" --out-dir "$out" --out-name gallery --target web --no-typescript \
|
wasm-bindgen "$wasm" --out-dir "$out" --out-name gallery --target web --no-typescript \
|
||||||
--remove-name-section --remove-producers-section
|
--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 \
|
wasm-opt -Oz --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext \
|
||||||
--enable-mutable-globals --enable-reference-types --enable-multivalue \
|
--enable-mutable-globals --enable-reference-types --enable-multivalue \
|
||||||
"$out/gallery_bg.wasm" -o "$out/gallery_bg.wasm"
|
"$out/gallery_bg.wasm" -o "$out/gallery_bg.wasm"
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ impl CodeEditor {
|
|||||||
div()
|
div()
|
||||||
.w(advance * digits as f32)
|
.w(advance * digits as f32)
|
||||||
.text_right()
|
.text_right()
|
||||||
|
// Snapped to device pixels, the box can run short.
|
||||||
|
.whitespace_nowrap()
|
||||||
.when(current, |number| number.text_color(colors.fg))
|
.when(current, |number| number.text_color(colors.fg))
|
||||||
.child(number),
|
.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-[", Fold, context),
|
||||||
KeyBinding::new("secondary-alt-]", Unfold, 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") {
|
if !cfg!(target_family = "wasm") {
|
||||||
bindings.push(KeyBinding::new("secondary-v", Paste, context));
|
bindings.push(KeyBinding::new("secondary-v", Paste, context));
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -112,6 +112,8 @@ pub struct CodeEditor {
|
|||||||
pub(crate) metrics: Metrics,
|
pub(crate) metrics: Metrics,
|
||||||
pub(crate) dragging: bool,
|
pub(crate) dragging: bool,
|
||||||
pub(crate) caret_on: 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) chat_field: gpui::Entity<TextInput>,
|
||||||
pub(crate) frame: Frame,
|
pub(crate) frame: Frame,
|
||||||
blink_epoch: u64,
|
blink_epoch: u64,
|
||||||
@@ -133,8 +135,12 @@ impl CodeEditor {
|
|||||||
let chat_field =
|
let chat_field =
|
||||||
cx.new(|cx| TextInput::new(window, cx).placeholder("Ask to change this code"));
|
cx.new(|cx| TextInput::new(window, cx).placeholder("Ask to change this code"));
|
||||||
let subscriptions = vec![
|
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| {
|
cx.on_blur(&focus, window, |editor, _, cx| {
|
||||||
|
editor.focused = false;
|
||||||
editor.dragging = false;
|
editor.dragging = false;
|
||||||
editor._blink = None;
|
editor._blink = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -178,6 +184,7 @@ impl CodeEditor {
|
|||||||
metrics: Metrics::default(),
|
metrics: Metrics::default(),
|
||||||
dragging: false,
|
dragging: false,
|
||||||
caret_on: true,
|
caret_on: true,
|
||||||
|
focused: false,
|
||||||
chat_field,
|
chat_field,
|
||||||
frame: Frame::default(),
|
frame: Frame::default(),
|
||||||
blink_epoch: 0,
|
blink_epoch: 0,
|
||||||
@@ -462,7 +469,7 @@ impl CodeEditor {
|
|||||||
self.blink_epoch += 1;
|
self.blink_epoch += 1;
|
||||||
let epoch = self.blink_epoch;
|
let epoch = self.blink_epoch;
|
||||||
let still = cx.theme().reduced_motion;
|
let still = cx.theme().reduced_motion;
|
||||||
self._blink = (!still).then(|| {
|
self._blink = (!still && self.focused).then(|| {
|
||||||
cx.spawn(async move |editor, cx| {
|
cx.spawn(async move |editor, cx| {
|
||||||
loop {
|
loop {
|
||||||
cx.background_executor().timer(BLINK).await;
|
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::{
|
use gpui::{
|
||||||
ClipboardItem, Entity, EntityInputHandler, KeyUpEvent, Keystroke, TestAppContext,
|
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(CONTEXT)),
|
||||||
KeyBinding::new("secondary-enter", Submit, Some(FORM_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") {
|
if !cfg!(target_family = "wasm") {
|
||||||
bindings.push(KeyBinding::new("secondary-v", Paste, Some(CONTEXT)));
|
bindings.push(KeyBinding::new("secondary-v", Paste, Some(CONTEXT)));
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-10
@@ -1,6 +1,9 @@
|
|||||||
use std::rc::Rc;
|
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::{
|
use super::{
|
||||||
formula::filled,
|
formula::filled,
|
||||||
@@ -283,15 +286,24 @@ pub(crate) fn keys(
|
|||||||
sheet.read(cx),
|
sheet.read(cx),
|
||||||
))),
|
))),
|
||||||
("v", true) => {
|
("v", true) => {
|
||||||
let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else {
|
// The web reads its clipboard only through the async reader.
|
||||||
log::info!("grid: the clipboard holds no text to paste");
|
let read = cx.read_from_clipboard_async();
|
||||||
return;
|
let on_change = face.on_change.clone();
|
||||||
};
|
window
|
||||||
let edits = pasted(&text, (row, col), (rows, cols));
|
.spawn(cx, async move |cx| {
|
||||||
log::info!("grid: pasted {} cells", edits.len());
|
let item = read.await.map_err(|error| anyhow!("{error:?}"))?;
|
||||||
if let Some(on_change) = &face.on_change {
|
let Some(text) = item.and_then(|item| item.text()) else {
|
||||||
on_change(&edits, window, cx);
|
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| {
|
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,
|
Styled, TestAppContext, VisualTestContext, Window, div, point, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod clipboard;
|
||||||
mod columns;
|
mod columns;
|
||||||
|
|
||||||
use super::{Column, DataTable, FilterBuilder, FilterRule, Row};
|
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)
|
(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;
|
struct Unset;
|
||||||
|
|
||||||
impl Index<usize> for 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>> {}
|
pub(crate) trait Set: Index<usize, Output = Option<Rgb>> {}
|
||||||
|
|
||||||
impl<T: Index<usize, Output = Option<Rgb>>> Set for T {}
|
impl<T: Index<usize, Output = Option<Rgb>>> Set for T {}
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ impl Theme {
|
|||||||
Self::fade_to_target(cx);
|
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) {
|
pub fn set_mode_now(mode: Mode, cx: &mut App) {
|
||||||
log::info!("theme: mode -> {mode:?}, at once");
|
log::info!("theme: mode -> {mode:?}, at once");
|
||||||
let theme = cx.global_mut::<Theme>();
|
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));
|
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]
|
#[gpui::test]
|
||||||
fn a_mode_set_at_once_skips_the_fade(cx: &mut TestAppContext) {
|
fn a_mode_set_at_once_skips_the_fade(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
|||||||
Reference in new issue
Block a user