T44d Web · Hidden builders start nothing, fields blink only in focus, grids paste with Control

This commit is contained in:
ZacharyZhang-NY committed 2026-09-29 23:02:37 -04:00
1 parent 81adec5c47
commit dcdd46038e
8 files changed
+107 -55

No files matched your search

+2 -2
View File
@@ -179,8 +179,8 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- gpui clears `debug_bounds` each frame: a test sees an element leave as well as appear.
- 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. 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.
- 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 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 starts work on its own, a clock, a load or a shell, 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`, 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.
+1 -1
View File
@@ -195,7 +195,7 @@ fn main() -> Result<()> {
page,
capture,
} = parse_args()?;
// A script aims across its page; cards and stories hold part.
// A script aims across its page; stories hold part.
let scripted = start.narrow.is_none() && start.story.is_none();
gpui_platform::application()
.with_assets(Assets)
+52 -42
View File
@@ -26,7 +26,7 @@ use gpui::{
use super::loading::later;
use crate::{
probe::probe,
ui::{keep, row, section, set, specimen, specimens},
ui::{keep, live, row, section, set, specimen, specimens},
};
const STEPS: [f32; 5] = [0.0, 0.35, 0.72, 1.0, 0.5];
@@ -137,6 +137,56 @@ struct Report {
}
pub fn suspense(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let section = section(
"Suspense / AsyncView",
"A spinner while a value loads off the main thread, then the value as it fades in, or the error with Try again.",
cx,
);
if !live("Suspense / AsyncView", cx) {
return section;
}
let (view, breaker) = report(window, cx);
let theme = cx.theme();
section
.child(
div()
.w_128()
.min_h_40()
.flex()
.items_center()
.justify_center()
.px_4()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.child(view.clone()),
)
.child(
row()
.child(
Button::new("report-reload", "Reload")
.variant(ButtonVariant::Ghost)
.on_click({
let view = view.clone();
move |_, _, cx| view.update(cx, |view, cx| view.reload(cx))
}),
)
.child(
Button::new("report-fail", "Reload and fail")
.variant(ButtonVariant::Ghost)
.on_click({
let view = view.clone();
move |_, _, cx| {
breaker.store(true, Ordering::Relaxed);
view.update(cx, |view, cx| view.reload(cx))
}
}),
),
)
}
/// The report's view and its fail switch.
fn report(window: &mut Window, cx: &mut App) -> (Entity<AsyncView<String>>, Arc<AtomicBool>) {
let report = keep(
"report",
|| Report {
@@ -171,47 +221,7 @@ pub fn suspense(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
)
});
let breaker = report.read(cx).fail.clone();
let theme = cx.theme();
section(
"Suspense / AsyncView",
"A spinner while a value loads off the main thread, then the value as it fades in, or the error with Try again.",
cx,
)
.child(
div()
.w_128()
.min_h_40()
.flex()
.items_center()
.justify_center()
.px_4()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.child(view.clone()),
)
.child(
row()
.child(
Button::new("report-reload", "Reload")
.variant(ButtonVariant::Ghost)
.on_click({
let view = view.clone();
move |_, _, cx| view.update(cx, |view, cx| view.reload(cx))
}),
)
.child(
Button::new("report-fail", "Reload and fail")
.variant(ButtonVariant::Ghost)
.on_click({
let view = view.clone();
move |_, _, cx| {
breaker.store(true, Ordering::Relaxed);
view.update(cx, |view, cx| view.reload(cx))
}
}),
),
)
(view, breaker)
}
pub fn refresh(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
+8 -1
View File
@@ -45,7 +45,14 @@ fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx,
);
#[cfg(not(target_family = "wasm"))]
return section.child(live::shell(window, cx));
return if crate::ui::live(
"Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink",
cx,
) {
section.child(live::shell(window, cx))
} else {
section
};
#[cfg(target_family = "wasm")]
{
let _ = window;
+7 -7
View File
@@ -79,7 +79,7 @@ def literal(text, at):
def functions(folder):
"""Every `fn` in a page's folder, by module and name, with its body's span."""
"""Every `fn` in a page's folder, with its body's span."""
found = {}
for file in sorted(folder.rglob("*.rs")):
module = file.stem
@@ -100,7 +100,7 @@ def functions(folder):
def close(bare, at):
"""The offset past the bracket that closes the one at `at`."""
"""The offset past the bracket that closes `at`."""
depth = 0
for k in range(at, len(bare)):
depth += {"(": 1, "[": 1, "{": 1, ")": -1, "]": -1, "}": -1}.get(bare[k], 0)
@@ -109,7 +109,7 @@ def close(bare, at):
def sections(folder):
"""Section titles in drawing order; fails on one never drawn or drawn inside another."""
"""Section titles in drawing order; stray or nested ones fail."""
fns = functions(folder)
order, seen, opens = [], set(), {}
@@ -120,7 +120,7 @@ def sections(folder):
yield m, (owner, m.group(2)), m.group(2) == "section" and m.group(1) is None
def draws(fn):
"""Whether the fn opens a section, itself or through what it calls."""
"""Whether the fn opens a section, itself or deeper."""
if fn not in opens:
opens[fn] = False
opens[fn] = any(own or (callee in fns and draws(callee)) for _, callee, own in calls(*fn))
@@ -163,7 +163,7 @@ def slug(title):
def entries():
"""The task lists' component names by key, and the ticked entries with a home."""
"""Component names by key, and ticked entries with a home."""
names, homed = {}, []
for file in sorted((ROOT / "tasks").glob("*.md")):
for line in file.read_text().splitlines():
@@ -177,7 +177,7 @@ def entries():
def home(line, aliases):
"""False for a pointer: `A → b::B` lives at B, another entry."""
"""False for a pointer: `A → b::B` lives at B."""
if " → " not in line:
return True
target = re.search(r" → `([\w:]+)", line)
@@ -189,7 +189,7 @@ def key(name):
def components(title, extra, known):
"""The task lists' names for what the title and `extra` name."""
"""The task lists' names for the title's and `extra`'s names."""
names = []
for part in re.sub(r"\s*\([^)]*\)", "", title).split(" · "):
for name in part.split(" → ")[0].split(" / ") + extra:
+28
View File
@@ -330,3 +330,31 @@ fn shift_enter_breaks_only_a_field_of_many_lines(cx: &mut TestAppContext) {
"a single line stays single"
);
}
#[gpui::test]
fn only_a_focused_field_blinks(cx: &mut TestAppContext) {
let (fields, cx) = open(1, false, cx);
let field = fields[0].clone();
let redraws = std::rc::Rc::new(std::cell::Cell::new(0));
let counted = redraws.clone();
let _count = cx.update(|_, cx| cx.observe(&field, move |_, _| counted.set(counted.get() + 1)));
field.update(cx, |input, cx| {
input.set_text("seeded", cx);
let word = 0..4;
input.select(word, cx);
});
cx.run_until_parked();
redraws.set(0);
cx.executor()
.advance_clock(std::time::Duration::from_secs(3));
cx.run_until_parked();
assert_eq!(redraws.get(), 0, "an unfocused field keeps still");
cx.update(|window, _| window.activate_window());
focus(&field, cx);
cx.run_until_parked();
redraws.set(0);
cx.executor()
.advance_clock(std::time::Duration::from_secs(3));
cx.run_until_parked();
assert!(redraws.get() > 2, "a focused one blinks");
}
+6 -1
View File
@@ -69,6 +69,8 @@ pub struct TextInput {
pub(crate) scroll: Point<Pixels>,
selecting: bool,
caret_on: bool,
/// Blink runs only while focused, or unseen carets redraw forever.
focused: bool,
blink_epoch: u64,
_blink: Option<Task<()>>,
_subscriptions: Vec<Subscription>,
@@ -87,10 +89,12 @@ impl TextInput {
let focus = cx.focus_handle().tab_stop(true);
let subscriptions = vec![
cx.on_focus(&focus, window, |input, _, cx| {
input.focused = true;
input.restart_blink(cx);
cx.emit(InputEvent::Focus);
}),
cx.on_blur(&focus, window, |input, _, cx| {
input.focused = false;
input.selecting = false;
input._blink = None;
cx.emit(InputEvent::Blur);
@@ -118,6 +122,7 @@ impl TextInput {
scroll: Point::default(),
selecting: false,
caret_on: true,
focused: false,
blink_epoch: 0,
_blink: None,
_subscriptions: subscriptions,
@@ -367,7 +372,7 @@ impl TextInput {
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 |input, cx| {
loop {
cx.background_executor().timer(BLINK).await;
+3 -1
View File
@@ -256,7 +256,9 @@ pub(crate) fn keys(
});
return;
}
match (stroke.key.as_str(), held.platform) {
// Browsers on a Mac send Cmd; wasm's secondary is Control.
let command = held.secondary() || (cfg!(target_family = "wasm") && held.platform);
match (stroke.key.as_str(), command) {
("escape", _) => sheet.update(cx, |sheet, cx| {
sheet.anchor = sheet.cursor;
cx.notify();