use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use frust::{RwSignal, Set, Theme};
use super::theme::{argb_u32, is_dark};
use crate::present::{
self, AlertOutcome, AlertSpec, PresentError, Presentation, PresentationHandle, SheetHandle,
SheetOutcome, SheetSpec,
};
pub fn show_native_alert(spec: AlertSpec) -> Presentation<AlertOutcome> {
present::show_alert(spec)
}
pub fn show_native_alert_into(
spec: AlertSpec,
signal: RwSignal<Option<AlertOutcome>>,
) -> Result<PresentationHandle, PresentError> {
deliver_into(
show_native_alert(spec),
move |outcome| signal.set(Some(outcome)),
frust::spawn_local,
)
}
pub fn show_native_sheet(spec: SheetSpec) -> Presentation<SheetOutcome> {
present::show_sheet(spec)
}
pub fn show_native_sheet_into(
spec: SheetSpec,
signal: RwSignal<Option<SheetOutcome>>,
) -> Result<SheetHandle, PresentError> {
let presentation = show_native_sheet(spec);
let sheet_handle = presentation.sheet_handle();
deliver_into(
presentation,
move |outcome| signal.set(Some(outcome)),
frust::spawn_local,
)?;
sheet_handle.ok_or_else(|| {
PresentError::Platform("an accepted native sheet carried no handle".to_string())
})
}
impl SheetSpec {
#[must_use]
pub fn with_theme(mut self, theme: &Theme) -> Self {
self.tint = Some(argb_u32(theme.scheme().primary));
self.dark = Some(is_dark(theme));
self
}
}
type Delivery = Pin<Box<dyn Future<Output = ()>>>;
fn deliver_into<T: 'static>(
mut presentation: Presentation<T>,
write: impl FnOnce(T) + 'static,
spawn: impl FnOnce(Delivery),
) -> Result<PresentationHandle, PresentError> {
let Some(handle) = presentation.handle() else {
return Err(refusal(&mut presentation));
};
spawn(Box::pin(async move {
match presentation.await {
Ok(outcome) => write(outcome),
Err(err) => log::warn!(
"frust-native-widgets: a native presentation ended without an outcome to \
report: {err}"
),
}
}));
Ok(handle)
}
fn refusal<T>(presentation: &mut Presentation<T>) -> PresentError {
if let Some(err) = presentation.take_refusal() {
return err;
}
match Pin::new(presentation).poll(&mut Context::from_waker(Waker::noop())) {
Poll::Ready(Err(err)) => err,
Poll::Ready(Ok(_)) | Poll::Pending => {
PresentError::Platform("a refused native presentation carried no error".to_string())
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use frust::GetUntracked;
use super::*;
use crate::present::test_support::{pending_pair, with_slot_held};
use crate::present::{ActionRole, AlertStyle, DismissReason, SheetContent};
fn spec() -> AlertSpec {
AlertSpec::new("Delete draft?", "This cannot be undone.")
.with_action("keep", "Keep", ActionRole::Cancel)
.with_action("delete", "Delete", ActionRole::Destructive)
}
fn capture() -> (Rc<RefCell<Vec<Delivery>>>, impl FnOnce(Delivery)) {
let spawned = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&spawned);
(spawned, move |task| sink.borrow_mut().push(task))
}
fn poll(task: &mut Delivery) -> Poll<()> {
task.as_mut().poll(&mut Context::from_waker(Waker::noop()))
}
#[test]
fn the_adapter_writes_the_signal_exactly_once() {
let signal: RwSignal<Option<AlertOutcome>> = RwSignal::new(None);
let writes = Rc::new(RefCell::new(0));
let counter = Rc::clone(&writes);
let (tx, presentation) = pending_pair::<AlertOutcome>();
let (spawned, spawn) = capture();
let handle = deliver_into(
presentation,
move |outcome| {
*counter.borrow_mut() += 1;
signal.set(Some(outcome));
},
spawn,
);
assert!(handle.is_ok(), "an accepted request answers its handle");
let mut task = spawned.borrow_mut().pop().expect("one task spawned");
assert!(spawned.borrow().is_empty());
assert_eq!(poll(&mut task), Poll::Pending);
assert_eq!(signal.get_untracked(), None);
assert!(tx.send(Ok(AlertOutcome::Action("delete".into()))));
assert_eq!(poll(&mut task), Poll::Ready(()));
assert_eq!(
signal.get_untracked(),
Some(AlertOutcome::Action("delete".into()))
);
assert_eq!(*writes.borrow(), 1);
}
#[test]
fn a_late_error_is_logged_and_never_written() {
let signal: RwSignal<Option<AlertOutcome>> = RwSignal::new(None);
let (tx, presentation) = pending_pair::<AlertOutcome>();
let (spawned, spawn) = capture();
deliver_into(presentation, move |o| signal.set(Some(o)), spawn).expect("accepted");
let mut task = spawned.borrow_mut().pop().expect("one task spawned");
assert!(tx.send(Err(PresentError::NoHost)));
assert_eq!(poll(&mut task), Poll::Ready(()));
assert_eq!(signal.get_untracked(), None);
}
#[test]
fn busy_surfaces_as_err_from_both_forms() {
with_slot_held(|| {
let mut presentation = show_native_alert(spec());
assert_eq!(presentation.handle(), None);
assert_eq!(
Pin::new(&mut presentation).poll(&mut Context::from_waker(Waker::noop())),
Poll::Ready(Err(PresentError::Busy))
);
let signal: RwSignal<Option<AlertOutcome>> = RwSignal::new(None);
assert_eq!(
show_native_alert_into(spec(), signal),
Err(PresentError::Busy)
);
assert_eq!(signal.get_untracked(), None);
});
}
#[test]
fn an_invalid_spec_surfaces_as_err_and_spawns_nothing() {
let mut too_many = spec()
.with_action("a", "A", ActionRole::Default)
.with_action("b", "B", ActionRole::Default);
too_many.style = AlertStyle::ActionSheet;
let (spawned, spawn) = capture();
let written = Rc::new(RefCell::new(false));
let flag = Rc::clone(&written);
let result = deliver_into(
show_native_alert(too_many),
move |_| *flag.borrow_mut() = true,
spawn,
);
assert!(matches!(result, Err(PresentError::InvalidSpec(_))));
assert!(spawned.borrow().is_empty());
assert!(!*written.borrow());
}
fn sheet() -> SheetSpec {
SheetSpec::new(SheetContent::new().with_title("Share draft").with_action(
"copy",
"Copy link",
ActionRole::Default,
))
}
#[test]
fn the_adapter_writes_a_sheet_outcome_exactly_once() {
let signal: RwSignal<Option<SheetOutcome>> = RwSignal::new(None);
let (tx, presentation) = pending_pair::<SheetOutcome>();
let (spawned, spawn) = capture();
deliver_into(presentation, move |o| signal.set(Some(o)), spawn).expect("accepted");
let mut task = spawned.borrow_mut().pop().expect("one task spawned");
assert_eq!(poll(&mut task), Poll::Pending);
assert!(tx.send(Ok(SheetOutcome::Dismissed(DismissReason::User))));
assert_eq!(poll(&mut task), Poll::Ready(()));
assert_eq!(
signal.get_untracked(),
Some(SheetOutcome::Dismissed(DismissReason::User))
);
}
#[test]
fn busy_surfaces_as_err_from_both_sheet_forms() {
with_slot_held(|| {
let mut presentation = show_native_sheet(sheet());
assert_eq!(presentation.sheet_handle(), None);
assert_eq!(
Pin::new(&mut presentation).poll(&mut Context::from_waker(Waker::noop())),
Poll::Ready(Err(PresentError::Busy))
);
let signal: RwSignal<Option<SheetOutcome>> = RwSignal::new(None);
assert_eq!(
show_native_sheet_into(sheet(), signal),
Err(PresentError::Busy)
);
assert_eq!(signal.get_untracked(), None);
});
}
#[cfg(not(target_os = "ios"))]
#[test]
fn the_sheet_forms_are_unsupported_off_ios() {
let _guard = crate::present::test_support::serialize();
let signal: RwSignal<Option<SheetOutcome>> = RwSignal::new(None);
assert_eq!(
show_native_sheet_into(sheet(), signal),
Err(PresentError::Unsupported)
);
assert_eq!(signal.get_untracked(), None);
}
#[test]
fn with_theme_folds_the_accent_ink_and_brightness() {
let light = Theme::neutral().with_brightness(frust::Brightness::Light);
let themed = sheet().with_theme(&light);
assert_eq!(themed.tint, Some(argb_u32(light.scheme().primary)));
assert_eq!(themed.dark, Some(false));
let dark = Theme::neutral().with_brightness(frust::Brightness::Dark);
assert_eq!(sheet().with_theme(&dark).dark, Some(true));
assert_eq!(themed.content, sheet().content);
assert_eq!(themed.detents, sheet().detents);
}
}