From 254fafe42bb923b1cfdadb7e646dfd025801720b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 27 Sep 2026 19:31:59 +0200 Subject: [PATCH] Document how the views switch (#184) --- .../calendula/ui/CalendarHost.kt | 7 ++-- .../calendula/ui/common/ViewFocus.kt | 2 +- docs/ARCHITECTURE.md | 42 +++++++++++++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 343145e8..e3e38c4c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -71,8 +71,9 @@ import kotlin.time.Clock /** * 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 - * pill in their top bars writes back here via [onSelectView]. + * screens. Each screen owns its own ViewModel; they all open on the one focused + * 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. * A lateral switch (pill / drawer) builds a visit history so back retraces it @@ -382,7 +383,7 @@ fun CalendarHost( } 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 // view keeps the directional slide. What both views show morphs across // (#184): see [ViewMorphKey]. Reduced motion keeps the plain fade. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewFocus.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewFocus.kt index 74ac3463..2393e480 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewFocus.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewFocus.kt @@ -37,5 +37,5 @@ val LocalViewFocus = staticCompositionLocalOf { null } @Composable fun EnterOnFocus(key: T, enter: (LocalDate) -> Unit) { val focus = LocalViewFocus.current - remember(key) { focus?.let { enter(it.date) } } + remember(key) { focus?.date?.also(enter) } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6431da65..f31a061f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -27,7 +27,7 @@ the package list (recurring writes, save conflicts, reminder delivery). ```mermaid flowchart TD 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 subgraph Data ["data/"] Repo["CalendarRepository\n(interface + impl, Flow-based, io-dispatched)"] @@ -79,15 +79,41 @@ flowchart TD There is no navigation library. `MainActivity` hosts `RootScreen`, which gates on the first-launch wizard (`ui/onboarding/`), then shows -`CalendarHost`. `CalendarHost` holds the active view (month/week/day) -plus overlay state for detail, edit, and settings — full-screen overlays +`CalendarHost`. `CalendarHost` holds the active view (month, day/multi-day/week, +agenda) plus overlay state for detail, edit, and settings — full-screen overlays driven by `AnimatedVisibility` with a *held-key* pattern: the last shown key stays alive through the slide-out so content never flashes empty. -The views themselves switch inside a `SharedTransitionLayout`: an event shown -in both morphs from its old bounds to its new ones, keyed per occurrence *and* -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 -a committed back gesture finishes from its scaled preview. +Overlays leave through floret-kit's `predictiveBackExit`, so a committed back +gesture finishes from its scaled preview. + +### Switching views (#184) + +- **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` + `onNewIntent`) as an external detail key that `CalendarHost` consumes exactly like an event tap.