Document how the views switch (#184)

This commit is contained in:
2026-09-27 19:31:59 +02:00
parent f9dd7a22b7
commit 254fafe42b
3 changed files with 39 additions and 12 deletions
@@ -71,8 +71,9 @@ import kotlin.time.Clock
/** /**
* Holds the top-level view back stack (spec M1) and swaps between the calendar * Holds the top-level view back stack (spec M1) and swaps between the calendar
* screens. Each screen owns its own ViewModel and date anchor; the view-switcher * screens. Each screen owns its own ViewModel; they all open on the one focused
* pill in their top bars writes back here via [onSelectView]. * date held here ([ViewFocus]), and the view-switcher pill in their top bars
* writes back here via [onSelectView].
* *
* The stack's bottom is the user's [CalendarHostViewModel.defaultView] home view. * The stack's bottom is the user's [CalendarHostViewModel.defaultView] home view.
* A lateral switch (pill / drawer) builds a visit history so back retraces it * A lateral switch (pill / drawer) builds a visit history so back retraces it
@@ -382,7 +383,7 @@ fun CalendarHost(
} }
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
// Switching between the peer views (month/week/day/agenda) is lateral // Switching between the month, timeline and agenda screens is lateral
// navigation, so it fades through rather than sliding — paging *within* a // navigation, so it fades through rather than sliding — paging *within* a
// view keeps the directional slide. What both views show morphs across // view keeps the directional slide. What both views show morphs across
// (#184): see [ViewMorphKey]. Reduced motion keeps the plain fade. // (#184): see [ViewMorphKey]. Reduced motion keeps the plain fade.
@@ -37,5 +37,5 @@ val LocalViewFocus = staticCompositionLocalOf<ViewFocus?> { null }
@Composable @Composable
fun <T : Any> EnterOnFocus(key: T, enter: (LocalDate) -> Unit) { fun <T : Any> EnterOnFocus(key: T, enter: (LocalDate) -> Unit) {
val focus = LocalViewFocus.current val focus = LocalViewFocus.current
remember(key) { focus?.let { enter(it.date) } } remember(key) { focus?.date?.also(enter) }
} }
+34 -8
View File
@@ -27,7 +27,7 @@ the package list (recurring writes, save conflicts, reminder delivery).
```mermaid ```mermaid
flowchart TD flowchart TD
subgraph UI ["ui/ — Compose screens + ViewModels"] subgraph UI ["ui/ — Compose screens + ViewModels"]
Screens["Month / Week / Day\nDetail / Edit / Settings\nOnboarding wizard"] Screens["Month / Timeline / Agenda\nDetail / Edit / Settings\nOnboarding wizard"]
end end
subgraph Data ["data/"] subgraph Data ["data/"]
Repo["CalendarRepository\n(interface + impl, Flow-based, io-dispatched)"] Repo["CalendarRepository\n(interface + impl, Flow-based, io-dispatched)"]
@@ -79,15 +79,41 @@ flowchart TD
There is no navigation library. `MainActivity` hosts `RootScreen`, which There is no navigation library. `MainActivity` hosts `RootScreen`, which
gates on the first-launch wizard (`ui/onboarding/`), then shows gates on the first-launch wizard (`ui/onboarding/`), then shows
`CalendarHost`. `CalendarHost` holds the active view (month/week/day) `CalendarHost`. `CalendarHost` holds the active view (month, day/multi-day/week,
plus overlay state for detail, edit, and settings — full-screen overlays agenda) plus overlay state for detail, edit, and settings — full-screen overlays
driven by `AnimatedVisibility` with a *held-key* pattern: the last shown driven by `AnimatedVisibility` with a *held-key* pattern: the last shown
key stays alive through the slide-out so content never flashes empty. key stays alive through the slide-out so content never flashes empty.
The views themselves switch inside a `SharedTransitionLayout`: an event shown Overlays leave through floret-kit's `predictiveBackExit`, so a committed back
in both morphs from its old bounds to its new ones, keyed per occurrence *and* gesture finishes from its scaled preview.
per day it is drawn from (`ViewMorphKey`), and only a pager's settled page
carries the tags. Overlays leave through floret-kit's `predictiveBackExit`, so ### Switching views (#184)
a committed back gesture finishes from its scaled preview.
- **One focused date.** `CalendarHost` owns a `ViewFocus` (`LocalViewFocus`)
that every view opens on and carries along. A view *reads* it once, on
entry, synchronously (`EnterOnFocus` before it reads its own position, so
its first frame is already on that date), and *writes* it only when the user
moves — a pager settling (`focusOnPage` keeps the date's place in the page),
a jump, Today, a tapped day, the agenda's top row. Writing on entry would
drift the date on every round trip.
- **Three screens, not five.** The host's `AnimatedContent` is keyed on
`ViewScreen` (month, timeline, agenda), not on the view: day, multi-day and
week are one `TimelineScreen` with a `TimelineKind` each and a view model
each. Keying the three on one `contentKey` instead would still replay the
enter transition on every switch among them.
- **Timeline ↔ timeline resizes in place.** `ColumnGeometry` places every
column (header cell, all-day bar, day column) in the layout pass; a switch
swaps the pager for a frame over both pages' days, each column lerped
between its slot on the one page and the other — a day the page doesn't
show carries on at its pitch, off screen, which is what slides it in or
out. Blocks sit in their column as shares of its width (`LaneColumn`), so
nothing recomposes per frame. The frame starts and ends exactly as the two
pages lay out, so the pagers either side hand over without a jump.
- **Across screens, what both show morphs.** Inside a `SharedTransitionLayout`,
an event shown in both views travels from its old bounds to its new ones,
keyed per occurrence *and* per day it is drawn from (`ViewMorphKey`); only a
pager's settled page carries the tags. The top bar's shared pieces and the
create FAB are tagged too (`morphChrome`), so they hold still while the
rest cross-fades — except over an open drawer.
A tapped reminder notification routes through `MainActivity` (`singleTop` + A tapped reminder notification routes through `MainActivity` (`singleTop` +
`onNewIntent`) as an external detail key that `CalendarHost` consumes `onNewIntent`) as an external detail key that `CalendarHost` consumes
exactly like an event tap. exactly like an event tap.