#[cfg(not(any(target_os = "ios", target_os = "macos")))]
compile_error!("the presentation Apple host targets iOS and macOS only");
use std::any::Any;
use std::cell::RefCell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use dispatch2::DispatchQueue;
use objc2::MainThreadMarker;
use objc2::rc::Retained;
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSApplication, NSWindow};
#[cfg(target_os = "ios")]
use objc2_ui_kit::{
UIApplication, UISceneActivationState, UIViewController, UIWindow, UIWindowScene,
};
#[cfg(target_os = "ios")]
pub(crate) type PresentingAnchor = UIViewController;
#[cfg(target_os = "macos")]
pub(crate) type PresentingAnchor = NSWindow;
pub(crate) fn on_main(f: impl FnOnce(MainThreadMarker) + Send + 'static) {
DispatchQueue::main().exec_async(move || {
let mtm = unsafe { MainThreadMarker::new_unchecked() };
if catch_unwind(AssertUnwindSafe(|| f(mtm))).is_err() {
log::error!(
"frust-native-widgets: a presentation step panicked on the main queue; its \
presentation resolves as a platform error"
);
}
});
}
#[cfg(target_os = "ios")]
pub(crate) fn presenting_anchor(mtm: MainThreadMarker) -> Option<Retained<PresentingAnchor>> {
let mut top = anchor_window(mtm)?.rootViewController()?;
while let Some(next) = top.presentedViewController() {
if next.isBeingDismissed() {
break;
}
top = next;
}
Some(top)
}
#[cfg(target_os = "macos")]
pub(crate) fn presenting_anchor(mtm: MainThreadMarker) -> Option<Retained<PresentingAnchor>> {
let app = NSApplication::sharedApplication(mtm);
non_sheet_anchor(app.keyWindow())
.or_else(|| non_sheet_anchor(app.mainWindow()))
.or_else(|| {
app.windows()
.iter()
.find(|window| window.isVisible() && !window.isSheet())
})
}
#[cfg(target_os = "macos")]
fn non_sheet_anchor(window: Option<Retained<NSWindow>>) -> Option<Retained<NSWindow>> {
let window = window?;
let is_sheet = window.isSheet();
let parent = is_sheet.then(|| window.sheetParent()).flatten();
match resolve_sheet(is_sheet, parent.as_ref().map(|p| p.isVisible())) {
SheetResolution::Itself => Some(window),
SheetResolution::Parent => parent,
SheetResolution::None => None,
}
}
#[cfg(target_os = "macos")]
fn resolve_sheet(is_sheet: bool, parent_is_visible: Option<bool>) -> SheetResolution {
if !is_sheet {
SheetResolution::Itself
} else if parent_is_visible == Some(true) {
SheetResolution::Parent
} else {
SheetResolution::None
}
}
#[cfg(target_os = "macos")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SheetResolution {
Itself,
Parent,
None,
}
#[cfg(target_os = "ios")]
fn anchor_window(mtm: MainThreadMarker) -> Option<Retained<UIWindow>> {
let scenes = UIApplication::sharedApplication(mtm).connectedScenes();
let mut fallback = None;
for scene in scenes.iter() {
let Some(window_scene) = scene.downcast_ref::<UIWindowScene>() else {
continue;
};
let Some(window) = window_scene
.keyWindow()
.or_else(|| window_scene.windows().firstObject())
else {
continue;
};
if scene.activationState() == UISceneActivationState::ForegroundActive {
return Some(window);
}
fallback = fallback.or(Some(window));
}
fallback
}
pub(crate) type AfterDismiss = Box<dyn FnOnce(MainThreadMarker)>;
pub(crate) trait LivePresentation: Any {
fn dismiss(self: Box<Self>, mtm: MainThreadMarker, then: Option<AfterDismiss>);
}
#[cfg_attr(not(target_os = "ios"), allow(dead_code))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DisplacedAction {
Repark,
ResolveAndContinue,
CloseThenContinue,
}
#[cfg_attr(not(target_os = "ios"), allow(dead_code))]
pub(crate) fn displaced_action(
is_being_dismissed: bool,
has_continuation: bool,
) -> DisplacedAction {
match (is_being_dismissed, has_continuation) {
(true, false) => DisplacedAction::Repark,
(true, true) => DisplacedAction::ResolveAndContinue,
(false, _) => DisplacedAction::CloseThenContinue,
}
}
struct LiveEntry {
generation: u64,
presentation: Box<dyn LivePresentation>,
}
thread_local! {
static LIVE: RefCell<Option<LiveEntry>> = const { RefCell::new(None) };
}
pub(crate) fn install_live(
_mtm: MainThreadMarker,
generation: u64,
presentation: Box<dyn LivePresentation>,
) -> Option<Box<dyn LivePresentation>> {
LIVE.with(|live| {
live.borrow_mut()
.replace(LiveEntry {
generation,
presentation,
})
.map(|displaced| displaced.presentation)
})
}
#[cfg(target_os = "macos")]
pub(crate) fn take_live_any(_mtm: MainThreadMarker) -> Option<Box<dyn LivePresentation>> {
LIVE.with(|live| live.borrow_mut().take().map(|entry| entry.presentation))
}
pub(crate) fn take_live(
_mtm: MainThreadMarker,
generation: u64,
) -> Option<Box<dyn LivePresentation>> {
LIVE.with(|live| {
let mut slot = live.borrow_mut();
match slot.as_ref() {
Some(entry) if entry.generation == generation => {
slot.take().map(|entry| entry.presentation)
}
_ => None,
}
})
}
pub(crate) fn take_live_as<P: LivePresentation>(
_mtm: MainThreadMarker,
generation: u64,
) -> Option<Box<P>> {
LIVE.with(|live| {
let mut slot = live.borrow_mut();
let matches = slot.as_ref().is_some_and(|entry| {
entry.generation == generation && (&*entry.presentation as &dyn Any).is::<P>()
});
if !matches {
return None;
}
let presentation: Box<dyn Any> = slot.take()?.presentation;
presentation.downcast::<P>().ok()
})
}
pub(crate) fn dismiss_live(generation: u64) {
on_main(move |mtm| {
if let Some(presentation) = take_live(mtm, generation) {
presentation.dismiss(mtm, None);
}
});
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::{SheetResolution, resolve_sheet};
#[test]
fn a_non_sheet_resolves_to_itself_whatever_the_parent_fact_says() {
assert_eq!(resolve_sheet(false, None), SheetResolution::Itself);
assert_eq!(resolve_sheet(false, Some(false)), SheetResolution::Itself);
assert_eq!(resolve_sheet(false, Some(true)), SheetResolution::Itself);
}
#[test]
fn a_sheet_resolves_to_its_visible_parent() {
assert_eq!(resolve_sheet(true, Some(true)), SheetResolution::Parent);
}
#[test]
fn a_sheet_with_no_visible_parent_has_no_candidate() {
assert_eq!(resolve_sheet(true, None), SheetResolution::None);
assert_eq!(resolve_sheet(true, Some(false)), SheetResolution::None);
}
}
#[cfg(test)]
mod displaced_action_tests {
use super::{DisplacedAction, displaced_action};
#[test]
fn already_dismissing_with_nothing_waiting_reparks() {
assert_eq!(displaced_action(true, false), DisplacedAction::Repark);
}
#[test]
fn already_dismissing_with_a_continuation_resolves_and_continues_at_once() {
assert_eq!(
displaced_action(true, true),
DisplacedAction::ResolveAndContinue
);
}
#[test]
fn not_yet_dismissing_always_closes_then_continues() {
assert_eq!(
displaced_action(false, false),
DisplacedAction::CloseThenContinue
);
assert_eq!(
displaced_action(false, true),
DisplacedAction::CloseThenContinue
);
}
}