# `Mob.Nav`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/nav.ex#L1)

Multi-stack navigation state.

Replaces the single `nav_history` list that `Mob.Screen` used to carry. One
`Mob.App.stack/2` declaration becomes one independent stack here: each keeps
its own history *and* its own current screen, so switching away from a stack
and back restores exactly where you were rather than re-mounting the root.

That is what makes `Mob.App.tab_bar/1` and `drawer/1` representable. Both have
been public API in `Mob.App`'s moduledoc for a long time while the runtime
behind them could only hold one history — see
`decisions/2026-08-27-screen-process-architecture.md`.

## Shape

The *active* stack's current screen is deliberately **not** stored here. It
lives in `Mob.Router`'s `current`, and this struct holds only the active
stack's `history` plus the fully parked state of every inactive stack. Only
`switch/3` moves state in or out of `parked`.

* `active` — name of the stack the current screen belongs to (`nil` when the
  app declares no stacks at all, i.e. a bare `start_root/1` with no layout)
* `history` — the active stack's history, head = most recent
* `parked` — `%{name => %{current: entry, history: [entry]}}` for inactive
  stacks. Never contains `active`.
* `order` — declared stack order, for tab-index mapping
* `roots` — `%{name => root_module}`, used to mount a stack on first visit

## Lazy stacks

A stack materializes on first visit. Until you switch to it, it has no socket
and has never mounted — matching UIKit's `UITabBarController`, which does not
instantiate a tab's view controller until it is first selected. After the
first visit its state is retained for the lifetime of the app.

# `entry`

```elixir
@type entry() :: term()
```

Whatever the caller uses to identify a screen. Opaque here.

`Mob.Router` puts `%{module:, pid:, params:, ref:}` in these slots since
MOB-112 — this module never looks inside one.

# `parked_stack`

```elixir
@type parked_stack() :: %{current: entry(), history: [entry()]}
```

# `stack_name`

```elixir
@type stack_name() :: atom()
```

# `t`

```elixir
@type t() :: %Mob.Nav{
  active: stack_name() | nil,
  history: [entry()],
  order: [stack_name()],
  parked: %{required(stack_name()) =&gt; parked_stack()},
  roots: %{required(stack_name()) =&gt; module()}
}
```

# `active`

```elixir
@spec active(t()) :: stack_name() | nil
```

Name of the active stack.

`nil` when no layout was declared. `:__mob_root__` when the mounted screen is
not the root of any declared stack — see `from_layout/2`.

# `back_target`

```elixir
@spec back_target(t()) :: {:switch, stack_name()} | :exit
```

What the platform back gesture should do when the active stack has nothing
left to pop.

Returns `{:switch, name}` when the active stack is a declared stack other than
the first, and `:exit` otherwise. This is the Android convention: back at the
root of a secondary tab returns to the first tab, and only back at the root of
the *first* tab leaves the app.

Without this, back at the root of any tab would exit — discarding every parked
stack, which is exactly the state this module exists to keep.

# `drop_parked`

```elixir
@spec drop_parked(t(), (entry() -&gt; boolean())) :: t()
```

Remove every parked entry for which `fun` returns true.

Dropping is not mapping: a stack whose *current* entry goes away has to
collapse. The head of its history is promoted; a stack left with nothing at
all is removed from `parked` entirely, so the next switch to it mounts its
root fresh rather than restoring a screen that is gone.

`Mob.Router` uses this when a screen crashes and cannot be re-mounted —
leaving the dead entry in place would freeze that tab permanently, since
switching to it would restore a corpse.

# `from_layout`

```elixir
@spec from_layout(map() | nil, module()) :: t()
```

Build navigation state from a declared layout, with `current_module` as the
screen that is already mounted.

`layout` is the map returned by `Mob.App.stack/2`, `tab_bar/1`, or `drawer/1`
(or `nil`/unrecognised when the app declares none).

The active stack is the one whose `:root` is `current_module`. When no stack
declares that module — `start_root/1` on a splash, login, or deep-link target
— the screen is filed under a private orphan stack rather than under the first
declared one. Its state is still parked and preserved across a switch, but it
does not occupy a declared stack's slot: squatting `:home` would leave the
real `HomeScreen` unreachable from the tab bar for the process lifetime, since
switching to the stack you are already on is a no-op. The orphan is not a
switch target, because no tab corresponds to it.

# `history`

```elixir
@spec history(t()) :: [entry()]
```

The active stack's history — head is the most recent entry.

# `map_parked`

```elixir
@spec map_parked(t(), (entry() -&gt; entry())) :: t()
```

Apply `fun` to every parked entry — each inactive stack's current screen and
every entry in its history.

Entries are opaque to this module, so the caller decides what an entry is and
what replacing one means. `Mob.Router` uses it to substitute a restarted
screen process wherever it was referenced.

The *active* stack's history is not covered: it lives in `history`, which the
caller already holds and can rewrite with `put_history/2`.

# `new`

```elixir
@spec new() :: t()
```

An empty single-stack navigation state.

Equivalent to the old `nav_history = []`. Used when no navigation layout has
been declared, or in tests that start a screen directly.

# `parked_stacks`

```elixir
@spec parked_stacks(t()) :: [stack_name()]
```

Names of the stacks currently parked, in declaration order.

# `put_history`

```elixir
@spec put_history(t(), [entry()]) :: t()
```

Replace the active stack's history.

# `reset`

```elixir
@spec reset(t(), module()) :: t()
```

Clear every materialized stack and select the stack for `current_module`.

The declared roots and their order are preserved. When `current_module` is
one of those roots, its declared stack becomes active; otherwise the fresh
screen belongs to the private orphan stack used for login and deep-link
screens. No previous history or parked screen survives the reset.

# `stacks`

```elixir
@spec stacks(t()) :: [stack_name()]
```

Declared stack names, in declaration order.

# `switch`

```elixir
@spec switch(t(), stack_name(), entry()) ::
  {:switched, t(), entry()} | {:mount_root, t(), module()} | :noop
```

Switch the active stack to `name`, parking `current_entry` under the stack it
belongs to.

Returns one of:

* `{:switched, nav, entry}` — the target has been visited before; `entry` is
  the `{module, socket}` to make current again, with no re-mount
* `{:mount_root, nav, root_module}` — first visit; the caller mounts
  `root_module` and makes it current
* `:noop` — `name` is already active, or is not a declared stack

`:noop` on an unknown stack is deliberate: `Mob.Socket.switch_tab/2` takes any
atom, and a typo should leave navigation untouched rather than crash the
screen or strand it on a stack that does not exist.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
