T44d Web · Hidden builders start nothing, fields blink only in focus, grids paste with Control
This commit is contained in:
8 files changed
+107
-55
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<> {
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user