EventEditor puts an event's parts in a form: its title, whole days or hours, where, its color, how it repeats, its reminders and notes. Save waits for a title and an end after the start and hands on an EventDraft; Cancel and Delete show when the owner handles them. A new draft from the owner starts the editor over. RecurrenceEditor sets a rule after RFC 5545: daily, weekly on its days, monthly or yearly, every so many, until a day or a count. A line reads the rule back with the next days it falls on. ReminderPicker keeps reminders from a fixed list, soonest first, and Add brings the nearest one not yet set. AvailabilityPicker counts the open slots of each day and lists the chosen day's, around what is busy, inside working hours and after now. FormField filled its column with a percent width, which taffy 0.9 leaves at content width when a block measures the column first and the width comes out the same: at 280px the editor's fields shrank to their labels. It now fills with min_w_full, as new roots do; older roots move over in T44. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
4.8 KiB
Rust
170 lines
4.8 KiB
Rust
use gpui::{
|
|
AppContext as _, Context, Entity, InteractiveElement, IntoElement, KeyUpEvent, Keystroke,
|
|
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, div, px,
|
|
};
|
|
|
|
use super::setup;
|
|
use crate::{
|
|
buttons::Button,
|
|
forms::{FieldArray, Form, FormField, Input, InputEvent, TextInput},
|
|
};
|
|
|
|
struct Signup {
|
|
name: Entity<TextInput>,
|
|
sent: usize,
|
|
field_submits: usize,
|
|
}
|
|
|
|
impl Render for Signup {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let view = cx.entity();
|
|
Form::new("signup")
|
|
.on_submit(move |_, cx| {
|
|
view.update(cx, |view, cx| {
|
|
view.sent += 1;
|
|
cx.notify();
|
|
})
|
|
})
|
|
.child(
|
|
FormField::new("signup-name", "Name")
|
|
.required()
|
|
.child(Input::new(&self.name)),
|
|
)
|
|
.child(Button::new("signup-save", "Save"))
|
|
}
|
|
}
|
|
|
|
fn signup(cx: &mut TestAppContext) -> (Entity<Signup>, &mut VisualTestContext) {
|
|
setup(cx);
|
|
cx.add_window_view(|window, cx| {
|
|
let name = cx.new(|cx| TextInput::new(window, cx));
|
|
cx.subscribe(&name, |view: &mut Signup, _, event, _| {
|
|
if *event == InputEvent::Submit {
|
|
view.field_submits += 1;
|
|
}
|
|
})
|
|
.detach();
|
|
Signup {
|
|
name,
|
|
sent: 0,
|
|
field_submits: 0,
|
|
}
|
|
})
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn cmd_enter_in_a_field_submits_the_form_once(cx: &mut TestAppContext) {
|
|
let (view, cx) = signup(cx);
|
|
cx.update(|window, _| window.focus_next());
|
|
cx.simulate_input("Ada");
|
|
cx.simulate_keystrokes("cmd-enter");
|
|
let counts = view.read_with(cx, |view, _| (view.sent, view.field_submits));
|
|
assert_eq!(counts, (1, 0));
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn cmd_enter_on_any_control_in_a_form_submits_it(cx: &mut TestAppContext) {
|
|
let (view, cx) = signup(cx);
|
|
cx.update(|window, _| {
|
|
window.focus_next();
|
|
window.focus_next();
|
|
});
|
|
cx.simulate_keystrokes("cmd-enter");
|
|
assert_eq!(
|
|
view.read_with(cx, |view, _| view.sent),
|
|
1,
|
|
"the button holds focus"
|
|
);
|
|
}
|
|
|
|
struct Rows {
|
|
rows: Vec<&'static str>,
|
|
}
|
|
|
|
impl Render for Rows {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (grow, shrink) = (cx.entity(), cx.entity());
|
|
self.rows.iter().fold(
|
|
FieldArray::new("rows")
|
|
.on_add(move |_, cx| {
|
|
grow.update(cx, |view, cx| {
|
|
view.rows.push("new");
|
|
cx.notify();
|
|
})
|
|
})
|
|
.on_remove(move |ix, _, cx| {
|
|
shrink.update(cx, |view, cx| {
|
|
view.rows.remove(ix);
|
|
cx.notify();
|
|
})
|
|
}),
|
|
|array, row| array.row(div().child(*row)),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn press(key: &str, cx: &mut VisualTestContext) {
|
|
cx.simulate_keystrokes(key);
|
|
cx.simulate_event(KeyUpEvent {
|
|
keystroke: Keystroke::parse(key).unwrap(),
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn field_array_buttons_remove_their_own_row_and_add_one(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let (view, cx) = cx.add_window_view(|_, _| Rows {
|
|
rows: vec!["a", "b", "c"],
|
|
});
|
|
cx.update(|window, _| {
|
|
window.focus_next();
|
|
window.focus_next();
|
|
});
|
|
press("space", cx);
|
|
assert_eq!(view.read_with(cx, |view, _| view.rows.clone()), ["a", "c"]);
|
|
cx.update(|window, _| {
|
|
window.blur();
|
|
for _ in 0..3 {
|
|
window.focus_next();
|
|
}
|
|
});
|
|
press("space", cx);
|
|
assert_eq!(
|
|
view.read_with(cx, |view, _| view.rows.clone()),
|
|
["a", "c", "new"]
|
|
);
|
|
}
|
|
|
|
/// A field over a long line in a column that a padded block measures by content.
|
|
struct Narrow;
|
|
|
|
impl Render for Narrow {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
let control = div().debug_selector(|| "narrow-control".into()).h_2();
|
|
div().w(px(280.)).flex().flex_col().child(
|
|
div().p_5().child(
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.child(FormField::new("narrow", "Name").child(control))
|
|
.child("A line long enough to wrap in a narrow card, so the column fills it."),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_field_fills_a_column_its_block_measures_by_content(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let (_, cx) = cx.add_window_view(|_, _| Narrow);
|
|
cx.run_until_parked();
|
|
let control = cx
|
|
.debug_bounds("narrow-control")
|
|
.expect("the control draws");
|
|
assert_eq!(
|
|
control.size.width,
|
|
px(240.),
|
|
"the field spans the card inside its padding"
|
|
);
|
|
}
|