use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, DiscardScene, EditingState, EventCtx,
EventResult, FrameTime, HeroDirective, HeroFrames, ImeState, InputEvent, LayoutCtx, PaintCtx,
PaintScene, PointerPhase, SemanticsCtx, SpringDesc, TOUCH_SLOP, View, Widget,
};
use frust_theme::Theme;
use kurbo::{Affine, Point, Rect, Size, Vec2};
use super::ambient::{
ambient_page_reach, ambient_swipe_claim, host_reachable, with_page_reach, with_swipe_claim,
};
use super::edge_swipe::{EDGE_SWIPE_ZONE_DP, EdgeSwipe};
use super::options::NavOp;
use super::path::Location;
use super::route_state::RouteStack;
use super::transition::{
Layer, PageTransition, TransitionDriver, TransitionSpec, TransitionState, lerp_rect,
make_driver, resolve_layers, resolve_spec, settle_driver,
};
pub use super::controller::NavigatorController;
pub use super::options::{
BackPolicy, NavigatorId, PageBuilder, PageVisibility, PopResult, PushOptions, ReplaceOptions,
ResultCallback, RouteChangeCallback, VisibilityCallback,
};
pub use super::view::{NavigatorView, navigator, overlay_host};
pub(crate) static NEXT_SNAPSHOT_KEY: AtomicU64 = AtomicU64::new(0);
pub(super) struct PageEntry<State: 'static> {
builder: PageBuilder<State>,
view: AnyView<State>,
pod: ChildPod,
opaque: bool,
on_result: Option<ResultCallback<State>>,
pub(super) transition: TransitionSpec,
pub(super) back: BackPolicy,
dismiss_signal: Option<Rc<Cell<u64>>>,
visibility: Option<PageVisibility>,
on_visibility: Option<VisibilityCallback>,
reconciled_covered: bool,
route: Option<Location>,
pub(super) pop_swipe: Option<bool>,
pub(super) reach: Rc<Cell<bool>>,
snapshot_key: u64,
}
pub(super) struct ActiveTransition<State: 'static> {
pub(super) driver: TransitionDriver,
pub(super) preset: PageTransition,
pub(super) is_pop: bool,
pub(super) stashed: Option<PageEntry<State>>,
pub(super) settle_spring: SpringDesc,
pub(super) settled: bool,
pub(super) interactive: bool,
pub(super) restore_on_finalize: bool,
pub(super) hero_leaving: HashMap<String, Rect>,
pub(super) hero_entering: HashMap<String, Rect>,
pub(super) pending_spec: Option<TransitionSpec>,
}
impl<State: 'static> crate::authoring::VisitPods for PageEntry<State> {
fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
visitor(&self.pod);
}
}
impl<State: 'static> crate::authoring::VisitPods for ActiveTransition<State> {
fn visit_pods(&self, visitor: &mut dyn FnMut(&ChildPod)) {
crate::authoring::VisitPods::visit_pods(&self.stashed, visitor);
}
}
pub struct NavigatorWidget<State: 'static> {
pub(super) pages: Vec<PageEntry<State>>,
pending_results: Vec<(ResultCallback<State>, PopResult)>,
pub(super) needs_ime_clear: bool,
default_transition: TransitionSpec,
pub(super) transition: Option<ActiveTransition<State>>,
pub(super) pop_swipe_enabled: bool,
pub(super) edge: EdgeSwipe,
last_frame_time: FrameTime,
depth: Rc<Cell<usize>>,
back_interest: Rc<Cell<bool>>,
transition_state: Rc<Cell<TransitionState>>,
mounted: Rc<Cell<usize>>,
cull_covered_builds: bool,
host_reach: Option<Rc<Cell<bool>>>,
route_stack: Rc<RefCell<RouteStack>>,
route_change: Option<RouteChangeCallback>,
}
fn compute_back_interest(depth: usize, top_policy: BackPolicy) -> bool {
depth > 1 || top_policy != BackPolicy::Pop
}
fn unmount_cell(mounted: &Cell<usize>) {
mounted.set(mounted.get().saturating_sub(1));
}
impl<State: 'static> NavigatorWidget<State> {
fn publish_state(&mut self) {
self.depth.set(self.pages.len());
let top_policy = self.pages.last().map(|p| p.back).unwrap_or(BackPolicy::Pop);
self.publish_reach();
self.back_interest
.set(compute_back_interest(self.pages.len(), top_policy));
let mut t = self.transition_state.get();
t.to_depth = self.pages.len();
if !t.active {
t.from_depth = t.to_depth;
}
self.transition_state.set(t);
self.publish_visibility();
self.publish_route_stack();
}
fn publish_route_stack(&mut self) {
if self.transition.as_ref().is_some_and(|t| t.interactive) {
return;
}
let unchanged = {
let published = self.route_stack.borrow();
let entries = published.entries();
entries.len() == self.pages.len()
&& entries
.iter()
.zip(self.pages.iter())
.all(|(prev, page)| *prev == page.route)
};
if unchanged {
return;
}
let entries: Vec<Option<Location>> = self.pages.iter().map(|p| p.route.clone()).collect();
self.route_stack.borrow_mut().set(entries);
if let Some(observer) = self.route_change.clone() {
observer(&self.route_stack.borrow());
}
}
fn reachable(&self) -> bool {
host_reachable(self.host_reach.as_ref())
}
fn publish_reach(&mut self) {
let own = self.reachable();
let routed = self.input_routed_pages();
for index in 0..self.pages.len() {
self.pages[index].reach.set(own && routed.contains(&index));
}
}
fn publish_visibility(&mut self) {
for i in 0..self.pages.len() {
let next = self.visibility_of(i);
if self.pages[i].visibility == Some(next) {
continue;
}
self.pages[i].visibility = Some(next);
if let Some(observer) = self.pages[i].on_visibility.clone() {
observer(next);
}
}
}
fn visibility_of(&self, index: usize) -> PageVisibility {
if index + 1 >= self.pages.len() {
PageVisibility::Current
} else if index < self.base_visible_index() {
PageVisibility::Covered
} else {
PageVisibility::Visible
}
}
fn base_visible_index(&self) -> usize {
for i in (0..self.pages.len()).rev() {
if self.pages[i].opaque {
return i;
}
}
0
}
pub(super) fn publish_transition_start(
&self,
from_depth: usize,
progress: f64,
is_pop: bool,
interactive: bool,
) {
let to_depth = self.pages.len();
let generation = self.transition_state.get().generation.wrapping_add(1);
self.transition_state.set(TransitionState {
active: true,
progress,
is_pop,
interactive,
from_depth,
to_depth,
generation,
});
}
pub(super) fn publish_transition_progress(&self, progress: f64, interactive: Option<bool>) {
let mut t = self.transition_state.get();
t.progress = progress;
if let Some(interactive) = interactive {
t.interactive = interactive;
}
self.transition_state.set(t);
}
pub(super) fn cancel_top(&mut self) {
if let Some(top) = self.pages.last_mut() {
if top.pod.is_active() {
crate::authoring::cancel_pod(&mut top.pod);
top.pod.set_active(false);
}
if top.pod.is_focused() {
top.pod.set_focused(false);
}
}
}
pub(super) fn top_pod_focused(&self) -> bool {
self.pages.last().is_some_and(|p| p.pod.is_focused())
}
fn effective_spec(&self, over: Option<TransitionSpec>) -> TransitionSpec {
over.unwrap_or(self.default_transition)
}
fn start_transition(
&mut self,
spec: TransitionSpec,
is_pop: bool,
stashed: Option<PageEntry<State>>,
ctx: &mut BuildCtx<'_>,
) {
self.finalize_transition(ctx);
if let Some(leaving) = stashed.as_ref() {
leaving.reach.set(false);
}
let to_depth = self.pages.len();
let from_depth = if is_pop {
to_depth + 1
} else if stashed.is_some() {
to_depth
} else {
to_depth.saturating_sub(1)
};
let (driver, settle_spring) = make_driver(spec.timing);
self.transition = Some(ActiveTransition {
driver,
preset: spec.preset,
is_pop,
stashed,
settle_spring,
settled: false,
interactive: false,
restore_on_finalize: false,
hero_leaving: HashMap::new(),
hero_entering: HashMap::new(),
pending_spec: Some(spec),
});
self.publish_transition_start(from_depth, 0.0, is_pop, false);
}
fn finalize_transition(&mut self, ctx: &mut BuildCtx<'_>) {
if let Some(mut t) = self.transition.take() {
self.edge.active = false;
self.edge.armed = false;
self.edge.inner_claimed = false;
if let Some(mut stashed) = t.stashed.take() {
if t.restore_on_finalize {
stashed.pod.set_origin(Point::ZERO);
self.pages.push(stashed);
} else {
if t.interactive
&& let Some(callback) = stashed.on_result.take()
{
self.pending_results.push((callback, PopResult::empty()));
frust_core::mark_pending_result_flush();
}
crate::authoring::teardown_child(&stashed.view, &mut stashed.pod, ctx);
}
}
let generation = self.transition_state.get().generation;
self.transition_state
.set(TransitionState::settled(self.pages.len(), generation));
}
}
pub fn set_transition_progress(&mut self, p: f64) {
let Some(t) = self.transition.as_mut() else {
return;
};
t.driver = TransitionDriver::Held { value: p };
t.settled = false;
self.publish_transition_progress(p, Some(true));
}
pub fn settle_transition(&mut self, velocity: f64) {
let Some(t) = self.transition.as_mut() else {
return;
};
let from = t.driver.value();
let target = if velocity >= 0.0 { 1.0 } else { 0.0 };
t.driver = settle_driver(t.settle_spring, from, velocity, target);
t.settled = false;
self.publish_transition_progress(from, Some(false));
}
fn event_time_ms(&self) -> f64 {
self.last_frame_time.as_secs_f64() * 1000.0
}
fn input_routed_pages(&self) -> std::ops::Range<usize> {
self.pages.len().saturating_sub(1)..self.pages.len()
}
fn route_top(&mut self, ctx: &mut EventCtx<'_>, event: &InputEvent) -> EventResult {
for i in self.input_routed_pages().rev() {
if crate::authoring::route_event_single(&mut self.pages[i].pod, ctx, event)
== EventResult::Handled
{
return EventResult::Handled;
}
}
EventResult::Ignored
}
fn event_at(&mut self, ctx: &mut EventCtx<'_>, event: &InputEvent, t_ms: f64) -> EventResult {
if self.edge.active {
return self.drive_edge_swipe(ctx, event, t_ms);
}
if self.transition.is_some() {
return EventResult::Ignored;
}
let InputEvent::Pointer(p) = event else {
return self.route_top(ctx, event);
};
match p.phase {
PointerPhase::Down => {
self.edge.armed = crate::authoring::presses(p)
&& self.swipe_armable()
&& self.pages.len() > 1
&& p.position.x <= EDGE_SWIPE_ZONE_DP;
if self.edge.armed {
self.edge.down_start = p.position;
self.edge.tracker.clear();
self.edge.tracker.record(t_ms, p.position.x);
}
let claim = Rc::new(Cell::new(false));
let routed = with_swipe_claim(&claim, || self.route_top(ctx, event));
self.edge.inner_claimed = claim.get();
if let Some(host) = ambient_swipe_claim() {
host.set(self.edge.inner_claimed || self.edge.armed);
}
routed
}
PointerPhase::Move => {
if !self.edge.armed {
return self.route_top(ctx, event);
}
self.edge.tracker.record(t_ms, p.position.x);
let dx = p.position.x - self.edge.down_start.x;
let dy = p.position.y - self.edge.down_start.y;
if dx > TOUCH_SLOP && dx.abs() > dy.abs() {
if self.edge.inner_claimed {
self.edge.armed = false;
return self.route_top(ctx, event);
}
if self.pages.len() <= 1 || !self.swipe_armable() {
self.edge.armed = false;
return self.route_top(ctx, event);
}
let width = ctx.size().width.max(1.0);
let progress = (dx / width).clamp(0.0, 1.0);
self.begin_interactive_pop(progress);
self.edge.armed = false;
self.edge.active = true;
ctx.capture_pointer();
ctx.request_redraw();
EventResult::Handled
} else if dy.abs() > TOUCH_SLOP || dx < -TOUCH_SLOP {
self.edge.armed = false;
self.edge.inner_claimed = false;
self.route_top(ctx, event)
} else {
self.route_top(ctx, event)
}
}
PointerPhase::Up | PointerPhase::Cancel => {
self.edge.armed = false;
self.edge.inner_claimed = false;
self.route_top(ctx, event)
}
}
}
fn apply_pop(&mut self, result: PopResult, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if self.pages.len() > 1 {
let outgoing_focused = self.top_pod_focused();
self.cancel_top();
self.edge.armed = false;
self.edge.inner_claimed = false;
let mut popped = self.pages.pop().expect("len checked > 1");
let spec = popped.transition;
if let Some(callback) = popped.on_result.take() {
self.pending_results.push((callback, result));
frust_core::mark_pending_result_flush();
}
if outgoing_focused && ctx.has_focus() {
self.needs_ime_clear = true;
}
if spec.is_animated() {
self.start_transition(spec, true, Some(popped), ctx);
} else {
crate::authoring::teardown_child(&popped.view, &mut popped.pod, ctx);
}
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
flags
}
fn apply_request_back(&mut self, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
let policy = self.pages.last().map(|p| p.back).unwrap_or(BackPolicy::Pop);
match policy {
BackPolicy::Pop => self.apply_pop(PopResult::empty(), ctx),
BackPolicy::DismissAnimated => {
if let Some(top) = self.pages.last()
&& let Some(signal) = &top.dismiss_signal
{
signal.set(signal.get().wrapping_add(1));
}
ChangeFlags::PAINT
}
BackPolicy::Veto => ChangeFlags::NONE,
}
}
fn apply_ops(&mut self, ops: Vec<NavOp<State>>, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
for op in ops {
match op {
NavOp::Push {
builder,
opaque,
on_result,
transition,
back,
dismiss_signal,
on_visibility,
route,
pop_swipe,
} => {
let spec = self.effective_spec(transition);
let covered_focused = self.top_pod_focused();
self.cancel_top();
self.edge.armed = false;
self.edge.inner_claimed = false;
let reach = Rc::new(Cell::new(self.reachable()));
let (view, pod) = with_page_reach(&reach, || {
let view = builder();
let pod = crate::authoring::build_child(&view, ctx);
(view, pod)
});
self.pages.push(PageEntry {
builder,
view,
pod,
opaque,
on_result,
transition: spec,
back,
dismiss_signal,
visibility: None,
on_visibility,
reconciled_covered: false,
route,
pop_swipe,
reach,
snapshot_key: NEXT_SNAPSHOT_KEY.fetch_add(1, Ordering::Relaxed),
});
if covered_focused && ctx.has_focus() {
self.needs_ime_clear = true;
}
if spec.is_animated() && self.pages.len() >= 2 {
self.start_transition(spec, false, None, ctx);
}
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
NavOp::Pop { result } => {
flags |= self.apply_pop(result, ctx);
}
NavOp::RequestBack => {
flags |= self.apply_request_back(ctx);
}
NavOp::Replace {
builder,
opaque,
transition,
route,
} => {
let spec = self.effective_spec(transition);
let outgoing_focused = self.top_pod_focused();
self.cancel_top();
self.edge.armed = false;
self.edge.inner_claimed = false;
let reach = Rc::new(Cell::new(self.reachable()));
let (view, pod) = with_page_reach(&reach, || {
let view = builder();
let pod = crate::authoring::build_child(&view, ctx);
(view, pod)
});
if let Some(top) = self.pages.last_mut() {
let entry = PageEntry {
builder,
view,
pod,
opaque,
on_result: None,
transition: spec,
back: BackPolicy::Pop,
dismiss_signal: None,
visibility: None,
on_visibility: None,
reconciled_covered: false,
route,
pop_swipe: None,
reach: Rc::clone(&reach),
snapshot_key: NEXT_SNAPSHOT_KEY.fetch_add(1, Ordering::Relaxed),
};
if spec.is_animated() {
let old = std::mem::replace(top, entry);
self.start_transition(spec, false, Some(old), ctx);
} else {
crate::authoring::teardown_child(&top.view, &mut top.pod, ctx);
*top = entry;
}
} else {
self.pages.push(PageEntry {
builder,
view,
pod,
opaque,
on_result: None,
transition: spec,
back: BackPolicy::Pop,
dismiss_signal: None,
visibility: None,
on_visibility: None,
reconciled_covered: false,
route,
pop_swipe: None,
reach,
snapshot_key: NEXT_SNAPSHOT_KEY.fetch_add(1, Ordering::Relaxed),
});
}
if outgoing_focused && ctx.has_focus() {
self.needs_ime_clear = true;
}
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
}
}
self.publish_state();
flags
}
fn layout_transition(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let mut size = Size::ZERO;
if let Some(entry) = self.pages.last_mut() {
let child = entry.pod.layout_child(ctx, bc);
entry.pod.set_origin(Point::ZERO);
size = Size::new(size.width.max(child.width), size.height.max(child.height));
}
let has_stashed = self
.transition
.as_ref()
.map(|t| t.stashed.is_some())
.unwrap_or(false);
if has_stashed {
if let Some(t) = self.transition.as_mut()
&& let Some(stashed) = t.stashed.as_mut()
{
let child = stashed.pod.layout_child(ctx, bc);
stashed.pod.set_origin(Point::ZERO);
size = Size::new(size.width.max(child.width), size.height.max(child.height));
}
} else {
let n = self.pages.len();
if n >= 2 {
let child = self.pages[n - 2].pod.layout_child(ctx, bc);
self.pages[n - 2].pod.set_origin(Point::ZERO);
size = Size::new(size.width.max(child.width), size.height.max(child.height));
}
}
bc.constrain(size)
}
fn paint_transition(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if let Some(spec) = self.transition.as_ref().and_then(|t| t.pending_spec) {
if let Some(theme) = Theme::from_paint_ctx(ctx) {
let resolved = resolve_spec(spec, Some(&theme.motion));
let (driver, settle_spring) = make_driver(resolved.timing);
if let Some(t) = self.transition.as_mut() {
t.driver = driver;
t.preset = resolved.preset;
t.settle_spring = settle_spring;
}
}
if let Some(t) = self.transition.as_mut() {
t.pending_spec = None;
}
}
let (adv, is_pop, preset, has_stashed) = {
let t = self.transition.as_mut().expect("transition present");
let adv = t.driver.advance(ctx.frame_time());
(adv, t.is_pop, t.preset, t.stashed.is_some())
};
self.publish_transition_progress(adv.value, None);
let area = ctx.size();
let nav_origin = ctx.origin();
let (enter_layer, leave_layer) = resolve_layers(preset, adv.value, is_pop, area);
let (enter_dirs, leave_dirs) =
self.hero_directives(adv.value.clamp(0.0, 1.0), is_pop, nav_origin);
scene.push_clip(nav_origin, area);
let (new_entering, new_leaving);
if is_pop {
new_entering =
self.paint_entering_heroes(ctx, scene, enter_layer, area, nav_origin, enter_dirs);
new_leaving = self.paint_leaving_heroes(
ctx,
scene,
leave_layer,
area,
nav_origin,
has_stashed,
leave_dirs,
);
} else {
new_leaving = self.paint_leaving_heroes(
ctx,
scene,
leave_layer,
area,
nav_origin,
has_stashed,
leave_dirs,
);
new_entering =
self.paint_entering_heroes(ctx, scene, enter_layer, area, nav_origin, enter_dirs);
}
scene.pop_clip();
if let Some(t) = self.transition.as_mut() {
t.hero_entering = new_entering;
t.hero_leaving = new_leaving;
}
if adv.animating {
ctx.request_frame();
}
if adv.done {
if let Some(t) = self.transition.as_mut() {
t.settled = true;
}
ctx.request_frame();
}
}
fn hero_directives(
&self,
p: f64,
is_pop: bool,
nav_origin: Point,
) -> (
HashMap<String, HeroDirective>,
HashMap<String, HeroDirective>,
) {
let mut enter_dirs = HashMap::new();
let mut leave_dirs = HashMap::new();
let Some(t) = self.transition.as_ref() else {
return (enter_dirs, leave_dirs);
};
for (tag, leaving_rect) in &t.hero_leaving {
let Some(entering_rect) = t.hero_entering.get(tag) else {
continue;
};
let interp = lerp_rect(*leaving_rect, *entering_rect, p);
let dest =
Rect::from_origin_size(nav_origin + interp.origin().to_vec2(), interp.size());
let (painter, suppressed) = if is_pop {
(&mut leave_dirs, &mut enter_dirs)
} else {
(&mut enter_dirs, &mut leave_dirs)
};
painter.insert(tag.clone(), HeroDirective::Morph { dest });
suppressed.insert(tag.clone(), HeroDirective::Suppress);
}
(enter_dirs, leave_dirs)
}
fn paint_entering_heroes(
&mut self,
ctx: &mut PaintCtx,
scene: &mut dyn PaintScene,
layer: Layer,
area: Size,
nav_origin: Point,
directives: HashMap<String, HeroDirective>,
) -> HashMap<String, Rect> {
let snapshot_eligible = self.snapshot_eligible(&directives);
if let Some(entry) = self.pages.last_mut() {
let snapshot = snapshot_eligible.then_some(entry.snapshot_key);
let reference = nav_origin + Vec2::new(layer.dx, layer.dy);
paint_page_heroes(
&mut entry.pod,
ctx,
scene,
layer,
area,
reference,
directives,
snapshot,
)
} else {
HashMap::new()
}
}
#[allow(clippy::too_many_arguments)]
fn paint_leaving_heroes(
&mut self,
ctx: &mut PaintCtx,
scene: &mut dyn PaintScene,
layer: Layer,
area: Size,
nav_origin: Point,
has_stashed: bool,
directives: HashMap<String, HeroDirective>,
) -> HashMap<String, Rect> {
let reference = nav_origin + Vec2::new(layer.dx, layer.dy);
let snapshot_eligible = self.snapshot_eligible(&directives);
if has_stashed {
if let Some(t) = self.transition.as_mut()
&& let Some(stashed) = t.stashed.as_mut()
{
let snapshot = snapshot_eligible.then_some(stashed.snapshot_key);
return paint_page_heroes(
&mut stashed.pod,
ctx,
scene,
layer,
area,
reference,
directives,
snapshot,
);
}
HashMap::new()
} else {
let n = self.pages.len();
if n >= 2 {
let snapshot = snapshot_eligible.then_some(self.pages[n - 2].snapshot_key);
paint_page_heroes(
&mut self.pages[n - 2].pod,
ctx,
scene,
layer,
area,
reference,
directives,
snapshot,
)
} else {
HashMap::new()
}
}
}
fn snapshot_eligible(&self, directives: &HashMap<String, HeroDirective>) -> bool {
let programmatic = self.transition.as_ref().is_some_and(|t| !t.interactive);
programmatic && directives.is_empty()
}
}
#[allow(clippy::too_many_arguments)]
fn paint_page_heroes(
pod: &mut ChildPod,
ctx: &mut PaintCtx,
scene: &mut dyn PaintScene,
layer: Layer,
area: Size,
reference: Point,
directives: HashMap<String, HeroDirective>,
snapshot: Option<u64>,
) -> HashMap<String, Rect> {
let registry = RefCell::new(HeroFrames::new(reference, directives));
ctx.with_hero_registry(®istry, |page_ctx| {
paint_page_layer(pod, page_ctx, scene, layer, area, snapshot);
});
registry.into_inner().into_captured()
}
fn scale_about(pivot: Point, scale: f64) -> Affine {
Affine::translate((pivot.x, pivot.y))
* Affine::scale(scale)
* Affine::translate((-pivot.x, -pivot.y))
}
fn paint_page_layer(
pod: &mut ChildPod,
ctx: &mut PaintCtx,
scene: &mut dyn PaintScene,
layer: Layer,
area: Size,
snapshot: Option<u64>,
) {
pod.set_origin(Point::new(layer.dx, layer.dy));
let alpha = layer.alpha.clamp(0.0, 1.0);
if alpha <= 0.0 {
let mut sink = DiscardScene;
pod.paint_child(ctx, &mut sink);
return;
}
if let Some(key) = snapshot {
let bracket_origin = ctx.origin() + Vec2::new(layer.dx, layer.dy);
scene.push_snapshot(key, bracket_origin, area, alpha, layer.scale);
pod.paint_child(ctx, scene);
scene.pop_snapshot();
return;
}
let has_scale = (layer.scale - 1.0).abs() > f64::EPSILON;
let has_alpha = alpha < 1.0;
if has_scale {
let origin = ctx.origin();
let pivot = Point::new(origin.x + area.width / 2.0, origin.y + area.height / 2.0);
scene.push_transform(scale_about(pivot, layer.scale));
}
if has_alpha {
scene.push_layer(ctx.origin(), area, alpha);
}
pod.paint_child(ctx, scene);
if has_alpha {
scene.pop_layer();
}
if has_scale {
scene.pop_transform();
}
}
impl<State: 'static> View<State> for NavigatorView<State> {
type Element = NavigatorWidget<State>;
fn build(&self, ctx: &mut BuildCtx<'_>) -> NavigatorWidget<State> {
self.controller.mount();
let host_reach = ambient_page_reach();
self.controller.bind_host_reach(host_reach.clone());
let root_reach = Rc::new(Cell::new(host_reachable(host_reach.as_ref())));
let (view, pod) = with_page_reach(&root_reach, || {
let view = (self.initial)();
let pod = crate::authoring::build_child(&view, ctx);
(view, pod)
});
let mut widget = NavigatorWidget {
pages: vec![PageEntry {
builder: self.initial.clone(),
view,
pod,
opaque: true,
on_result: None,
transition: self.default_transition,
back: BackPolicy::Pop,
dismiss_signal: None,
visibility: None,
on_visibility: self.root_visibility.clone(),
reconciled_covered: false,
route: self.root_route.clone(),
pop_swipe: None,
reach: root_reach,
snapshot_key: NEXT_SNAPSHOT_KEY.fetch_add(1, Ordering::Relaxed),
}],
pending_results: Vec::new(),
needs_ime_clear: false,
default_transition: self.default_transition,
transition: None,
pop_swipe_enabled: self.resolve_pop_swipe(),
edge: EdgeSwipe::new(),
last_frame_time: FrameTime::ZERO,
depth: Rc::clone(&self.controller.depth),
back_interest: Rc::clone(&self.controller.back_interest),
transition_state: Rc::clone(&self.controller.transition),
mounted: Rc::clone(&self.controller.mounted),
cull_covered_builds: self.cull_covered_builds,
host_reach,
route_stack: Rc::clone(&self.controller.route_stack),
route_change: self.route_change.clone(),
};
let ops = self.controller.drain();
if !ops.is_empty() {
widget.apply_ops(ops, ctx);
}
widget.publish_state();
widget
}
fn rebuild(
&self,
_prev: &Self,
element: &mut NavigatorWidget<State>,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if self.controller.id() != _prev.controller.id() {
unmount_cell(&element.mounted);
element.depth = Rc::clone(&self.controller.depth);
element.back_interest = Rc::clone(&self.controller.back_interest);
element.transition_state = Rc::clone(&self.controller.transition);
element.mounted = Rc::clone(&self.controller.mounted);
element.route_stack = Rc::clone(&self.controller.route_stack);
self.controller.mount();
}
element.default_transition = self.default_transition;
element.pop_swipe_enabled = self.resolve_pop_swipe();
element.route_change = self.route_change.clone();
element.cull_covered_builds = self.cull_covered_builds;
element.host_reach = ambient_page_reach();
self.controller.bind_host_reach(element.host_reach.clone());
element.publish_reach();
let ops = self.controller.drain();
if !ops.is_empty() {
flags |= element.apply_ops(ops, ctx);
}
if element
.transition
.as_ref()
.map(|t| t.settled)
.unwrap_or(false)
{
element.finalize_transition(ctx);
element.publish_state();
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
let cull = element.cull_covered_builds;
for index in 0..element.pages.len() {
let covered = element.pages[index].visibility == Some(PageVisibility::Covered);
let skip = cull && covered && element.pages[index].reconciled_covered;
element.pages[index].reconciled_covered = covered;
if skip {
continue;
}
let reach = Rc::clone(&element.pages[index].reach);
flags |= with_page_reach(&reach, || {
let entry = &mut element.pages[index];
let next_view = (entry.builder)();
let child_flags =
crate::authoring::rebuild_child(&entry.view, &next_view, &mut entry.pod, ctx);
entry.view = next_view;
child_flags
});
}
element.publish_state();
flags
}
fn teardown(&self, element: &mut NavigatorWidget<State>, ctx: &mut BuildCtx<'_>) {
element.finalize_transition(ctx);
for entry in &mut element.pages {
crate::authoring::teardown_child(&entry.view, &mut entry.pod, ctx);
}
unmount_cell(&element.mounted);
}
}
impl<State: 'static> Widget for NavigatorWidget<State> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
if self.transition.is_some() {
return self.layout_transition(ctx, bc);
}
let start = self.base_visible_index();
let mut size = Size::ZERO;
for entry in &mut self.pages[start..] {
let child = entry.pod.layout_child(ctx, bc);
entry.pod.set_origin(Point::ZERO);
size = Size::new(size.width.max(child.width), size.height.max(child.height));
}
bc.constrain(size)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.last_frame_time = ctx.frame_time();
if self.transition.is_some() {
self.paint_transition(ctx, scene);
} else {
let start = self.base_visible_index();
for entry in &mut self.pages[start..] {
entry.pod.paint_child(ctx, scene);
}
}
if self.needs_ime_clear {
ctx.publish_ime_state(cleared_ime_state());
self.needs_ime_clear = false;
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if !self.pending_results.is_empty() {
let pending = std::mem::take(&mut self.pending_results);
let state = ctx.state_mut::<State>();
for (callback, result) in pending {
callback(state, result);
}
}
if event.is_broadcast() {
for entry in &mut self.pages {
crate::authoring::route_event_single(&mut entry.pod, ctx, event);
}
return EventResult::Ignored;
}
let t_ms = self.event_time_ms();
self.event_at(ctx, event, t_ms)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
debug_assert!(
self.input_routed_pages()
.all(|i| self.visibility_of(i) == PageVisibility::Current),
"R23: the input-routed page set must be exactly the Current page(s)"
);
for i in self.input_routed_pages() {
self.pages[i].pod.semantics_child(ctx);
}
}
crate::authoring::visit_children!(pages, transition);
}
fn cleared_ime_state() -> ImeState {
ImeState {
active: false,
editing: EditingState {
text: String::new(),
selection_base: -1,
selection_extent: -1,
composing_base: -1,
composing_extent: -1,
},
caret: None,
content_type: Default::default(),
suppress_soft_keyboard: false,
}
}
#[cfg(test)]
#[path = "navigator_tests/mod.rs"]
mod tests;