Misc · Asking and learning: Submit never rests; radio labels wrap

Survey's Submit asks for any missing answer instead of resting, so no
untick takes its focus handle out of the tree. Flashcards turn and go
round, and a quiz pick clears its ask, through methods the tests reach;
the poll's rows show which one holds the vote. forms::Radio's label
wraps in its own block, as Checkbox's does, so a long one stays in a
narrow box.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-28 03:11:52 -04:00
1 parent 86c9de7d0a
commit 46aa381eee
9 files changed
+136 -56

No files matched your search

+7 -1
View File
@@ -143,7 +143,13 @@ impl RenderOnce for Radio {
})
})
.child(radio_mark(self.on, focused, self.disabled, changes, cx))
.children(self.label)
.children(self.label.map(|label| {
div()
.debug_selector(|| "radio-label".into())
.flex_1()
.min_w_0()
.child(label)
}))
}
}
-21
View File
@@ -475,24 +475,3 @@ fn a_long_choice_keeps_its_list_inside_the_window(cx: &mut TestAppContext) {
"{list:?}"
);
}
/// A checkbox whose label runs past its narrow box.
struct Worded;
impl Render for Worded {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let label = "Projects/Atrium/Recordings/Walkthrough of the atrium at noon.mov";
div()
.w(px(200.0))
.child(Checkbox::new("worded", false).label(label))
}
}
#[gpui::test]
fn a_long_checkbox_label_wraps_inside_its_box(cx: &mut TestAppContext) {
setup(cx);
let (_, cx) = cx.add_window_view(|_, _| Worded);
cx.run_until_parked();
let label = cx.debug_bounds("checkbox-label").expect("the label draws");
assert!(label.right() <= px(200.0), "{label:?}");
}
+30 -3
View File
@@ -1,11 +1,11 @@
use gpui::{
Context, IntoElement, KeyBinding, KeyUpEvent, Keystroke, Modifiers, MouseButton, Render,
SharedString, TestAppContext, VisualTestContext, Window, point, px,
Context, IntoElement, KeyBinding, KeyUpEvent, Keystroke, Modifiers, MouseButton, ParentElement,
Render, SharedString, Styled, TestAppContext, VisualTestContext, Window, div, point, px,
};
use super::setup;
use crate::{
forms::{IconPicker, InlineEdit, SignaturePad, Stroke},
forms::{Checkbox, Choice, IconPicker, InlineEdit, RadioGroup, SignaturePad, Stroke},
primitives::{FocusNext, IconName},
};
@@ -144,3 +144,30 @@ fn an_inline_edit_takes_tab_opens_on_enter_and_takes_focus_back(cx: &mut TestApp
"focus came back after the first"
);
}
/// A checkbox and a radio whose labels run past their narrow box.
struct Worded;
impl Render for Worded {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let label = "Projects/Atrium/Recordings/Walkthrough of the atrium at noon.mov";
div()
.w(px(200.0))
.child(Checkbox::new("worded", false).label(label))
.child(RadioGroup::new(
"worded-radios",
[Choice::new("one", label)],
))
}
}
#[gpui::test]
fn long_labels_wrap_inside_their_box(cx: &mut TestAppContext) {
setup(cx);
let (_, cx) = cx.add_window_view(|_, _| Worded);
cx.run_until_parked();
for selector in ["checkbox-label", "radio-label"] {
let label = cx.debug_bounds(selector).expect("the label draws");
assert!(label.right() <= px(200.0), "{selector}: {label:?}");
}
}
+32 -3
View File
@@ -18,6 +18,18 @@ struct Deck {
back: bool,
}
impl Deck {
fn turn(&mut self) {
self.back = !self.back;
}
/// Goes `by` cards round a deck of `count`, front up.
fn go(&mut self, by: isize, count: usize) {
self.at = (self.at as isize + by).rem_euclid(count as isize) as usize;
self.back = false;
}
}
/// Cards with a question on the front and its answer on the back, one at a time: a press on the card turns it, and Previous and Next go round the deck, front up. Each face fades in.
#[derive(IntoElement)]
pub struct Flashcards {
@@ -90,8 +102,7 @@ impl RenderOnce for Flashcards {
let deck = deck.clone();
move |_: &gpui::ClickEvent, _: &mut Window, cx: &mut App| {
deck.update(cx, |deck, cx| {
deck.at = (deck.at as isize + by).rem_euclid(count as isize) as usize;
deck.back = false;
deck.go(by, count);
log::info!("flashcards: card {} of {count}", deck.at + 1);
cx.notify();
})
@@ -117,7 +128,7 @@ impl RenderOnce for Flashcards {
.cursor_pointer()
.on_click(move |_, _, cx| {
turned.update(cx, |deck, cx| {
deck.back = !deck.back;
deck.turn();
log::info!(
"flashcards: turned to the {}",
if deck.back { "back" } else { "front" }
@@ -152,3 +163,21 @@ impl RenderOnce for Flashcards {
)
}
}
#[cfg(test)]
mod tests {
use super::Deck;
#[test]
fn a_deck_turns_and_goes_round_front_up() {
let mut deck = Deck::default();
deck.turn();
deck.turn();
assert!(!deck.back, "a second press turns it back");
deck.turn();
deck.go(-1, 3);
assert_eq!((deck.at, deck.back), (2, false));
deck.go(1, 3);
assert_eq!(deck.at, 0);
}
}
+10 -11
View File
@@ -128,7 +128,10 @@ impl RenderOnce for Poll {
};
let percent = format::percent(share as f64, 0, false);
div()
.debug_selector(|| format!("poll-{label}-{percent}"))
.debug_selector(|| match ix == voted {
true => format!("poll-{label}-{percent}-voted"),
false => format!("poll-{label}-{percent}"),
})
.flex()
.flex_col()
.gap_1()
@@ -189,16 +192,12 @@ impl RenderOnce for Poll {
window.focus(&focus);
})
}
Some(_) => {
let focus = action.clone();
Button::new((id.clone(), "change"), "Change vote")
.focus_handle(&action)
.on_click(move |_, window, cx| {
log::info!("poll: vote taken back");
on_vote(None, window, cx);
window.focus(&focus);
})
}
Some(_) => Button::new((id.clone(), "change"), "Change vote")
.focus_handle(&action)
.on_click(move |_, window, cx| {
log::info!("poll: vote taken back");
on_vote(None, window, cx);
}),
};
div()
.flex()
+9 -4
View File
@@ -59,6 +59,11 @@ struct Sitting {
}
impl Sitting {
fn pick(&mut self, option: usize) {
self.pick = Some(option);
self.nudged = false;
}
/// What the one button does next, and says.
fn next(&self, count: usize) -> &'static str {
match (self.done, self.checked, self.at + 1 == count) {
@@ -172,8 +177,7 @@ impl RenderOnce for Quiz {
.on_change(move |value, _, cx| {
let ix = value.parse().expect("an option's place");
chose.update(cx, |sitting, cx| {
sitting.pick = Some(ix);
sitting.nudged = false;
sitting.pick(ix);
cx.notify();
})
});
@@ -289,7 +293,8 @@ mod tests {
assert_eq!(sitting.next(2), "Check");
sitting.advance(1, 2);
assert!(sitting.nudged, "Check with no pick asks for one");
sitting.pick = Some(1);
sitting.pick(1);
assert!(!sitting.nudged, "a pick answers the ask");
sitting.advance(1, 2);
assert_eq!(
(sitting.checked, sitting.right, sitting.next(2)),
@@ -300,7 +305,7 @@ mod tests {
(sitting.at, sitting.pick, sitting.checked),
(1, None, false)
);
sitting.pick = Some(0);
sitting.pick(0);
sitting.advance(2, 2);
assert_eq!(
(sitting.right, sitting.next(2)),
+26 -8
View File
@@ -2,7 +2,7 @@ use std::rc::Rc;
use gpui::{
App, AppContext as _, ElementId, Entity, InteractiveElement, IntoElement, ParentElement,
RenderOnce, SharedString, Styled, Window, div,
RenderOnce, SharedString, Styled, Window, div, prelude::*,
};
use crate::{
@@ -43,14 +43,15 @@ pub enum Answer {
Text(SharedString),
}
/// What the survey holds: the answers picked, a field for each question in words, and whether it was sent.
/// What the survey holds: the answers picked, a field for each question in words, whether it was sent, and whether Submit came with an answer missing.
struct Sheet {
picked: Vec<Option<Answer>>,
fields: Vec<Option<Entity<TextInput>>>,
sent: bool,
nudged: bool,
}
/// Questions on one page, each answered by one option, several, or words; Submit rests until every one is answered, then hands the answers to the owner, and the survey thanks the viewer in Submit's place and focus.
/// Questions on one page, each answered by one option, several, or words. Submit hands the answers to the owner once every one is answered, and asks for the rest before; then the survey thanks the viewer in Submit's place and focus.
#[derive(IntoElement)]
pub struct Survey {
id: ElementId,
@@ -125,6 +126,7 @@ impl RenderOnce for Survey {
})
.collect(),
sent: false,
nudged: false,
}
});
assert_eq!(
@@ -226,6 +228,7 @@ impl RenderOnce for Survey {
)
.child(input)
});
let nudged = sheet.read(cx).nudged && !complete;
let (submitted, focus, asked) = (sheet.clone(), action.clone(), questions.clone());
div()
.flex()
@@ -233,6 +236,14 @@ impl RenderOnce for Survey {
.gap_5()
.text_size(theme.text_size(TextSize::Sm))
.children(items)
.when(nudged, |survey| {
survey.child(
div()
.debug_selector(|| "survey-answer-first".into())
.text_color(colors.danger)
.child("Answer each question first."),
)
})
.child(
div()
.debug_selector(|| "survey-action".into())
@@ -241,12 +252,19 @@ impl RenderOnce for Survey {
Button::new((id, "submit-button"), "Submit")
.variant(ButtonVariant::Primary)
.focus_handle(&action)
.disabled(!complete)
.on_click(move |_, window, cx| {
let given: Vec<Answer> = answers(&asked, submitted.read(cx), cx)
.into_iter()
.map(|answer| answer.expect("Submit waits for every answer"))
.collect();
let given: Option<Vec<Answer>> =
answers(&asked, submitted.read(cx), cx)
.into_iter()
.collect();
let Some(given) = given else {
log::info!("survey: an answer is missing");
submitted.update(cx, |sheet, cx| {
sheet.nudged = true;
cx.notify();
});
return;
};
log::info!("survey: sent {} answers", given.len());
on_submit(&given, window, cx);
submitted.update(cx, |sheet, cx| {
+21 -4
View File
@@ -53,7 +53,7 @@ fn a_poll_votes_shows_results_and_takes_the_vote_back(cx: &mut TestAppContext) {
assert_eq!(said(), ["Some(1)"]);
VOTED.set(Some(1));
show(cx);
assert!(shown("poll-Tea-60%", cx) && shown("poll-Soup-20%", cx));
assert!(shown("poll-Tea-60%-voted", cx) && shown("poll-Soup-20%", cx));
press("enter", cx);
assert_eq!(said(), ["Some(1)", "None"], "Change vote held Vote's focus");
VOTED.set(None);
@@ -133,7 +133,7 @@ fn a_survey_sends_every_answer(cx: &mut TestAppContext) {
assert!(shown("survey-sent", cx) && !at_root(cx));
}
/// Submit rests while an answer is missing; a pointer's Submit hands focus to the thanks.
/// Submit asks for what is missing: words of only spaces, or a box ticked and unticked; a pointer's Submit hands focus to the thanks.
#[gpui::test]
fn a_survey_waits_for_every_answer(cx: &mut TestAppContext) {
let (_, cx) = stage(survey, cx);
@@ -141,13 +141,30 @@ fn a_survey_waits_for_every_answer(cx: &mut TestAppContext) {
press("down", cx);
press("tab", cx);
press("space", cx);
press("tab", cx);
press("tab", cx);
cx.simulate_input(" ");
press("tab", cx);
press("enter", cx);
assert!(
said().is_empty() && shown("survey-answer-first", cx),
"blank words are no answer"
);
press("shift-tab", cx);
cx.simulate_input("Fine");
press("shift-tab", cx);
press("shift-tab", cx);
press("space", cx);
for _ in 0..3 {
press("tab", cx);
}
press("enter", cx);
assert!(said().is_empty(), "no words yet, so Submit is no stop");
assert!(said().is_empty(), "no box ticked is no answer");
assert!(!at_root(cx), "Submit kept its focus");
press("shift-tab", cx);
cx.simulate_input("Fine");
press("shift-tab", cx);
press("shift-tab", cx);
press("space", cx);
click("survey-action", cx);
assert_eq!(said().len(), 1);
assert!(!at_root(cx), "the field left, and focus went to the thanks");