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))
|
.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:?}"
|
"{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::{
|
use gpui::{
|
||||||
Context, IntoElement, KeyBinding, KeyUpEvent, Keystroke, Modifiers, MouseButton, Render,
|
Context, IntoElement, KeyBinding, KeyUpEvent, Keystroke, Modifiers, MouseButton, ParentElement,
|
||||||
SharedString, TestAppContext, VisualTestContext, Window, point, px,
|
Render, SharedString, Styled, TestAppContext, VisualTestContext, Window, div, point, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::setup;
|
use super::setup;
|
||||||
use crate::{
|
use crate::{
|
||||||
forms::{IconPicker, InlineEdit, SignaturePad, Stroke},
|
forms::{Checkbox, Choice, IconPicker, InlineEdit, RadioGroup, SignaturePad, Stroke},
|
||||||
primitives::{FocusNext, IconName},
|
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"
|
"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,
|
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.
|
/// 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)]
|
#[derive(IntoElement)]
|
||||||
pub struct Flashcards {
|
pub struct Flashcards {
|
||||||
@@ -90,8 +102,7 @@ impl RenderOnce for Flashcards {
|
|||||||
let deck = deck.clone();
|
let deck = deck.clone();
|
||||||
move |_: &gpui::ClickEvent, _: &mut Window, cx: &mut App| {
|
move |_: &gpui::ClickEvent, _: &mut Window, cx: &mut App| {
|
||||||
deck.update(cx, |deck, cx| {
|
deck.update(cx, |deck, cx| {
|
||||||
deck.at = (deck.at as isize + by).rem_euclid(count as isize) as usize;
|
deck.go(by, count);
|
||||||
deck.back = false;
|
|
||||||
log::info!("flashcards: card {} of {count}", deck.at + 1);
|
log::info!("flashcards: card {} of {count}", deck.at + 1);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
@@ -117,7 +128,7 @@ impl RenderOnce for Flashcards {
|
|||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.on_click(move |_, _, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
turned.update(cx, |deck, cx| {
|
turned.update(cx, |deck, cx| {
|
||||||
deck.back = !deck.back;
|
deck.turn();
|
||||||
log::info!(
|
log::info!(
|
||||||
"flashcards: turned to the {}",
|
"flashcards: turned to the {}",
|
||||||
if deck.back { "back" } else { "front" }
|
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);
|
let percent = format::percent(share as f64, 0, false);
|
||||||
div()
|
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()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -189,16 +192,12 @@ impl RenderOnce for Poll {
|
|||||||
window.focus(&focus);
|
window.focus(&focus);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Some(_) => {
|
Some(_) => Button::new((id.clone(), "change"), "Change vote")
|
||||||
let focus = action.clone();
|
.focus_handle(&action)
|
||||||
Button::new((id.clone(), "change"), "Change vote")
|
.on_click(move |_, window, cx| {
|
||||||
.focus_handle(&action)
|
log::info!("poll: vote taken back");
|
||||||
.on_click(move |_, window, cx| {
|
on_vote(None, window, cx);
|
||||||
log::info!("poll: vote taken back");
|
}),
|
||||||
on_vote(None, window, cx);
|
|
||||||
window.focus(&focus);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
+9
-4
@@ -59,6 +59,11 @@ struct Sitting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Sitting {
|
impl Sitting {
|
||||||
|
fn pick(&mut self, option: usize) {
|
||||||
|
self.pick = Some(option);
|
||||||
|
self.nudged = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// What the one button does next, and says.
|
/// What the one button does next, and says.
|
||||||
fn next(&self, count: usize) -> &'static str {
|
fn next(&self, count: usize) -> &'static str {
|
||||||
match (self.done, self.checked, self.at + 1 == count) {
|
match (self.done, self.checked, self.at + 1 == count) {
|
||||||
@@ -172,8 +177,7 @@ impl RenderOnce for Quiz {
|
|||||||
.on_change(move |value, _, cx| {
|
.on_change(move |value, _, cx| {
|
||||||
let ix = value.parse().expect("an option's place");
|
let ix = value.parse().expect("an option's place");
|
||||||
chose.update(cx, |sitting, cx| {
|
chose.update(cx, |sitting, cx| {
|
||||||
sitting.pick = Some(ix);
|
sitting.pick(ix);
|
||||||
sitting.nudged = false;
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -289,7 +293,8 @@ mod tests {
|
|||||||
assert_eq!(sitting.next(2), "Check");
|
assert_eq!(sitting.next(2), "Check");
|
||||||
sitting.advance(1, 2);
|
sitting.advance(1, 2);
|
||||||
assert!(sitting.nudged, "Check with no pick asks for one");
|
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);
|
sitting.advance(1, 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(sitting.checked, sitting.right, sitting.next(2)),
|
(sitting.checked, sitting.right, sitting.next(2)),
|
||||||
@@ -300,7 +305,7 @@ mod tests {
|
|||||||
(sitting.at, sitting.pick, sitting.checked),
|
(sitting.at, sitting.pick, sitting.checked),
|
||||||
(1, None, false)
|
(1, None, false)
|
||||||
);
|
);
|
||||||
sitting.pick = Some(0);
|
sitting.pick(0);
|
||||||
sitting.advance(2, 2);
|
sitting.advance(2, 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(sitting.right, sitting.next(2)),
|
(sitting.right, sitting.next(2)),
|
||||||
|
|||||||
+26
-8
@@ -2,7 +2,7 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, ElementId, Entity, InteractiveElement, IntoElement, ParentElement,
|
App, AppContext as _, ElementId, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
RenderOnce, SharedString, Styled, Window, div,
|
RenderOnce, SharedString, Styled, Window, div, prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -43,14 +43,15 @@ pub enum Answer {
|
|||||||
Text(SharedString),
|
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 {
|
struct Sheet {
|
||||||
picked: Vec<Option<Answer>>,
|
picked: Vec<Option<Answer>>,
|
||||||
fields: Vec<Option<Entity<TextInput>>>,
|
fields: Vec<Option<Entity<TextInput>>>,
|
||||||
sent: bool,
|
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)]
|
#[derive(IntoElement)]
|
||||||
pub struct Survey {
|
pub struct Survey {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -125,6 +126,7 @@ impl RenderOnce for Survey {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
sent: false,
|
sent: false,
|
||||||
|
nudged: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -226,6 +228,7 @@ impl RenderOnce for Survey {
|
|||||||
)
|
)
|
||||||
.child(input)
|
.child(input)
|
||||||
});
|
});
|
||||||
|
let nudged = sheet.read(cx).nudged && !complete;
|
||||||
let (submitted, focus, asked) = (sheet.clone(), action.clone(), questions.clone());
|
let (submitted, focus, asked) = (sheet.clone(), action.clone(), questions.clone());
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -233,6 +236,14 @@ impl RenderOnce for Survey {
|
|||||||
.gap_5()
|
.gap_5()
|
||||||
.text_size(theme.text_size(TextSize::Sm))
|
.text_size(theme.text_size(TextSize::Sm))
|
||||||
.children(items)
|
.children(items)
|
||||||
|
.when(nudged, |survey| {
|
||||||
|
survey.child(
|
||||||
|
div()
|
||||||
|
.debug_selector(|| "survey-answer-first".into())
|
||||||
|
.text_color(colors.danger)
|
||||||
|
.child("Answer each question first."),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.debug_selector(|| "survey-action".into())
|
.debug_selector(|| "survey-action".into())
|
||||||
@@ -241,12 +252,19 @@ impl RenderOnce for Survey {
|
|||||||
Button::new((id, "submit-button"), "Submit")
|
Button::new((id, "submit-button"), "Submit")
|
||||||
.variant(ButtonVariant::Primary)
|
.variant(ButtonVariant::Primary)
|
||||||
.focus_handle(&action)
|
.focus_handle(&action)
|
||||||
.disabled(!complete)
|
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let given: Vec<Answer> = answers(&asked, submitted.read(cx), cx)
|
let given: Option<Vec<Answer>> =
|
||||||
.into_iter()
|
answers(&asked, submitted.read(cx), cx)
|
||||||
.map(|answer| answer.expect("Submit waits for every answer"))
|
.into_iter()
|
||||||
.collect();
|
.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());
|
log::info!("survey: sent {} answers", given.len());
|
||||||
on_submit(&given, window, cx);
|
on_submit(&given, window, cx);
|
||||||
submitted.update(cx, |sheet, 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)"]);
|
assert_eq!(said(), ["Some(1)"]);
|
||||||
VOTED.set(Some(1));
|
VOTED.set(Some(1));
|
||||||
show(cx);
|
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);
|
press("enter", cx);
|
||||||
assert_eq!(said(), ["Some(1)", "None"], "Change vote held Vote's focus");
|
assert_eq!(said(), ["Some(1)", "None"], "Change vote held Vote's focus");
|
||||||
VOTED.set(None);
|
VOTED.set(None);
|
||||||
@@ -133,7 +133,7 @@ fn a_survey_sends_every_answer(cx: &mut TestAppContext) {
|
|||||||
assert!(shown("survey-sent", cx) && !at_root(cx));
|
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]
|
#[gpui::test]
|
||||||
fn a_survey_waits_for_every_answer(cx: &mut TestAppContext) {
|
fn a_survey_waits_for_every_answer(cx: &mut TestAppContext) {
|
||||||
let (_, cx) = stage(survey, cx);
|
let (_, cx) = stage(survey, cx);
|
||||||
@@ -141,13 +141,30 @@ fn a_survey_waits_for_every_answer(cx: &mut TestAppContext) {
|
|||||||
press("down", cx);
|
press("down", cx);
|
||||||
press("tab", cx);
|
press("tab", cx);
|
||||||
press("space", 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 {
|
for _ in 0..3 {
|
||||||
press("tab", cx);
|
press("tab", cx);
|
||||||
}
|
}
|
||||||
press("enter", 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);
|
press("shift-tab", cx);
|
||||||
cx.simulate_input("Fine");
|
press("shift-tab", cx);
|
||||||
|
press("shift-tab", cx);
|
||||||
|
press("space", cx);
|
||||||
click("survey-action", cx);
|
click("survey-action", cx);
|
||||||
assert_eq!(said().len(), 1);
|
assert_eq!(said().len(), 1);
|
||||||
assert!(!at_root(cx), "the field left, and focus went to the thanks");
|
assert!(!at_root(cx), "the field left, and focus went to the thanks");
|
||||||
|
|||||||
+1
-1
@@ -328,7 +328,7 @@
|
|||||||
|
|
||||||
## T42d Misc · Asking and learning
|
## T42d Misc · Asking and learning
|
||||||
|
|
||||||
- [x] Poll / Survey → `misc::Poll`: one vote, then each option's share with the vote marked, and Change vote on Vote's stop; `misc::Survey`: questions on one page answered by an option, several, or words, Submit once each is answered, and thanks in its place and focus
|
- [x] Poll / Survey → `misc::Poll`: one vote, then each option's share with the vote marked, and Change vote on Vote's stop; `misc::Survey`: questions on one page answered by an option, several, or words; Submit asks for what is missing, then sends, and thanks in its place and focus
|
||||||
- [x] Quiz / Flashcard → `misc::Quiz`: a question at a time, Check shows the right answer, then Next and the score, one button on one stop; `misc::Flashcards`: a press turns the card, Previous and Next go round the deck
|
- [x] Quiz / Flashcard → `misc::Quiz`: a question at a time, Check shows the right answer, then Next and the score, one button on one stop; `misc::Flashcards`: a press turns the card, Previous and Next go round the deck
|
||||||
|
|
||||||
## T42e Misc · Terms and notices
|
## T42e Misc · Terms and notices
|
||||||
|
|||||||
Reference in new issue
Block a user