Skip to main content

back_fallback

Function back_fallback 

Source
pub fn back_fallback<V>(
    fallback: &BackFallback,
    child: V,
) -> BackFallbackView<V>
Expand description

An app-level back rule for per-tab navigator stacks: BackFallback claims a back press only when no navigator does (it ranks after every navigator, whatever the wire order), and back_fallback wraps the view it belongs to. The Android/Material tab convention — back at a tab root returns to the home tab, back at the home tab root exits — is one fallback:

use frust::{
    BackFallback, Component, Get, GetUntracked, IndexedStack, NavigatorController, RwSignal,
    Set, View, back_fallback, navigator, text,
};

const HOME: usize = 0;

#[derive(Default)]
struct App;

struct AppState {
    selected: RwSignal<usize>,
    tabs: [NavigatorController<AppState>; 3],
    home_fallback: BackFallback,
}

impl Component for App {
    type State = AppState;

    fn init(&self) -> AppState {
        let selected = RwSignal::new(HOME);
        let tabs = [
            NavigatorController::new(),
            NavigatorController::new(),
            NavigatorController::new(),
        ];
        let probe = tabs.clone();
        // Constructed ONCE. It claims a press only off the home tab with the
        // selected tab at its root; any poppable tab claims first anyway.
        let home_fallback = BackFallback::new(
            move || {
                let sel = selected.get_untracked();
                sel != HOME && probe[sel].depth() <= 1
            },
            move || selected.set(HOME),
        );
        AppState { selected, tabs, home_fallback }
    }

    fn build(&self, state: &mut AppState) -> impl View<AppState> {
        let [home, b, c] = &state.tabs;
        // A tracked read: a selection change rebuilds.
        let sel = state.selected.get();
        back_fallback(
            &state.home_fallback,
            IndexedStack(
                Some(sel),
                (
                    navigator(home, || text("home")),
                    navigator(b, || text("search")),
                    navigator(c, || text("profile")),
                ),
            ),
        )
    }
}

frust::app!(App);

Liveness contract. A fallback claims only while a back_fallback wrapper over it is mounted: leaving the screen unmounts the wrapper and the fallback stops claiming, though the handle lives on in app state. Wrap the view every Component::build that should honor it, like navigator.

Accepted residual. A wrapper mounted inside a branch input does not reach — a non-selected IndexedStack child, or the page under a covering page whose own navigator does not claim — still counts as live (it is mounted). Gate the interest closure there on that branch being current. An app-level back rule for per-tab navigator stacks: BackFallback claims a back press only when no navigator does (it ranks after every navigator, whatever the wire order), and back_fallback wraps the view it belongs to. The Android/Material tab convention — back at a tab root returns to the home tab, back at the home tab root exits — is one fallback:

use frust::{
    BackFallback, Component, Get, GetUntracked, IndexedStack, NavigatorController, RwSignal,
    Set, View, back_fallback, navigator, text,
};

const HOME: usize = 0;

#[derive(Default)]
struct App;

struct AppState {
    selected: RwSignal<usize>,
    tabs: [NavigatorController<AppState>; 3],
    home_fallback: BackFallback,
}

impl Component for App {
    type State = AppState;

    fn init(&self) -> AppState {
        let selected = RwSignal::new(HOME);
        let tabs = [
            NavigatorController::new(),
            NavigatorController::new(),
            NavigatorController::new(),
        ];
        let probe = tabs.clone();
        // Constructed ONCE. It claims a press only off the home tab with the
        // selected tab at its root; any poppable tab claims first anyway.
        let home_fallback = BackFallback::new(
            move || {
                let sel = selected.get_untracked();
                sel != HOME && probe[sel].depth() <= 1
            },
            move || selected.set(HOME),
        );
        AppState { selected, tabs, home_fallback }
    }

    fn build(&self, state: &mut AppState) -> impl View<AppState> {
        let [home, b, c] = &state.tabs;
        // A tracked read: a selection change rebuilds.
        let sel = state.selected.get();
        back_fallback(
            &state.home_fallback,
            IndexedStack(
                Some(sel),
                (
                    navigator(home, || text("home")),
                    navigator(b, || text("search")),
                    navigator(c, || text("profile")),
                ),
            ),
        )
    }
}

frust::app!(App);

Liveness contract. A fallback claims only while a back_fallback wrapper over it is mounted: leaving the screen unmounts the wrapper and the fallback stops claiming, though the handle lives on in app state. Wrap the view every Component::build that should honor it, like navigator.

Accepted residual. A wrapper mounted inside a branch input does not reach — a non-selected IndexedStack child, or the page under a covering page whose own navigator does not claim — still counts as live (it is mounted). Gate the interest closure there on that branch being current. An app-level back rule for per-tab navigator stacks: BackFallback claims a back press only when no navigator does (it ranks after every navigator, whatever the wire order), and back_fallback wraps the view it belongs to. The Android/Material tab convention — back at a tab root returns to the home tab, back at the home tab root exits — is one fallback:

use frust::{
    BackFallback, Component, Get, GetUntracked, IndexedStack, NavigatorController, RwSignal,
    Set, View, back_fallback, navigator, text,
};

const HOME: usize = 0;

#[derive(Default)]
struct App;

struct AppState {
    selected: RwSignal<usize>,
    tabs: [NavigatorController<AppState>; 3],
    home_fallback: BackFallback,
}

impl Component for App {
    type State = AppState;

    fn init(&self) -> AppState {
        let selected = RwSignal::new(HOME);
        let tabs = [
            NavigatorController::new(),
            NavigatorController::new(),
            NavigatorController::new(),
        ];
        let probe = tabs.clone();
        // Constructed ONCE. It claims a press only off the home tab with the
        // selected tab at its root; any poppable tab claims first anyway.
        let home_fallback = BackFallback::new(
            move || {
                let sel = selected.get_untracked();
                sel != HOME && probe[sel].depth() <= 1
            },
            move || selected.set(HOME),
        );
        AppState { selected, tabs, home_fallback }
    }

    fn build(&self, state: &mut AppState) -> impl View<AppState> {
        let [home, b, c] = &state.tabs;
        // A tracked read: a selection change rebuilds.
        let sel = state.selected.get();
        back_fallback(
            &state.home_fallback,
            IndexedStack(
                Some(sel),
                (
                    navigator(home, || text("home")),
                    navigator(b, || text("search")),
                    navigator(c, || text("profile")),
                ),
            ),
        )
    }
}

frust::app!(App);

Liveness contract. A fallback claims only while a back_fallback wrapper over it is mounted: leaving the screen unmounts the wrapper and the fallback stops claiming, though the handle lives on in app state. Wrap the view every Component::build that should honor it, like navigator.

Accepted residual. A wrapper mounted inside a branch input does not reach — a non-selected IndexedStack child, or the page under a covering page whose own navigator does not claim — still counts as live (it is mounted). Gate the interest closure there on that branch being current. Wrap child so fallback takes part in back arbitration while this view is mounted — the view-scoped half of BackFallback.

Call from every Component::build, exactly like navigator: at call time it consumes any new back press (the tracked read that subscribes the rebuild, routed under R44-back, so the fallback receives it only if no navigator claims it) and then registers or refreshes the fallback at the fallback rank. The returned BackFallbackView is a pass-through: it renders child unchanged and counts its own mounts, which is the fallback’s liveness. It is a View for every State its child is one for, so the function itself names no State and a state-agnostic child (a text) needs no annotation.

The facade’s re-export docs carry a back-to-home-tab example.

§Panics

Runs during a rebuild’s build pass; the first wire registers the live handles_back provider, which panics off the UI thread (always the UI thread here).