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:
1 parent
86c9de7d0a
commit
46aa381eee
9 files changed
+136
-56
No files matched your search
+7
-1
@@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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| {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user