use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::{Rc, Weak},
sync::atomic::{AtomicU64, Ordering},
};
use incular_config::Constraints;
use incular_navigation::{ModalBarrier, ModalRoute, Navigator, Route, RouteId, RoutePresentation};
use incular_rendering::DisplayList;
use incular_widgets::{
AnimatedModalBarrier, ExcludeFocusTraversal, LayoutBuilder, SizedBox, Stack, Visibility,
Widget,
internal::{ElementId, Key, TreeError, WidgetTree},
};
use super::{
frame::{FrameStats, Runtime},
tasks::TaskScope,
};
use crate::{RouteTaskBinding, route_focus::RouteFocusState};
static OUTLET_SEQUENCE: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OutletPlacement {
Stacked {
visible: bool,
barrier: Option<ModalBarrier>,
},
ExternalOverlay,
}
impl OutletPlacement {
pub fn of(presentation: &RoutePresentation, visible: bool) -> Self {
if presentation.is_overlay() {
return Self::ExternalOverlay;
}
Self::Stacked {
visible,
barrier: presentation.barrier().cloned(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct FrameAttempt {
revision: u64,
active: Option<RouteId>,
members: Vec<RouteId>,
}
impl FrameAttempt {
fn capture(navigator: &Navigator) -> Self {
Self::of(navigator.revision(), &navigator.routes())
}
fn of(revision: u64, routes: &[Route]) -> Self {
Self {
revision,
active: routes.last().map(|route| route.id),
members: routes.iter().map(|route| route.id).collect(),
}
}
fn covers(&self, navigator: &Navigator) -> bool {
if navigator.revision() == self.revision {
return true;
}
let current = Self::capture(navigator);
current.active == self.active
&& current.members.len() == self.members.len()
&& self.members.iter().all(|id| current.members.contains(id))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct PendingTransition {
previous: Option<RouteId>,
incoming: Option<RouteId>,
saved: Option<ElementId>,
attempt: FrameAttempt,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct OutletId(u64);
impl std::fmt::Display for OutletId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "route-outlet-{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum OutletError {
#[error(
"{outlet} cannot present portal-managed overlay routes: {routes:?} \
(host overlay entries in an OverlayPortal separately)"
)]
UnsupportedPresentation {
outlet: OutletId,
routes: Vec<RouteId>,
},
#[error(
"{outlet} is already driven by runtime-{owner}: refusing runtime-{attempted} \
(one lifecycle per runtime; release the first claim before driving here)"
)]
DriverConflict {
outlet: OutletId,
owner: u64,
attempted: u64,
},
#[error(
"{outlet} nests under {parent}: drive the root instead of presenting \
the child separately (detach first to drive it standalone)"
)]
SeparateDrive { outlet: OutletId, parent: OutletId },
#[error("route outlet frame failed: {0}")]
Frame(#[from] TreeError),
}
struct AttemptGuard<'a> {
outlet: &'a Rc<RefCell<RouteOutlet>>,
armed: bool,
}
impl<'a> AttemptGuard<'a> {
fn arm(outlet: &'a Rc<RefCell<RouteOutlet>>) -> Self {
Self {
outlet,
armed: true,
}
}
fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for AttemptGuard<'_> {
fn drop(&mut self) {
if self.armed {
self.outlet.borrow().reset_signaled_tree();
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum OutletAttachError {
#[error("a route outlet cannot nest inside itself")]
SelfAttachment,
#[error(
"attaching this outlet would cycle cascade driving: \
it already reaches its parent"
)]
Cycle,
#[error(
"this outlet is already attached to a different parent: \
detach it explicitly before reattaching"
)]
AlreadyAttached,
}
pub struct RouteOutlet {
navigator: Navigator,
focus: RouteFocusState,
bindings: HashMap<RouteId, RouteTaskBinding>,
tags: Rc<RefCell<HashMap<RouteId, Key>>>,
nested: Rc<RefCell<Vec<Weak<RefCell<RouteOutlet>>>>>,
modal_route_scopes: bool,
parent: Rc<RefCell<Option<Weak<RefCell<RouteOutlet>>>>>,
driver: RefCell<Option<(u64, Weak<u64>)>>,
task_parent: TaskScope,
presented: Option<RouteId>,
pending: Option<PendingTransition>,
epoch: Cell<u64>,
composing: RefCell<Option<(u64, FrameAttempt)>>,
consumed: Option<FrameAttempt>,
signaled: Cell<u64>,
namespace: u64,
tag_sequence: Cell<u64>,
root_key: Key,
revision: Rc<Cell<u64>>,
mount: Cell<Option<ElementId>>,
}
impl RouteOutlet {
pub fn new(navigator: &Navigator, task_parent: &TaskScope) -> Self {
let tags: Rc<RefCell<HashMap<RouteId, Key>>> = Rc::default();
let tags_for_oracle = tags.clone();
let nested: Rc<RefCell<Vec<Weak<RefCell<RouteOutlet>>>>> = Rc::default();
let nested_for_oracle = nested.clone();
let focus = RouteFocusState::new(move |tree: &WidgetTree, id: ElementId| {
let mut chain = Vec::new();
let mut cursor = Some(id);
while let Some(current) = cursor {
chain.push(current);
cursor = tree.parent(current);
}
for nested in nested_for_oracle.borrow().iter() {
let Some(nested) = nested.upgrade() else {
continue;
};
let nested = nested.borrow();
if let Some(root) = tree.element_with_key(&nested.root_key)
&& (root == id || chain.contains(&root))
{
return None;
}
}
tags_for_oracle.borrow().iter().find_map(|(route, key)| {
tree.element_with_key(key)
.is_some_and(|root| chain.contains(&root))
.then_some(*route)
})
});
let namespace = OUTLET_SEQUENCE
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
.expect("route outlet identity space exhausted");
Self {
navigator: navigator.clone(),
focus,
bindings: HashMap::new(),
tags,
nested,
modal_route_scopes: false,
parent: Rc::default(),
driver: RefCell::default(),
task_parent: task_parent.clone(),
presented: None,
pending: None,
epoch: Cell::new(0),
composing: RefCell::default(),
consumed: None,
signaled: Cell::new(0),
namespace,
tag_sequence: Cell::new(0),
root_key: Key::String(format!("route-outlet-root-{namespace}")),
revision: Rc::new(Cell::new(0)),
mount: Cell::new(None),
}
}
pub fn attach_nested(
outlet: &Rc<RefCell<Self>>,
nested: &Rc<RefCell<Self>>,
) -> Result<(), OutletAttachError> {
if Rc::ptr_eq(outlet, nested) {
return Err(OutletAttachError::SelfAttachment);
}
if nested.borrow().reaches(outlet) {
return Err(OutletAttachError::Cycle);
}
{
let child = nested.borrow();
let mut parent = child.parent.borrow_mut();
if let Some(current) = parent.as_ref().and_then(Weak::upgrade) {
if Rc::ptr_eq(¤t, outlet) {
return Ok(());
}
return Err(OutletAttachError::AlreadyAttached);
}
*parent = Some(Rc::downgrade(outlet));
}
let outlet = outlet.borrow_mut();
let mut attached = outlet.nested.borrow_mut();
attached.retain(|existing| {
existing
.upgrade()
.is_some_and(|live| !Rc::ptr_eq(&live, nested))
});
attached.push(Rc::downgrade(nested));
Ok(())
}
pub fn detach_nested(outlet: &Rc<RefCell<Self>>, nested: &Rc<RefCell<Self>>) {
{
let child = nested.borrow_mut();
let mut parent = child.parent.borrow_mut();
let mine = parent
.as_ref()
.and_then(Weak::upgrade)
.is_some_and(|current| Rc::ptr_eq(¤t, outlet));
if mine {
parent.take();
child.driver.borrow_mut().take();
}
}
{
let outlet = outlet.borrow_mut();
outlet.nested.borrow_mut().retain(|existing| {
existing
.upgrade()
.is_some_and(|live| !Rc::ptr_eq(&live, nested))
});
}
}
fn reaches(&self, target: &Rc<RefCell<Self>>) -> bool {
let mut visited: Vec<*const RefCell<Self>> = Vec::new();
let mut stack: Vec<Rc<RefCell<Self>>> = self.live_nested();
while let Some(node) = stack.pop() {
if Rc::ptr_eq(&node, target) {
return true;
}
let address = Rc::as_ptr(&node);
if visited.contains(&address) {
continue;
}
visited.push(address);
stack.extend(node.borrow().live_nested());
}
false
}
#[must_use]
pub fn frame_drive_count(&self) -> u64 {
self.revision.get()
}
pub fn nested_widget(nested: &Rc<RefCell<Self>>) -> Widget {
let (revision, namespace) = {
let nested = nested.borrow();
(nested.revision.clone(), nested.namespace)
};
let nested_for_build = Rc::clone(nested);
Widget::stateful_builder(revision, move |_| {
nested_for_build.borrow().compose_and_record()
})
.with_key(Key::String(format!("route-outlet-slot-{namespace}")))
}
#[must_use]
pub fn attached_nested_count(&self) -> usize {
self.nested
.borrow()
.iter()
.filter(|weak| weak.upgrade().is_some())
.count()
}
fn live_nested(&self) -> Vec<Rc<RefCell<Self>>> {
self.nested
.borrow()
.iter()
.filter_map(Weak::upgrade)
.collect()
}
fn live_parent(&self) -> Option<Rc<RefCell<Self>>> {
self.parent.borrow().as_ref().and_then(Weak::upgrade)
}
fn live_driver(&self) -> Option<u64> {
self.driver
.borrow()
.as_ref()
.and_then(|(id, token)| token.upgrade().is_some().then_some(*id))
}
fn claim_driver(&self, runtime: &Runtime) {
*self.driver.borrow_mut() = Some((runtime.id(), runtime.driver_token()));
}
fn check_drivers(&self, runtime: &Runtime, is_root: bool) -> Result<(), OutletError> {
if is_root && let Some(parent) = self.live_parent() {
return Err(OutletError::SeparateDrive {
outlet: self.id(),
parent: parent.borrow().id(),
});
}
if let Some(owner) = self.live_driver()
&& owner != runtime.id()
{
return Err(OutletError::DriverConflict {
outlet: self.id(),
owner,
attempted: runtime.id(),
});
}
for nested in self.live_nested() {
nested.borrow().check_drivers(runtime, false)?;
}
Ok(())
}
fn claim_tree(&self, runtime: &Runtime) {
self.claim_driver(runtime);
for nested in self.live_nested() {
nested.borrow().claim_tree(runtime);
}
}
pub fn set_modal_route_scopes(&mut self, enabled: bool) {
self.modal_route_scopes = enabled;
}
#[must_use]
pub fn modal_route_scopes(&self) -> bool {
self.modal_route_scopes
}
#[must_use]
pub fn navigator(&self) -> &Navigator {
&self.navigator
}
#[must_use]
pub fn id(&self) -> OutletId {
OutletId(self.namespace)
}
#[must_use]
pub fn needs_frame(&self) -> bool {
let stale = match &self.consumed {
None => !self.navigator.routes().is_empty(),
Some(consumed) => consumed.revision != self.navigator.revision(),
};
stale
|| self
.live_nested()
.iter()
.any(|nested| nested.borrow().needs_frame())
}
fn preflight(&self) -> Result<(), OutletError> {
let unsupported: Vec<RouteId> = self
.navigator
.routes()
.iter()
.filter(|route| route.presentation.is_overlay())
.map(|route| route.id)
.collect();
if unsupported.is_empty() {
Ok(())
} else {
Err(OutletError::UnsupportedPresentation {
outlet: self.id(),
routes: unsupported,
})
}
}
fn preflight_tree(&self) -> Result<(), OutletError> {
self.preflight()?;
for nested in self.live_nested() {
nested.borrow().preflight_tree()?;
}
Ok(())
}
pub fn attach(
outlet: &Rc<RefCell<RouteOutlet>>,
runtime: &mut Runtime,
) -> Result<(), OutletError> {
{
let outlet = outlet.borrow();
if let Some(parent) = outlet.live_parent() {
return Err(OutletError::SeparateDrive {
outlet: outlet.id(),
parent: parent.borrow().id(),
});
}
if let Some(owner) = outlet.live_driver()
&& owner != runtime.id()
{
return Err(OutletError::DriverConflict {
outlet: outlet.id(),
owner,
attempted: runtime.id(),
});
}
}
let mount = {
let outlet = outlet.borrow();
runtime
.tree()
.element_with_key(&outlet.root_key)
.ok_or_else(|| {
OutletError::Frame(TreeError::InvalidWidgetConfiguration {
widget: "RouteOutlet",
reason: "mount outlet.widget() in the tree before attaching".to_owned(),
})
})?
};
let outlet_for_build = Rc::clone(outlet);
runtime
.register_builder(mount, move || {
outlet_for_build.borrow().compose_and_record()
})
.map_err(OutletError::Frame)?;
outlet.borrow().claim_driver(runtime);
outlet.borrow_mut().mount.set(Some(mount));
Ok(())
}
#[must_use]
pub fn widget(&self) -> Widget {
self.compose().0
}
fn compose(&self) -> (Widget, FrameAttempt) {
let routes = self.navigator.routes();
let attempt = FrameAttempt::of(self.navigator.revision(), &routes);
let mut visible = vec![false; routes.len()];
let mut occluded = false;
for (index, route) in routes.iter().enumerate().rev() {
visible[index] = !occluded;
if route.presentation.is_opaque() {
occluded = true;
}
}
let mut children = Vec::new();
for (route, visible) in routes.iter().zip(visible) {
let OutletPlacement::Stacked { visible, barrier } =
OutletPlacement::of(&route.presentation, visible)
else {
continue;
};
if !visible && !route.presentation.maintains_state() {
continue;
}
if visible && let Some(barrier) = barrier {
children.push(self.veil_widget(route.id, &barrier));
}
children.push(self.content_widget(route, visible));
}
let root: Widget = Stack::new(children).into();
(root.with_key(self.root_key.clone()), attempt)
}
fn compose_and_record(&self) -> Widget {
let (widget, attempt) = self.compose();
*self.composing.borrow_mut() = Some((self.epoch.get(), attempt));
widget
}
fn content_widget(&self, route: &Route, visible: bool) -> Widget {
let content = if self.modal_route_scopes {
Widget::environment_scope(
ModalRoute::new(route.clone(), self.navigator.clone()),
route.presented_child(),
)
} else {
route.presented_child()
};
let mut wrapped: Widget = Visibility::new(content)
.visible(visible)
.maintain_state(true)
.into();
if !visible {
wrapped = ExcludeFocusTraversal::new(wrapped).into();
}
wrapped.with_key(self.tag_for(route.id))
}
fn veil_widget(&self, route: RouteId, barrier: &ModalBarrier) -> Widget {
let navigator = self.navigator.clone();
let barrier = barrier.clone();
Widget::from(LayoutBuilder::new(move |_, constraints| {
let size = constraints.biggest();
let navigator = navigator.clone();
let mut veil = AnimatedModalBarrier::with_flat_color(barrier.color)
.dismissible(barrier.dismissible);
if let Some(label) = barrier.label.clone() {
veil = veil.semantics_label(label);
}
let veiled: Widget = veil
.on_dismiss(move || {
if navigator
.current()
.is_some_and(|current| current.id == route)
{
navigator.pop();
}
})
.into();
if size.width.is_finite() && size.height.is_finite() {
SizedBox::from_dimensions(Some(size.width), Some(size.height), Some(veiled)).into()
} else {
veiled
}
}))
.block_semantics()
}
#[must_use]
pub fn route_task_scope(&self, route: RouteId) -> Option<TaskScope> {
self.bindings
.get(&route)
.map(|binding| binding.scope().clone())
}
pub fn present_frame(
outlet: &Rc<RefCell<Self>>,
runtime: &mut Runtime,
constraints: Constraints,
) -> Result<(DisplayList, FrameStats), OutletError> {
outlet.borrow().preflight_tree()?;
outlet.borrow().check_drivers(runtime, true)?;
outlet.borrow().claim_tree(runtime);
{
let mut outlet = outlet.borrow_mut();
outlet.begin_frame(runtime);
}
let mut attempt = AttemptGuard::arm(outlet);
let attached = outlet
.borrow()
.mount
.get()
.filter(|mount| runtime.tree().element_exists(*mount));
if attached.is_none() {
outlet.borrow_mut().mount.set(None);
}
if let Some(mount) = attached {
match runtime.rebuild_from_builder(mount) {
Ok(()) => {}
Err(TreeError::MissingElement(_)) => {
outlet.borrow_mut().mount.set(None);
}
Err(error) => return Err(OutletError::Frame(error)),
}
}
let output = runtime.run_frame(constraints)?;
outlet.borrow().preflight_tree()?;
outlet.borrow_mut().reconcile_tree(runtime);
attempt.disarm();
outlet.borrow().flag_stale_frames(runtime);
Ok(output)
}
fn capture_transition(&mut self, runtime: &Runtime) {
if self.pending.is_some() {
return;
}
let active = self.navigator.current().map(|route| route.id);
if active == self.presented {
return;
}
let saved = self.presented.and_then(|previous| {
self.navigator
.lifetime_of(previous)
.is_some()
.then(|| self.focus.read_owned_focus(runtime, previous))
.flatten()
});
self.pending = Some(PendingTransition {
previous: self.presented,
incoming: active,
saved,
attempt: FrameAttempt::capture(&self.navigator),
});
}
fn commit_transition(&mut self, runtime: &mut Runtime) -> bool {
let active = self.navigator.current().map(|route| route.id);
let Some(pending) = self.pending.take() else {
return false;
};
if active != pending.incoming
|| pending
.incoming
.is_some_and(|incoming| self.navigator.lifetime_of(incoming).is_none())
|| !pending.attempt.covers(&self.navigator)
{
self.capture_transition(runtime);
return false;
}
if let Some(previous) = pending.previous {
self.focus.commit_saved(previous, pending.saved);
}
self.presented = active;
self.pending = None;
true
}
fn prune_removed(&mut self) {
self.focus.retain_mounted(&self.navigator);
self.bindings
.retain(|id, _| self.navigator.lifetime_of(*id).is_some());
self.tags
.borrow_mut()
.retain(|id, _| self.navigator.lifetime_of(*id).is_some());
}
fn ensure_bindings(&mut self) {
for route in self.navigator.routes() {
if !self.bindings.contains_key(&route.id)
&& let Some(lifetime) = self.navigator.lifetime_of(route.id)
{
self.bindings.insert(
route.id,
RouteTaskBinding::bind(&self.task_parent, &lifetime),
);
}
}
}
fn restore_or_retry(&mut self, runtime: &mut Runtime, transitioned: bool) {
let Some(id) = self.navigator.current().map(|route| route.id) else {
return;
};
if transitioned {
self.focus.restore_saved(runtime, &self.navigator, id);
} else {
self.focus.restore_if_vacant(runtime, &self.navigator, id);
}
}
fn adopt_consumed(&mut self) {
let Some((epoch, composing)) = self.composing.borrow().clone() else {
return;
};
if epoch != self.epoch.get() {
return;
}
if let Some(pending) = self.pending.as_mut()
&& composing.revision >= pending.attempt.revision
{
pending.attempt = composing.clone();
}
self.consumed = Some(composing);
}
fn flag_stale_frames(&self, runtime: &mut Runtime) {
let live = self.navigator.revision();
let stale = match &self.consumed {
None => !self.navigator.routes().is_empty(),
Some(consumed) => consumed.revision != live,
};
if stale && self.signaled.get() != live {
self.signaled.set(live);
runtime.request_frame();
}
for nested in self.live_nested() {
nested.borrow().flag_stale_frames(runtime);
}
}
fn reset_signaled_tree(&self) {
self.signaled.set(0);
for nested in self.live_nested() {
nested.borrow().reset_signaled_tree();
}
}
fn reconcile_presented(&mut self, runtime: &mut Runtime) {
self.adopt_consumed();
let transitioned = self.commit_transition(runtime);
self.prune_removed();
self.ensure_bindings();
self.restore_or_retry(runtime, transitioned);
}
fn begin_frame(&mut self, runtime: &Runtime) {
self.epoch.set(self.epoch.get().wrapping_add(1));
self.revision.set(
self.revision
.get()
.checked_add(1)
.expect("revision exhausted"),
);
self.capture_transition(runtime);
for nested in self.live_nested() {
nested.borrow_mut().begin_frame(runtime);
}
}
fn reconcile_tree(&mut self, runtime: &mut Runtime) {
for nested in self.live_nested() {
nested.borrow_mut().reconcile_tree(runtime);
}
self.reconcile_presented(runtime);
self.nested
.borrow_mut()
.retain(|weak| weak.upgrade().is_some());
}
fn tag_for(&self, route: RouteId) -> Key {
if let Some(key) = self.tags.borrow().get(&route) {
return key.clone();
}
let mut tags = self.tags.borrow_mut();
if let Some(key) = tags.get(&route) {
return key.clone();
}
let sequence = self.tag_sequence.get();
self.tag_sequence.set(sequence.wrapping_add(1));
let key = Key::String(format!("route-outlet-{}-{sequence}", self.namespace));
tags.insert(route, key.clone());
key
}
}