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. - 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. - 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 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 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. - 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). - 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.
+1 -1
View File
@@ -195,7 +195,7 @@ fn main() -> Result<()> {
page, page,
capture, capture,
} = parse_args()?; } = 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(); let scripted = start.narrow.is_none() && start.story.is_none();
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
+52 -42
View File
@@ -26,7 +26,7 @@ use gpui::{
use super::loading::later; use super::loading::later;
use crate::{ use crate::{
probe::probe, 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]; 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<> { 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( let report = keep(
"report", "report",
|| 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 breaker = report.read(cx).fail.clone();
let theme = cx.theme(); (view, breaker)
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))
}
}),
),
)
} }
pub fn refresh(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { 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, cx,
); );
#[cfg(not(target_family = "wasm"))] #[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")] #[cfg(target_family = "wasm")]
{ {
let _ = window; let _ = window;
+7 -7
View File
@@ -79,7 +79,7 @@ def literal(text, at):
def functions(folder): 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 = {} found = {}
for file in sorted(folder.rglob("*.rs")): for file in sorted(folder.rglob("*.rs")):
module = file.stem module = file.stem
@@ -100,7 +100,7 @@ def functions(folder):
def close(bare, at): 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 depth = 0
for k in range(at, len(bare)): for k in range(at, len(bare)):
depth += {"(": 1, "[": 1, "{": 1, ")": -1, "]": -1, "}": -1}.get(bare[k], 0) depth += {"(": 1, "[": 1, "{": 1, ")": -1, "]": -1, "}": -1}.get(bare[k], 0)
@@ -109,7 +109,7 @@ def close(bare, at):
def sections(folder): 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) fns = functions(folder)
order, seen, opens = [], set(), {} 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 yield m, (owner, m.group(2)), m.group(2) == "section" and m.group(1) is None
def draws(fn): 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: if fn not in opens:
opens[fn] = False opens[fn] = False
opens[fn] = any(own or (callee in fns and draws(callee)) for _, callee, own in calls(*fn)) 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(): 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 = {}, [] names, homed = {}, []
for file in sorted((ROOT / "tasks").glob("*.md")): for file in sorted((ROOT / "tasks").glob("*.md")):
for line in file.read_text().splitlines(): for line in file.read_text().splitlines():
@@ -177,7 +177,7 @@ def entries():
def home(line, aliases): 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: if " → " not in line:
return True return True
target = re.search(r" → `([\w:]+)", line) target = re.search(r" → `([\w:]+)", line)
@@ -189,7 +189,7 @@ def key(name):
def components(title, extra, known): 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 = [] names = []
for part in re.sub(r"\s*\([^)]*\)", "", title).split(" · "): for part in re.sub(r"\s*\([^)]*\)", "", title).split(" · "):
for name in part.split(" → ")[0].split(" / ") + extra: 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" "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>, pub(crate) scroll: Point<Pixels>,
selecting: bool, selecting: bool,
caret_on: bool, caret_on: bool,
/// Blink runs only while focused, or unseen carets redraw forever.
focused: bool,
blink_epoch: u64, blink_epoch: u64,
_blink: Option<Task<()>>, _blink: Option<Task<()>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
@@ -87,10 +89,12 @@ impl TextInput {
let focus = cx.focus_handle().tab_stop(true); let focus = cx.focus_handle().tab_stop(true);
let subscriptions = vec![ let subscriptions = vec![
cx.on_focus(&focus, window, |input, _, cx| { cx.on_focus(&focus, window, |input, _, cx| {
input.focused = true;
input.restart_blink(cx); input.restart_blink(cx);
cx.emit(InputEvent::Focus); cx.emit(InputEvent::Focus);
}), }),
cx.on_blur(&focus, window, |input, _, cx| { cx.on_blur(&focus, window, |input, _, cx| {
input.focused = false;
input.selecting = false; input.selecting = false;
input._blink = None; input._blink = None;
cx.emit(InputEvent::Blur); cx.emit(InputEvent::Blur);
@@ -118,6 +122,7 @@ impl TextInput {
scroll: Point::default(), scroll: Point::default(),
selecting: false, selecting: false,
caret_on: true, caret_on: true,
focused: false,
blink_epoch: 0, blink_epoch: 0,
_blink: None, _blink: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
@@ -367,7 +372,7 @@ impl TextInput {
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 |input, cx| { cx.spawn(async move |input, cx| {
loop { loop {
cx.background_executor().timer(BLINK).await; cx.background_executor().timer(BLINK).await;
+3 -1
View File
@@ -256,7 +256,9 @@ pub(crate) fn keys(
}); });
return; 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| { ("escape", _) => sheet.update(cx, |sheet, cx| {
sheet.anchor = sheet.cursor; sheet.anchor = sheet.cursor;
cx.notify(); cx.notify();