Panics · P42 Misc: handlers in new; the web view shows wry's failures; kept state fits lists that changed
This commit is contained in:
1 parent
770d13575d
commit
3a0a4bb699
20 files changed
+362
-189
No files matched your search
@@ -20,19 +20,19 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
"poll",
|
||||
"Which chart should come next?",
|
||||
["Sankey", "Sunburst", "Candlestick"],
|
||||
move |vote, _, cx| {
|
||||
tally.update(cx, |tally, cx| {
|
||||
match (vote, tally.voted) {
|
||||
(Some(ix), _) => tally.votes[ix] += 1,
|
||||
(None, Some(ix)) => tally.votes[ix] -= 1,
|
||||
(None, None) => panic!("gallery poll: no vote to take back"),
|
||||
}
|
||||
tally.voted = vote;
|
||||
cx.notify();
|
||||
})
|
||||
},
|
||||
)
|
||||
.votes(votes)
|
||||
.on_vote(move |vote, _, cx| {
|
||||
tally.update(cx, |tally, cx| {
|
||||
match (vote, tally.voted) {
|
||||
(Some(ix), _) => tally.votes[ix] += 1,
|
||||
(None, Some(ix)) => tally.votes[ix] -= 1,
|
||||
(None, None) => panic!("gallery poll: no vote to take back"),
|
||||
}
|
||||
tally.voted = vote;
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
.votes(votes);
|
||||
let poll = match voted {
|
||||
Some(ix) => poll.voted(ix),
|
||||
None => poll,
|
||||
@@ -62,9 +62,7 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
vec!["Forms".into(), "Charts".into(), "Tables".into()],
|
||||
),
|
||||
Question::Text("What should we build next?".into()),
|
||||
],
|
||||
)
|
||||
.on_submit(|answers, _, _| log::info!("gallery: survey {answers:?}")),
|
||||
], |answers, _, _| log::info!("gallery: survey {answers:?}")),
|
||||
),
|
||||
cx,
|
||||
)),
|
||||
|
||||
@@ -53,9 +53,8 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
)
|
||||
.child(
|
||||
div().w(px(280.0)).child(
|
||||
QrCodeScanner::new("scanner", 1.5)
|
||||
.frame(frame.read(cx).clone())
|
||||
.on_scan(|text, _, _| log::info!("gallery: scanned {text}")),
|
||||
QrCodeScanner::new("scanner", 1.5, |text, _, _| log::info!("gallery: scanned {text}"))
|
||||
.frame(frame.read(cx).clone()),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -68,9 +67,7 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
.child(probe(
|
||||
"captcha",
|
||||
div().w(px(280.0)).child(
|
||||
Captcha::new("captcha", picture, 3.0)
|
||||
.state(state)
|
||||
.on_answer(move |text, _, cx| {
|
||||
Captcha::new("captcha", picture, 3.0, move |text, _, cx| {
|
||||
checked.update(cx, |desk, cx| {
|
||||
desk.1 = match text.eq_ignore_ascii_case(answer) {
|
||||
true => CaptchaState::Passed,
|
||||
@@ -79,6 +76,7 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.state(state)
|
||||
.on_refresh(move |_, cx| {
|
||||
refreshed.update(cx, |desk, cx| {
|
||||
*desk = ((desk.0 + 1) % ANSWERS.len(), CaptchaState::Asking);
|
||||
|
||||
@@ -110,11 +110,7 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
div().w(px(360.0)).child(
|
||||
CookieBanner::new(
|
||||
"cookies",
|
||||
"The help pages set cookies. Necessary ones keep them working; the rest are up to you.",
|
||||
)
|
||||
.kind("Analytics", "Counts which pages help, without names.")
|
||||
.kind("Video", "Lets embedded tutorials play and remember where you stopped.")
|
||||
.on_choose(move |kinds, _, cx| {
|
||||
"The help pages set cookies. Necessary ones keep them working; the rest are up to you.", move |kinds, _, cx| {
|
||||
let words = match kinds.len() {
|
||||
0 => "Only necessary cookies.".to_string(),
|
||||
_ => format!("Cookies on: {}.", kinds.join(", ")),
|
||||
@@ -123,7 +119,9 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
*said = words.into();
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
})
|
||||
.kind("Analytics", "Counts which pages help, without names.")
|
||||
.kind("Video", "Lets embedded tutorials play and remember where you stopped."),
|
||||
),
|
||||
))
|
||||
.child(
|
||||
|
||||
+9
-15
@@ -36,19 +36,24 @@ pub struct Captcha {
|
||||
picture: ImageSource,
|
||||
ratio: f32,
|
||||
state: CaptchaState,
|
||||
on_answer: Option<OnAnswer>,
|
||||
on_answer: OnAnswer,
|
||||
on_refresh: Option<OnRefresh>,
|
||||
}
|
||||
|
||||
impl Captcha {
|
||||
/// `ratio` is the picture's width over its height.
|
||||
pub fn new(id: impl Into<ElementId>, picture: impl Into<ImageSource>, ratio: f32) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
picture: impl Into<ImageSource>,
|
||||
ratio: f32,
|
||||
on_answer: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
picture: picture.into(),
|
||||
ratio: checked_ratio(ratio),
|
||||
state: CaptchaState::default(),
|
||||
on_answer: None,
|
||||
on_answer: Rc::new(on_answer),
|
||||
on_refresh: None,
|
||||
}
|
||||
}
|
||||
@@ -58,15 +63,6 @@ impl Captcha {
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs with the typed answer, trimmed.
|
||||
pub fn on_answer(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_answer = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Asks for a new picture.
|
||||
pub fn on_refresh(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_refresh = Some(Rc::new(handler));
|
||||
@@ -77,9 +73,7 @@ impl Captcha {
|
||||
impl RenderOnce for Captcha {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let on_answer = self
|
||||
.on_answer
|
||||
.unwrap_or_else(|| panic!("captcha {id:?} has no on_answer"));
|
||||
let on_answer = self.on_answer;
|
||||
let field = window.use_keyed_state((id.clone(), "field"), cx, |window, cx| {
|
||||
TextInput::new(window, cx).placeholder("Type the characters")
|
||||
});
|
||||
|
||||
+12
-20
@@ -26,16 +26,20 @@ pub struct CookieBanner {
|
||||
id: ElementId,
|
||||
message: SharedString,
|
||||
kinds: Vec<(SharedString, SharedString)>,
|
||||
on_choose: Option<OnChoose>,
|
||||
on_choose: OnChoose,
|
||||
}
|
||||
|
||||
impl CookieBanner {
|
||||
pub fn new(id: impl Into<ElementId>, message: impl Into<SharedString>) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
on_choose: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
message: message.into(),
|
||||
kinds: Vec::new(),
|
||||
on_choose: None,
|
||||
on_choose: Rc::new(on_choose),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,15 +48,6 @@ impl CookieBanner {
|
||||
self.kinds.push((name.into(), detail.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs with the kinds turned on, by name; necessary cookies go unnamed.
|
||||
pub fn on_choose(
|
||||
mut self,
|
||||
handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_choose = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CookieBanner {
|
||||
@@ -62,19 +57,16 @@ impl RenderOnce for CookieBanner {
|
||||
!self.kinds.is_empty(),
|
||||
"cookie banner {id:?} has no kinds to choose"
|
||||
);
|
||||
let on_choose = self
|
||||
.on_choose
|
||||
.unwrap_or_else(|| panic!("cookie banner {id:?} has no on_choose"));
|
||||
let on_choose = self.on_choose;
|
||||
let count = self.kinds.len();
|
||||
let choosing = window.use_keyed_state((id.clone(), "choosing"), cx, move |_, _| Choosing {
|
||||
open: false,
|
||||
on: vec![false; count],
|
||||
});
|
||||
assert_eq!(
|
||||
choosing.read(cx).on.len(),
|
||||
count,
|
||||
"cookie banner {id:?} changed its kinds"
|
||||
);
|
||||
if choosing.read(cx).on.len() != count {
|
||||
log::error!("cookie banner {id:?}: its kinds changed; each starts off again");
|
||||
choosing.update(cx, |choosing, _| choosing.on = vec![false; count]);
|
||||
}
|
||||
let choose = tab_stop((id.clone(), "choose").into(), true, window, cx);
|
||||
let (open, on) = (choosing.read(cx).open, choosing.read(cx).on.clone());
|
||||
let theme = cx.theme();
|
||||
|
||||
+6
-5
@@ -97,11 +97,12 @@ impl RenderOnce for ExportDialog {
|
||||
.first()
|
||||
.unwrap_or_else(|| panic!("export dialog {id:?} has no formats"));
|
||||
let chosen = window.use_keyed_state((id.clone(), "chosen"), cx, move |_, _| first);
|
||||
let format = *chosen.read(cx);
|
||||
assert!(
|
||||
self.formats.contains(&format),
|
||||
"export dialog {id:?} lost its format {format:?}"
|
||||
);
|
||||
let mut format = *chosen.read(cx);
|
||||
if !self.formats.contains(&format) {
|
||||
log::error!("export dialog {id:?}: {format:?} left the formats; {first:?} is chosen");
|
||||
format = first;
|
||||
chosen.update(cx, |chosen, _| *chosen = first);
|
||||
}
|
||||
let cards = self.formats.iter().map(|&each| {
|
||||
let pick = chosen.clone();
|
||||
RadioCard::new((id.clone(), each.label()), each == format, each.label())
|
||||
|
||||
@@ -58,6 +58,13 @@ impl RenderOnce for Flashcards {
|
||||
let count = self.cards.len();
|
||||
assert!(count > 0, "flashcards {id:?} has no cards");
|
||||
let deck = window.use_keyed_state((id.clone(), "deck"), cx, |_, _| Deck::default());
|
||||
if deck.read(cx).at >= count {
|
||||
log::error!(
|
||||
"flashcards {id:?}: card {} of {count}; the deck starts over",
|
||||
deck.read(cx).at
|
||||
);
|
||||
deck.update(cx, |deck, _| *deck = Deck::default());
|
||||
}
|
||||
let card = tab_stop((id.clone(), "card").into(), true, window, cx);
|
||||
let (at, back) = (deck.read(cx).at, deck.read(cx).back);
|
||||
let (front_words, back_words) = self.cards[at].clone();
|
||||
|
||||
+2
-1
@@ -134,7 +134,8 @@ impl RenderOnce for ImportDialog {
|
||||
nudged: false,
|
||||
}
|
||||
});
|
||||
if mapping.read(cx).columns != columns {
|
||||
let now = mapping.read(cx);
|
||||
if now.columns != columns || now.picked.len() != fields.len() {
|
||||
let picked = matched(&fields, &columns);
|
||||
mapping.update(cx, |mapping, _| {
|
||||
*mapping = Mapping {
|
||||
|
||||
@@ -59,12 +59,15 @@ impl RenderOnce for LicenseViewer {
|
||||
"license viewer {id:?} has no packages"
|
||||
);
|
||||
let chosen = window.use_keyed_state((id.clone(), "chosen"), cx, |_, _| 0);
|
||||
let at = *chosen.read(cx);
|
||||
assert!(
|
||||
at < self.packages.len(),
|
||||
"license viewer {id:?} lost package {at} of {}",
|
||||
self.packages.len()
|
||||
);
|
||||
let mut at = *chosen.read(cx);
|
||||
if at >= self.packages.len() {
|
||||
log::error!(
|
||||
"license viewer {id:?}: package {at} of {}; the first is shown",
|
||||
self.packages.len()
|
||||
);
|
||||
at = 0;
|
||||
chosen.update(cx, |chosen, _| *chosen = 0);
|
||||
}
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let list = self.packages.iter().enumerate().fold(
|
||||
|
||||
+16
-25
@@ -24,7 +24,7 @@ pub struct Poll {
|
||||
options: Vec<SharedString>,
|
||||
votes: Vec<u64>,
|
||||
voted: Option<usize>,
|
||||
on_vote: Option<OnVote>,
|
||||
on_vote: OnVote,
|
||||
}
|
||||
|
||||
impl Poll {
|
||||
@@ -32,6 +32,7 @@ impl Poll {
|
||||
id: impl Into<ElementId>,
|
||||
question: impl Into<SharedString>,
|
||||
options: impl IntoIterator<Item = impl Into<SharedString>>,
|
||||
on_vote: impl Fn(Option<usize>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
let options: Vec<SharedString> = options.into_iter().map(Into::into).collect();
|
||||
Self {
|
||||
@@ -40,7 +41,7 @@ impl Poll {
|
||||
votes: vec![0; options.len()],
|
||||
options,
|
||||
voted: None,
|
||||
on_vote: None,
|
||||
on_vote: Rc::new(on_vote),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,15 +56,6 @@ impl Poll {
|
||||
self.voted = Some(option);
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs with the option voted for, or none when Change vote takes the vote back.
|
||||
pub fn on_vote(
|
||||
mut self,
|
||||
handler: impl Fn(Option<usize>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_vote = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Poll {
|
||||
@@ -77,25 +69,24 @@ impl RenderOnce for Poll {
|
||||
"poll {id:?} has {} counts for {count} options",
|
||||
self.votes.len()
|
||||
);
|
||||
if let Some(voted) = self.voted {
|
||||
assert!(
|
||||
voted < count,
|
||||
"poll {id:?}: vote {voted} of {count} options"
|
||||
);
|
||||
}
|
||||
let on_vote = self
|
||||
.on_vote
|
||||
.unwrap_or_else(|| panic!("poll {id:?} has no on_vote"));
|
||||
let voted = self.voted.filter(|voted| {
|
||||
let held = *voted < count;
|
||||
if !held {
|
||||
log::error!("poll {id:?}: vote {voted} of {count} options; none counted");
|
||||
}
|
||||
held
|
||||
});
|
||||
let on_vote = self.on_vote;
|
||||
let pick = window.use_keyed_state((id.clone(), "pick"), cx, |_, _| None::<usize>);
|
||||
if self.voted.is_some() && *pick.read(cx) != self.voted {
|
||||
pick.update(cx, |pick, _| *pick = self.voted);
|
||||
if voted.is_some() && *pick.read(cx) != voted {
|
||||
pick.update(cx, |pick, _| *pick = voted);
|
||||
}
|
||||
let action = tab_stop((id.clone(), "action").into(), true, window, cx);
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let picked = *pick.read(cx);
|
||||
let picked = pick.read(cx).filter(|picked| *picked < count);
|
||||
let body =
|
||||
match self.voted {
|
||||
match voted {
|
||||
None => {
|
||||
let choices = self
|
||||
.options
|
||||
@@ -179,7 +170,7 @@ impl RenderOnce for Poll {
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
let button = match self.voted {
|
||||
let button = match voted {
|
||||
None => {
|
||||
let (focus, on_vote) = (action.clone(), on_vote.clone());
|
||||
Button::new((id.clone(), "vote"), "Vote")
|
||||
|
||||
@@ -132,6 +132,13 @@ impl RenderOnce for Quiz {
|
||||
assert!(count > 0, "quiz {id:?} has no questions");
|
||||
let sitting =
|
||||
window.use_keyed_state((id.clone(), "sitting"), cx, |_, _| Sitting::default());
|
||||
if sitting.read(cx).at >= count {
|
||||
log::error!(
|
||||
"quiz {id:?}: question {} of {count}; it starts over",
|
||||
sitting.read(cx).at
|
||||
);
|
||||
sitting.update(cx, |sitting, _| *sitting = Sitting::default());
|
||||
}
|
||||
let action = tab_stop((id.clone(), "action").into(), true, window, cx);
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
|
||||
+17
-16
@@ -16,7 +16,10 @@ type OnScan = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
||||
|
||||
/// The text of the first QR code that decodes in a frame of gpui's BGRA pixels.
|
||||
pub(crate) fn read(frame: &RenderImage) -> Option<String> {
|
||||
let pixels = frame.as_bytes(0).expect("a frame has pixels");
|
||||
let Some(pixels) = frame.as_bytes(0) else {
|
||||
log::error!("qr code scanner: a frame with no pixels");
|
||||
return None;
|
||||
};
|
||||
let size = frame.size(0);
|
||||
let (width, height) = (size.width.0 as usize, size.height.0 as usize);
|
||||
let mut image = rqrr::PreparedImage::prepare_from_greyscale(width, height, |x, y| {
|
||||
@@ -45,17 +48,21 @@ pub struct QrCodeScanner {
|
||||
id: ElementId,
|
||||
ratio: f32,
|
||||
frame: Option<Arc<RenderImage>>,
|
||||
on_scan: Option<OnScan>,
|
||||
on_scan: OnScan,
|
||||
}
|
||||
|
||||
impl QrCodeScanner {
|
||||
/// `ratio` is the camera's width over its height.
|
||||
pub fn new(id: impl Into<ElementId>, ratio: f32) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
ratio: f32,
|
||||
on_scan: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
ratio,
|
||||
frame: None,
|
||||
on_scan: None,
|
||||
on_scan: Rc::new(on_scan),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,23 +71,12 @@ impl QrCodeScanner {
|
||||
self.frame = Some(frame);
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs with the text of each code read.
|
||||
pub fn on_scan(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_scan = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for QrCodeScanner {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let on_scan = self
|
||||
.on_scan
|
||||
.unwrap_or_else(|| panic!("qr code scanner {id:?} has no on_scan"));
|
||||
let on_scan = self.on_scan;
|
||||
let scan = window.use_keyed_state((id.clone(), "scan"), cx, |_, _| Scan::default());
|
||||
scan.update(cx, |scan, _| scan.on_scan = Some(on_scan));
|
||||
let fresh = self.frame.clone().filter(|frame| {
|
||||
@@ -201,6 +197,11 @@ mod tests {
|
||||
|
||||
use super::{frame_of, read};
|
||||
|
||||
#[test]
|
||||
fn a_picture_with_no_frame_reads_nothing() {
|
||||
assert_eq!(read(&RenderImage::new(Vec::new())), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_reads_as_the_code_it_shows() {
|
||||
assert_eq!(
|
||||
|
||||
+37
-32
@@ -56,26 +56,21 @@ struct Sheet {
|
||||
pub struct Survey {
|
||||
id: ElementId,
|
||||
questions: Vec<Question>,
|
||||
on_submit: Option<OnSubmit>,
|
||||
on_submit: OnSubmit,
|
||||
}
|
||||
|
||||
impl Survey {
|
||||
pub fn new(id: impl Into<ElementId>, questions: impl IntoIterator<Item = Question>) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
questions: impl IntoIterator<Item = Question>,
|
||||
on_submit: impl Fn(&[Answer], &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
questions: questions.into_iter().collect(),
|
||||
on_submit: None,
|
||||
on_submit: Rc::new(on_submit),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs with every answer, in the order of the questions.
|
||||
pub fn on_submit(
|
||||
mut self,
|
||||
handler: impl Fn(&[Answer], &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_submit = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Each question's answer as it stands, none where it waits for one.
|
||||
@@ -102,6 +97,22 @@ fn choices(options: &[SharedString]) -> impl Iterator<Item = Choice> + '_ {
|
||||
.map(|(ix, label)| Choice::new(ix.to_string(), label.clone()))
|
||||
}
|
||||
|
||||
/// A sheet with no answers, a field for each question answered in words.
|
||||
fn blank(questions: &[Question], window: &mut Window, cx: &mut App) -> Sheet {
|
||||
Sheet {
|
||||
picked: vec![None; questions.len()],
|
||||
fields: questions
|
||||
.iter()
|
||||
.map(|question| {
|
||||
matches!(question, Question::Text(_))
|
||||
.then(|| cx.new(|cx| TextInput::new(window, cx).multi_line(2, 5)))
|
||||
})
|
||||
.collect(),
|
||||
sent: false,
|
||||
nudged: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn place(value: &SharedString) -> usize {
|
||||
value.parse().expect("an option's place")
|
||||
}
|
||||
@@ -110,30 +121,24 @@ impl RenderOnce for Survey {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
assert!(!self.questions.is_empty(), "survey {id:?} has no questions");
|
||||
let on_submit = self
|
||||
.on_submit
|
||||
.unwrap_or_else(|| panic!("survey {id:?} has no on_submit"));
|
||||
let on_submit = self.on_submit;
|
||||
let questions = Rc::new(self.questions);
|
||||
let sheet = window.use_keyed_state((id.clone(), "sheet"), cx, {
|
||||
let questions = questions.clone();
|
||||
move |window, cx| Sheet {
|
||||
picked: vec![None; questions.len()],
|
||||
fields: questions
|
||||
.iter()
|
||||
.map(|question| {
|
||||
matches!(question, Question::Text(_))
|
||||
.then(|| cx.new(|cx| TextInput::new(window, cx).multi_line(2, 5)))
|
||||
})
|
||||
.collect(),
|
||||
sent: false,
|
||||
nudged: false,
|
||||
}
|
||||
move |window, cx| blank(&questions, window, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
sheet.read(cx).picked.len(),
|
||||
questions.len(),
|
||||
"survey {id:?} changed its questions"
|
||||
);
|
||||
let fits = {
|
||||
let now = sheet.read(cx);
|
||||
now.picked.len() == questions.len()
|
||||
&& questions.iter().zip(&now.fields).all(|(question, field)| {
|
||||
matches!(question, Question::Text(_)) == field.is_some()
|
||||
})
|
||||
};
|
||||
if !fits {
|
||||
log::error!("survey {id:?}: its questions changed; the answers start over");
|
||||
let fresh = blank(&questions, window, cx);
|
||||
sheet.update(cx, |sheet, _| *sheet = fresh);
|
||||
}
|
||||
let sent = sheet.read(cx).sent;
|
||||
let action = tab_stop((id.clone(), "submit").into(), !sent, window, cx);
|
||||
let theme = cx.theme();
|
||||
|
||||
@@ -32,9 +32,10 @@ fn show(cx: &mut VisualTestContext) {
|
||||
|
||||
fn poll() -> AnyElement {
|
||||
let voted = VOTED.get();
|
||||
let poll = Poll::new("poll", "Lunch?", ["Soup", "Tea", "Toast"])
|
||||
.votes([1, u64::from(voted == Some(1)) + 2, 1])
|
||||
.on_vote(|vote, _, _| say(format!("{vote:?}")));
|
||||
let poll = Poll::new("poll", "Lunch?", ["Soup", "Tea", "Toast"], |vote, _, _| {
|
||||
say(format!("{vote:?}"))
|
||||
})
|
||||
.votes([1, u64::from(voted == Some(1)) + 2, 1]);
|
||||
match voted {
|
||||
Some(vote) => poll.voted(vote),
|
||||
None => poll,
|
||||
@@ -106,8 +107,8 @@ fn survey() -> AnyElement {
|
||||
Question::Several("Which parts?".into(), vec!["Forms".into(), "Charts".into()]),
|
||||
Question::Text("Anything else?".into()),
|
||||
],
|
||||
|answers, _, _| say(format!("{answers:?}")),
|
||||
)
|
||||
.on_submit(|answers, _, _| say(format!("{answers:?}")))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ fn show(cx: &mut VisualTestContext) {
|
||||
}
|
||||
|
||||
fn scanner() -> AnyElement {
|
||||
let scanner = QrCodeScanner::new("scanner", 1.0).on_scan(|text, _, _| say(text));
|
||||
let scanner = QrCodeScanner::new("scanner", 1.0, |text, _, _| say(text));
|
||||
match FRAME.with(|frame| frame.borrow().clone()) {
|
||||
Some(frame) => scanner.frame(frame),
|
||||
None => scanner,
|
||||
@@ -70,9 +70,8 @@ fn a_frame_that_comes_mid_read_is_read_next(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
fn captcha() -> AnyElement {
|
||||
Captcha::new("captcha", frame_of("W7XK"), 1.0)
|
||||
Captcha::new("captcha", frame_of("W7XK"), 1.0, |text, _, _| say(text))
|
||||
.state(STATE.get())
|
||||
.on_answer(|text, _, _| say(text))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use gpui::{
|
||||
AnyElement, Context, IntoElement, ParentElement, Render, Styled, TestAppContext, Window, div,
|
||||
px,
|
||||
};
|
||||
|
||||
use super::{press, settle, shown, stage};
|
||||
use crate::{
|
||||
misc::{
|
||||
CookieBanner, ExportDialog, ExportFormat, Flashcards, ImportDialog, ImportField, Poll,
|
||||
Question, Quiz, QuizQuestion, Survey,
|
||||
},
|
||||
theme::Theme,
|
||||
};
|
||||
|
||||
/// Parts whose lists change once `changed` turns: kinds, formats, fields, questions and a vote.
|
||||
struct Drifting {
|
||||
changed: bool,
|
||||
}
|
||||
|
||||
impl Drifting {
|
||||
fn parts(&self) -> Vec<AnyElement> {
|
||||
let changed = self.changed;
|
||||
let mut cookies =
|
||||
CookieBanner::new("cookies", "This page would set cookies.", |_, _, _| {})
|
||||
.kind("Analytics", "Counts visits.")
|
||||
.kind("Marketing", "Shows offers.");
|
||||
if changed {
|
||||
cookies = cookies.kind("Support", "Remembers chats.");
|
||||
}
|
||||
let formats = match changed {
|
||||
false => vec![ExportFormat::Pdf, ExportFormat::Png],
|
||||
true => vec![ExportFormat::Json],
|
||||
};
|
||||
let mut fields = vec![ImportField::new("name").required()];
|
||||
if changed {
|
||||
fields.push(ImportField::new("email"));
|
||||
}
|
||||
let first = match changed {
|
||||
false => Question::Choice("How often?".into(), vec!["Daily".into(), "Weekly".into()]),
|
||||
true => Question::Text("Anything else?".into()),
|
||||
};
|
||||
let poll = Poll::new("poll", "Lunch?", ["Soup", "Tea"], |_, _, _| {}).votes([1, 2]);
|
||||
vec![
|
||||
cookies.into_any_element(),
|
||||
ExportDialog::new("export", "Export", formats, |_, _, _| {}, |_, _| {})
|
||||
.into_any_element(),
|
||||
ImportDialog::new(
|
||||
"import",
|
||||
"Import",
|
||||
["Who", "Mail"],
|
||||
fields,
|
||||
|_, _, _| {},
|
||||
|_, _| {},
|
||||
)
|
||||
.into_any_element(),
|
||||
Survey::new("survey", [first], |_, _, _| {}).into_any_element(),
|
||||
match changed {
|
||||
false => poll.into_any_element(),
|
||||
true => poll.voted(9).into_any_element(),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Drifting {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().w(px(400.0)).children(self.parts())
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn parts_whose_lists_changed_fit_their_state_again(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Drifting { changed: false });
|
||||
settle(cx);
|
||||
view.update(cx, |view, cx| {
|
||||
view.changed = true;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Whether the staged quiz and deck show their shorter lists.
|
||||
static SHORTER: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
fn quiz() -> AnyElement {
|
||||
let one = || QuizQuestion::new("One?", ["A", "B"], 0);
|
||||
let questions = match SHORTER.get() {
|
||||
false => vec![one(), QuizQuestion::new("Two?", ["C", "D"], 0)],
|
||||
true => vec![one()],
|
||||
};
|
||||
Quiz::new("quiz", questions).into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_quiz_past_its_questions_starts_over(cx: &mut TestAppContext) {
|
||||
SHORTER.set(false);
|
||||
let (_, cx) = stage(quiz, cx);
|
||||
for key in ["tab", "down", "tab", "enter", "enter"] {
|
||||
press(key, cx);
|
||||
}
|
||||
SHORTER.set(true);
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn deck() -> AnyElement {
|
||||
let cards = match SHORTER.get() {
|
||||
false => vec![("A", "a"), ("B", "b"), ("C", "c")],
|
||||
true => vec![("A", "a")],
|
||||
};
|
||||
Flashcards::new("cards", cards).into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_deck_past_its_cards_starts_over(cx: &mut TestAppContext) {
|
||||
SHORTER.set(false);
|
||||
let (_, cx) = stage(deck, cx);
|
||||
for key in ["tab", "tab", "tab", "enter", "enter"] {
|
||||
press(key, cx);
|
||||
}
|
||||
assert!(shown("flashcard-front-C", cx));
|
||||
SHORTER.set(true);
|
||||
settle(cx);
|
||||
assert!(shown("flashcard-front-A", cx));
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
|
||||
mod asking;
|
||||
mod codes;
|
||||
mod drift;
|
||||
mod notices;
|
||||
mod numbers;
|
||||
mod transfer;
|
||||
|
||||
@@ -161,11 +161,12 @@ fn consent_hands_focus_back_to_its_opener(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
fn cookies() -> AnyElement {
|
||||
CookieBanner::new("cookies", "This page would set cookies.")
|
||||
.kind("Analytics", "Counts visits.")
|
||||
.kind("Marketing", "Shows offers.")
|
||||
.on_choose(|kinds, _, _| say(format!("{kinds:?}")))
|
||||
.into_any_element()
|
||||
CookieBanner::new("cookies", "This page would set cookies.", |kinds, _, _| {
|
||||
say(format!("{kinds:?}"))
|
||||
})
|
||||
.kind("Analytics", "Counts visits.")
|
||||
.kind("Marketing", "Shows offers.")
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Accept all names every kind and Reject all none; Choose turns into Save choices on its focus, which names the switches turned on.
|
||||
|
||||
+76
-29
@@ -66,7 +66,7 @@ impl RenderOnce for WebView {
|
||||
mod mac {
|
||||
use gpui::{
|
||||
AnyElement, App, Bounds, DispatchPhase, ElementId, IntoElement, MouseDownEvent,
|
||||
ParentElement, Pixels, Styled, Window, canvas, div,
|
||||
ParentElement, Pixels, SharedString, Styled, Window, canvas, div,
|
||||
};
|
||||
use wry::{
|
||||
Rect, WebViewBuilder,
|
||||
@@ -74,11 +74,13 @@ mod mac {
|
||||
};
|
||||
|
||||
use super::{WebSource, in_view};
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
/// The native view and what it shows.
|
||||
/// The native view, what it shows, and why the last load failed.
|
||||
struct Page {
|
||||
view: wry::WebView,
|
||||
shown: WebSource,
|
||||
fault: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Drop for Page {
|
||||
@@ -98,6 +100,20 @@ mod mac {
|
||||
}
|
||||
}
|
||||
|
||||
/// The words a box shows in place of a page that cannot show.
|
||||
fn fault(why: SharedString, cx: &App) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.p_4()
|
||||
.text_color(theme.colors.danger)
|
||||
.child(format!("This page cannot show: {why}"))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub(super) fn page(
|
||||
id: ElementId,
|
||||
source: WebSource,
|
||||
@@ -111,24 +127,54 @@ mod mac {
|
||||
WebSource::Url(url) => WebViewBuilder::new().with_url(url.to_string()),
|
||||
WebSource::Html(html) => WebViewBuilder::new().with_html(html.to_string()),
|
||||
};
|
||||
let view = builder
|
||||
.build_as_child(&*window)
|
||||
.unwrap_or_else(|error| panic!("web view {id:?}: wry built no view: {error}"));
|
||||
log::info!("web view {id:?}: built");
|
||||
Page {
|
||||
view,
|
||||
shown: source,
|
||||
match builder.build_as_child(&*window) {
|
||||
Ok(view) => {
|
||||
log::info!("web view {id:?}: built");
|
||||
Ok(Page {
|
||||
view,
|
||||
shown: source,
|
||||
fault: None,
|
||||
})
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("web view {id:?}: wry built no view: {error}");
|
||||
Err(SharedString::from(error.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if page.read(cx).shown != source {
|
||||
let loaded = match page.read(cx) {
|
||||
Err(why) => return fault(why.clone(), cx),
|
||||
Ok(built) if built.shown != source => Some(match &source {
|
||||
WebSource::Url(url) => built.view.load_url(url),
|
||||
WebSource::Html(html) => built.view.load_html(html),
|
||||
}),
|
||||
Ok(_) => None,
|
||||
};
|
||||
if let Some(loaded) = loaded {
|
||||
log::info!("web view {id:?}: loads anew");
|
||||
let loaded = match &source {
|
||||
WebSource::Url(url) => page.read(cx).view.load_url(url),
|
||||
WebSource::Html(html) => page.read(cx).view.load_html(html),
|
||||
};
|
||||
loaded.unwrap_or_else(|error| panic!("web view {id:?}: load failed: {error}"));
|
||||
page.update(cx, |page, _| page.shown = source);
|
||||
let fault = loaded.err().map(|error| {
|
||||
log::error!("web view {id:?}: load failed: {error}");
|
||||
SharedString::from(error.to_string())
|
||||
});
|
||||
page.update(cx, |page, _| {
|
||||
if let Ok(page) = page {
|
||||
page.shown = source;
|
||||
page.fault = fault;
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Ok(Page {
|
||||
fault: Some(why), ..
|
||||
}) = page.read(cx)
|
||||
{
|
||||
let why = why.clone();
|
||||
if let Ok(built) = page.read(cx)
|
||||
&& let Err(error) = built.view.set_visible(false)
|
||||
{
|
||||
log::error!("web view {id:?}: visibility: {error}");
|
||||
}
|
||||
return fault(why, cx);
|
||||
}
|
||||
let pressed = page.clone();
|
||||
div()
|
||||
@@ -137,23 +183,24 @@ mod mac {
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, window, cx| {
|
||||
let view = &page.read(cx).view;
|
||||
view.set_bounds(rect(bounds))
|
||||
.unwrap_or_else(|error| panic!("web view {id:?}: bounds: {error}"));
|
||||
view.set_visible(in_view(bounds, window.content_mask().bounds))
|
||||
.unwrap_or_else(|error| panic!("web view {id:?}: visibility: {error}"));
|
||||
let Ok(built) = page.read(cx) else { return };
|
||||
if let Err(error) = built.view.set_bounds(rect(bounds)) {
|
||||
log::error!("web view {id:?}: bounds: {error}");
|
||||
}
|
||||
let shown = in_view(bounds, window.content_mask().bounds);
|
||||
if let Err(error) = built.view.set_visible(shown) {
|
||||
log::error!("web view {id:?}: visibility: {error}");
|
||||
}
|
||||
},
|
||||
move |_, _, window, _| {
|
||||
window.on_mouse_event(move |_: &MouseDownEvent, phase, _, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
if phase == DispatchPhase::Capture
|
||||
&& let Ok(built) = pressed.read(cx)
|
||||
{
|
||||
log::debug!("web view: keys go back to gpui");
|
||||
pressed
|
||||
.read(cx)
|
||||
.view
|
||||
.focus_parent()
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("web view: keys stay with the page: {error}")
|
||||
});
|
||||
if let Err(error) = built.view.focus_parent() {
|
||||
log::error!("web view: keys stay with the page: {error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
+2
-1
@@ -56,7 +56,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
|
||||
- [x] P39 Theme — `src/theme`
|
||||
- [x] P40 i18n & a11y — `src/i18n`
|
||||
- [x] P41 Maps — `src/maps`
|
||||
- [ ] P42 Misc — `src/misc`
|
||||
- [x] P42 Misc — `src/misc`
|
||||
- [ ] P43 Library Tooling — `src/tooling`
|
||||
- [ ] P44 Rendering — `src/rendering`
|
||||
- [ ] P45 Recount, acceptance table
|
||||
@@ -107,3 +107,4 @@ One line per item: what changed, what stayed and why, review rounds.
|
||||
- P39: no change. Kept: palette and syntax tokens named from constant tables, chart hues checked against the fixed eight (the owner's contract). Review: 1 round.
|
||||
- P40: no change. Kept, as AGENTS.md says: catalogs checked against each other at construction, message keys and arguments from code, Ely's locale table, the owner's switch over its own catalogs, constant date patterns. Review: 1 round.
|
||||
- P41: no change. Kept: finite and unique values, places and zooms in range (the map's own moves clamp and wrap), heat weights, routes of two places, builder misuse, tile sources and zooms, unique pins, ISO codes, the bundled world. Review: 1 round.
|
||||
- P42: Captcha, CookieBanner, Poll, QrCodeScanner and Survey take their handlers in `new`; the macOS web view shows wry's build and load failures in its box and logs failed bounds, visibility and focus calls (checked by reading, as Windows cannot build that module; CI builds it); kept state fits lists that changed: cookie switches, export formats, licenses, poll picks and votes, survey sheets, import mappings, quiz and flashcard places; a picture with no frame reads no code. Kept: lists present, a question's right answer, counts per option, unit tables, places from their own choices, Vote behind a pick. Review: 2 rounds.
|
||||
Reference in new issue
Block a user