Panics · P42 Misc: handlers in new; the web view shows wry's failures; kept state fits lists that changed

This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 07:17:20 -04:00
1 parent 770d13575d
commit 3a0a4bb699
20 files changed
+362 -189

No files matched your search

+9 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+7
View File
@@ -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
View File
@@ -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 {
+9 -6
View File
@@ -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
View File
@@ -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")
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+5 -4
View File
@@ -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()
}
+2 -3
View File
@@ -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()
}
+127
View File
@@ -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));
}
+1
View File
@@ -15,6 +15,7 @@ use crate::{
mod asking;
mod codes;
mod drift;
mod notices;
mod numbers;
mod transfer;
+6 -5
View File
@@ -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
View File
@@ -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}");
}
}
});
},