chore: clean up
This commit is contained in:
@@ -66,7 +66,10 @@ GPUI is a UI framework which also provides primitives for state and concurrency
|
|||||||
- Keep `render` declarative; use `.when(...)` / `.when_some(...)` for small conditional refinements.
|
- Keep `render` declarative; use `.when(...)` / `.when_some(...)` for small conditional refinements.
|
||||||
- Controlled components: the owner passes the current value, callbacks report intent, the owner updates state and calls `cx.notify()`. No second source of truth.
|
- Controlled components: the owner passes the current value, callbacks report intent, the owner updates state and calls `cx.notify()`. No second source of truth.
|
||||||
- Call `cx.notify()` after a mutation that changes rendering; notify the narrowest owning entity. Never notify from `render`.
|
- Call `cx.notify()` after a mutation that changes rendering; notify the narrowest owning entity. Never notify from `render`.
|
||||||
- Use `cx.emit(...)` for semantic events (declare `impl EventEmitter<EventType>`), `cx.subscribe(...)` / `cx.observe(...)` for reactions, and store returned `Subscription`s in the owner (commonly `_subscriptions: Vec<Subscription>`).
|
- `render` is side-effect free: no `cx.notify()`, no `entity.update(...)`, no writing derived text or state back into other entities during render. A write there forces another frame, and a write that emits an event re-enters the event handler — an unbounded redraw loop. Reconcile state in event handlers instead, with silent setters (`set_value_silent`-style) so a control's own write does not re-enter the events it mirrors.
|
||||||
|
- Use `cx.emit(...)` for semantic events (declare `impl EventEmitter<EventType>`), `cx.subscribe(...)` / `cx.observe(...)` for reactions, and store returned `Subscription`s in the owner (commonly `_subscriptions: Vec<Subscription>`). Never `.detach()` a component's own subscription — detached subscriptions outlive the component and fire into dropped state; detach is only for app-lifetime wiring registered once at startup.
|
||||||
|
- To take a key a focused child has already bound, add bindings in a deeper context (`"Parent > Child"` predicate) registered after the child's — gpui breaks equal-depth ties by later registration. Do not intercept keystrokes globally for component-scoped keys; an interceptor runs for every keystroke in every window.
|
||||||
|
- Animations schedule frames that re-render the whole window. Continuous animations (`repeat()`) must cap their frame rate (`with_max_fps`); skip entry animations on surfaces that redraw with typing (popups over text fields), where each frame is a full window redraw.
|
||||||
- Use stable domain-derived `ElementId`s (`("delete-project", project.id)`); never list indexes or freshly generated random IDs for reorderable content.
|
- Use stable domain-derived `ElementId`s (`("delete-project", project.id)`); never list indexes or freshly generated random IDs for reorderable content.
|
||||||
- `cx.listener(...)` adapts event handlers to update the current entity.
|
- `cx.listener(...)` adapts event handlers to update the current entity.
|
||||||
- Actions: `actions!(namespace, [Action])` for data-free actions, the `Action` derive otherwise; register `on_action` on the focused region with a matching `key_context`.
|
- Actions: `actions!(namespace, [Action])` for data-free actions, the `Action` derive otherwise; register `on_action` on the focused region with a matching `key_context`.
|
||||||
|
|||||||
@@ -191,12 +191,12 @@ impl RenderOnce for Alert {
|
|||||||
.flex_none()
|
.flex_none()
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.p_0p5()
|
.p_0p5()
|
||||||
.rounded(px(4.))
|
.rounded(theme.radius.sm)
|
||||||
.hover(|style| style.bg(muted))
|
.hover(|style| style.bg(muted))
|
||||||
.child(
|
.child(
|
||||||
icon(IconRole::Close)
|
icon(IconRole::Close)
|
||||||
.size(theme.sizes.small.dense_icon_size)
|
.size(theme.sizes.small.dense_icon_size)
|
||||||
.color(foreground.alpha(0.55)),
|
.color(theme.colors.muted_foreground),
|
||||||
)
|
)
|
||||||
.on_click(move |_, _window, cx| on_dismiss(cx)),
|
.on_click(move |_, _window, cx| on_dismiss(cx)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
//! Toasts: succinct, temporary messages at the bottom center of the window
|
//! Toasts: succinct, temporary messages at the bottom center of the window.
|
||||||
//! — Adwaita Toast, the macOS banner. One toast at a time: content, an
|
|
||||||
//! optional action, and a close button; fades in, then fades out.
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -333,12 +331,12 @@ fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful
|
|||||||
.ml_4()
|
.ml_4()
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.p_0p5()
|
.p_0p5()
|
||||||
.rounded(px(4.))
|
.rounded(theme.radius.sm)
|
||||||
.hover(|style| style.bg(muted))
|
.hover(|style| style.bg(muted))
|
||||||
.child(
|
.child(
|
||||||
icon(IconRole::Close)
|
icon(IconRole::Close)
|
||||||
.size(dense_icon_size)
|
.size(dense_icon_size)
|
||||||
.color(foreground.alpha(0.55)),
|
.color(theme.colors.muted_foreground),
|
||||||
)
|
)
|
||||||
.on_click(move |_, _window, cx| {
|
.on_click(move |_, _window, cx| {
|
||||||
toasts.update(cx, |toasts, cx| {
|
toasts.update(cx, |toasts, cx| {
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString,
|
||||||
|
Styled as _, TestAppContext, VisualTestContext, Window, div,
|
||||||
|
};
|
||||||
|
use gpui_native_core::common::v_flex;
|
||||||
|
use gpui_native_core::field::Field;
|
||||||
|
use gpui_native_core::init;
|
||||||
|
use gpui_native_core::input::{Input, InputState};
|
||||||
|
|
||||||
|
struct FieldHost {
|
||||||
|
url: Entity<InputState>,
|
||||||
|
renders: Rc<RefCell<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for FieldHost {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
*self.renders.borrow_mut() += 1;
|
||||||
|
let url_error = self
|
||||||
|
.url
|
||||||
|
.read(cx)
|
||||||
|
.value()
|
||||||
|
.is_empty()
|
||||||
|
.then_some("Required")
|
||||||
|
.map(SharedString::from);
|
||||||
|
|
||||||
|
div().size_full().p_4().child(
|
||||||
|
v_flex().gap_3().child(
|
||||||
|
Field::new("bench-url", "Server URL")
|
||||||
|
.description("Where your patches are sent.")
|
||||||
|
.error(url_error)
|
||||||
|
.child(Input::new("bench-url-input", self.url.clone())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A field page in a real window, with the url input focused.
|
||||||
|
fn field_host() -> (
|
||||||
|
TestAppContext,
|
||||||
|
Entity<InputState>,
|
||||||
|
Rc<RefCell<usize>>,
|
||||||
|
VisualTestContext,
|
||||||
|
) {
|
||||||
|
let cx = TestAppContext::single();
|
||||||
|
cx.update(init);
|
||||||
|
let (url, renders, window) = cx.update(|cx| {
|
||||||
|
let renders = Rc::new(RefCell::new(0));
|
||||||
|
let url = InputState::new(cx);
|
||||||
|
let url_for_host = url.clone();
|
||||||
|
let renders_for_host = renders.clone();
|
||||||
|
let window = cx
|
||||||
|
.open_window(gpui::WindowOptions::default(), |_, cx| {
|
||||||
|
cx.new(|_| FieldHost {
|
||||||
|
url: url_for_host,
|
||||||
|
renders: renders_for_host,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
(url, renders, window)
|
||||||
|
});
|
||||||
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
let focus = url.read(cx).focus_handle();
|
||||||
|
window.focus(&focus, cx);
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
(cx, url, renders, visual)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(visual: &mut VisualTestContext) {
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_and_time(visual: &mut VisualTestContext, text: &str) -> std::time::Duration {
|
||||||
|
let started = Instant::now();
|
||||||
|
for letter in text.chars() {
|
||||||
|
visual.simulate_keystrokes(&letter.to_string());
|
||||||
|
draw(visual);
|
||||||
|
}
|
||||||
|
let elapsed = started.elapsed();
|
||||||
|
println!(
|
||||||
|
"{} keystrokes: {elapsed:?} ({:.2} ms per keystroke)",
|
||||||
|
text.chars().count(),
|
||||||
|
elapsed.as_secs_f64() / text.chars().count() as f64 * 1000.
|
||||||
|
);
|
||||||
|
elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_idle_field_page_schedules_no_frames() {
|
||||||
|
let (_cx, _url, _renders, mut visual) = field_host();
|
||||||
|
let frames = visual.update(|window, cx| window.simulate_next_frame(cx));
|
||||||
|
assert_eq!(
|
||||||
|
frames, 0,
|
||||||
|
"an idle field page must not request animation frames"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_typing_frame_cost() {
|
||||||
|
let (_cx, _url, _renders, mut visual) = field_host();
|
||||||
|
let elapsed = type_and_time(&mut visual, "dsdsadsadadadada");
|
||||||
|
assert!(
|
||||||
|
elapsed.as_millis() < 1000,
|
||||||
|
"typing 16 characters took {elapsed:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ use gpui_native_core::context_menu::ContextMenu;
|
|||||||
use gpui_native_core::dialog::Dialog;
|
use gpui_native_core::dialog::Dialog;
|
||||||
use gpui_native_core::empty_state::EmptyState;
|
use gpui_native_core::empty_state::EmptyState;
|
||||||
use gpui_native_core::field::{Field, FieldVariant};
|
use gpui_native_core::field::{Field, FieldVariant};
|
||||||
use gpui_native_core::input::{Input, InputState, InputStateEvent};
|
use gpui_native_core::input::{Input, InputState};
|
||||||
use gpui_native_core::link::Link;
|
use gpui_native_core::link::Link;
|
||||||
use gpui_native_core::menu::{Menu, MenuItem};
|
use gpui_native_core::menu::{Menu, MenuItem};
|
||||||
use gpui_native_core::popover::Popover;
|
use gpui_native_core::popover::Popover;
|
||||||
@@ -389,7 +389,9 @@ impl Gallery {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
dock.register_panel(
|
dock.register_panel(
|
||||||
PanelDescriptor::new("field", "Field", |_, cx| cx.new(FieldDemo::new).into()),
|
PanelDescriptor::new("field", "Field", |window, cx| {
|
||||||
|
cx.new(|cx| FieldDemo::new(window, cx)).into()
|
||||||
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
dock.register_panel(
|
dock.register_panel(
|
||||||
@@ -822,6 +824,7 @@ struct InputDemo {
|
|||||||
text: Entity<InputState>,
|
text: Entity<InputState>,
|
||||||
password: Entity<InputState>,
|
password: Entity<InputState>,
|
||||||
notes: Entity<InputState>,
|
notes: Entity<InputState>,
|
||||||
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputDemo {
|
impl InputDemo {
|
||||||
@@ -831,10 +834,18 @@ impl InputDemo {
|
|||||||
password.update(cx, |state, cx| state.set_password(true, cx));
|
password.update(cx, |state, cx| state.set_password(true, cx));
|
||||||
let notes = InputState::new(cx);
|
let notes = InputState::new(cx);
|
||||||
notes.update(cx, |state, cx| state.set_multiline(true, cx));
|
notes.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
|
// The panel is a cached view in the dock: the value readouts re-render
|
||||||
|
// only when the inputs it reads are notified.
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.observe(&text, |_, _, cx| cx.notify()),
|
||||||
|
cx.observe(&password, |_, _, cx| cx.notify()),
|
||||||
|
cx.observe(¬es, |_, _, cx| cx.notify()),
|
||||||
|
];
|
||||||
Self {
|
Self {
|
||||||
text,
|
text,
|
||||||
password,
|
password,
|
||||||
notes,
|
notes,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -871,24 +882,25 @@ struct FieldDemo {
|
|||||||
name: Entity<InputState>,
|
name: Entity<InputState>,
|
||||||
notifications: bool,
|
notifications: bool,
|
||||||
sync_on_open: bool,
|
sync_on_open: bool,
|
||||||
_subscription: gpui::Subscription,
|
_subscriptions: (gpui::Subscription, gpui::Subscription),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldDemo {
|
impl FieldDemo {
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let url = InputState::new(cx);
|
let url = InputState::new(cx);
|
||||||
let name = InputState::new_with_text("reya", cx);
|
let name = InputState::new_with_text("reya", cx);
|
||||||
let subscription = cx.subscribe(&url, |_, _, event, cx| {
|
// The panel is a cached view in the dock: it re-renders only when the
|
||||||
if matches!(event, InputStateEvent::TextChanged) {
|
// entities it reads are notified. Both inputs are read at render time
|
||||||
cx.notify();
|
// (validation, error text), so every change must bust the cache.
|
||||||
}
|
let url_observation = cx.observe(&url, |_, _, cx| cx.notify());
|
||||||
});
|
let name_observation = cx.observe(&name, |_, _, cx| cx.notify());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
url,
|
url,
|
||||||
name,
|
name,
|
||||||
notifications: false,
|
notifications: false,
|
||||||
sync_on_open: true,
|
sync_on_open: true,
|
||||||
_subscription: subscription,
|
_subscriptions: (url_observation, name_observation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1036,6 +1048,7 @@ struct ComboboxDemo {
|
|||||||
branch: Entity<ComboboxState<SharedString>>,
|
branch: Entity<ComboboxState<SharedString>>,
|
||||||
label: Entity<ComboboxState<SharedString>>,
|
label: Entity<ComboboxState<SharedString>>,
|
||||||
label_created: Option<SharedString>,
|
label_created: Option<SharedString>,
|
||||||
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComboboxDemo {
|
impl ComboboxDemo {
|
||||||
@@ -1087,10 +1100,17 @@ impl ComboboxDemo {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
let label = cx.new(|cx| ComboboxState::new(label_builder, window, cx));
|
let label = cx.new(|cx| ComboboxState::new(label_builder, window, cx));
|
||||||
|
// The panel is a cached view in the dock: the value readouts re-render
|
||||||
|
// only when the picker states it reads are notified.
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.observe(&branch, |_, _, cx| cx.notify()),
|
||||||
|
cx.observe(&label, |_, _, cx| cx.notify()),
|
||||||
|
];
|
||||||
Self {
|
Self {
|
||||||
branch,
|
branch,
|
||||||
label,
|
label,
|
||||||
label_created: None,
|
label_created: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2835,7 +2855,15 @@ fn main() {
|
|||||||
gallery.downgrade(),
|
gallery.downgrade(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
dock.open_panel("welcome", window, cx);
|
// TEMPORARY perf probe: open the field panel under the
|
||||||
|
// autoplay env var so the probe types into a real window.
|
||||||
|
let first = if std::env::var("GPUI_FIELD_AUTOPLAY").is_ok() {
|
||||||
|
eprintln!("[field-probe] autoplay: opening field panel");
|
||||||
|
"field"
|
||||||
|
} else {
|
||||||
|
"welcome"
|
||||||
|
};
|
||||||
|
dock.open_panel(first, window, cx);
|
||||||
});
|
});
|
||||||
gallery
|
gallery
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user