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
+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}");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user