T44d Web · Hidden builders start nothing, fields blink only in focus, grids paste with Control
This commit is contained in:
1 parent
81adec5c47
commit
dcdd46038e
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.
|
- 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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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<> {
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user