11package org.matrix.vector.manager.ui
22
3- import androidx.compose.material3.Icon
4- import androidx.compose.material3.Text
3+ import androidx.activity.compose.BackHandler
4+ import androidx.compose.foundation.layout.Box
5+ import androidx.compose.foundation.layout.fillMaxSize
6+ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
57import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
8+ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
9+ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
610import androidx.compose.material3.adaptive.navigationsuite.rememberNavigationSuiteScaffoldState
711import androidx.compose.runtime.Composable
812import androidx.compose.runtime.CompositionLocalProvider
913import androidx.compose.runtime.LaunchedEffect
1014import androidx.compose.runtime.getValue
11- import androidx.compose.ui.res.stringResource
15+ import androidx.compose.ui.Modifier
1216import androidx.lifecycle.compose.collectAsStateWithLifecycle
1317import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
1418import androidx.navigation3.runtime.EntryProviderScope
1519import androidx.navigation3.runtime.NavKey
1620import androidx.navigation3.runtime.entryProvider
1721import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
1822import androidx.navigation3.ui.NavDisplay
23+ import org.matrix.vector.manager.di.ServiceLocator
1924import org.matrix.vector.manager.ui.navigation.FrameworkUpdate
2025import org.matrix.vector.manager.ui.screens.update.FrameworkUpdateScreen
2126import org.matrix.vector.manager.ui.navigation.Canary
2227import org.matrix.vector.manager.ui.screens.canary.CanaryScreen
2328import org.matrix.vector.manager.ui.navigation.Troubleshoot
2429import org.matrix.vector.manager.ui.screens.report.TroubleshootScreen
2530import org.matrix.vector.manager.ui.navigation.DeepLink
31+ import org.matrix.vector.manager.ui.navigation.FloatingPanelNav
2632import org.matrix.vector.manager.ui.navigation.LocalNavigator
2733import org.matrix.vector.manager.ui.navigation.Navigator
34+ import org.matrix.vector.manager.ui.navigation.PanelBar
35+ import org.matrix.vector.manager.ui.navigation.PanelEditDone
2836import org.matrix.vector.manager.ui.navigation.Scope
2937import org.matrix.vector.manager.ui.navigation.StoreDetail
3038import org.matrix.vector.manager.ui.navigation.CrashTrace
3139import org.matrix.vector.manager.ui.navigation.LogTrace
3240import org.matrix.vector.manager.ui.navigation.SystemStatus
3341import org.matrix.vector.manager.ui.navigation.Web
34- import org.matrix.vector.manager.ui.navigation.TOP_LEVEL_DESTINATIONS
3542import org.matrix.vector.manager.ui.navigation.TopLevelRoute
3643import org.matrix.vector.manager.ui.navigation.rememberNavigator
3744import org.matrix.vector.manager.ui.screens.home.HomeScreen
@@ -53,6 +60,11 @@ import org.matrix.vector.manager.ui.screens.web.WebScreen
5360 * no longer lock itself to portrait or declare itself non-resizable on large screens, so the shell
5461 * has to work unfolded and in landscape regardless. The scaffold also owns where that container
5562 * sits, so the destinations below it are laid out beside or above it rather than under it.
63+ *
64+ * Which panels that container holds, in which order, is the reader's — see NavPanels — and there is
65+ * a third arrangement it can take, a ball floating over the content with no container at all. The
66+ * two are not independent: rearranging the panels needs something to rearrange, so edit mode always
67+ * puts the container back for as long as it lasts.
5668 */
5769@Composable
5870fun VectorApp () {
@@ -82,7 +94,10 @@ fun VectorApp() {
8294 }
8395
8496 CompositionLocalProvider (LocalNavigator provides navigator) {
85- // The bar shows only at the root of a tab. On a detail screen none of the four items is
97+ val settings = ServiceLocator .settings
98+ val floating by settings.floatingNav.collectAsStateWithLifecycle()
99+ val editing = navigator.editingPanels
100+ // The container shows only at the root of a panel. On a detail screen none of the items is
86101 // the current destination, and a navigation bar highlighting nothing is worse than none.
87102 val atRoot = ! navigator.canGoBack
88103
@@ -92,42 +107,85 @@ fun VectorApp() {
92107 val suiteState = rememberNavigationSuiteScaffoldState()
93108 LaunchedEffect (atRoot) { if (atRoot) suiteState.show() else suiteState.hide() }
94109
110+ // Computed rather than left to the scaffold's default, for two reasons: the floating style
111+ // forces None, which is what actually removes the container instead of hiding it, and
112+ // PanelBar has to be told which axis it is laying items along. Entering edit mode overrules
113+ // the floating setting for as long as it lasts — there is nothing to rearrange otherwise.
114+ val suiteType =
115+ if (floating && ! editing) NavigationSuiteType .None
116+ else NavigationSuiteScaffoldDefaults .navigationSuiteType(currentWindowAdaptiveInfo())
117+
95118 NavigationSuiteScaffold (
119+ navigationItems = {
120+ // NavigationSuite's `when` over the type has no None branch and no else, so under
121+ // None this slot is silently dropped along with the container. Skipping it here
122+ // says so out loud rather than leaving a composable that never runs.
123+ if (suiteType != NavigationSuiteType .None ) {
124+ PanelBar (
125+ panels = navigator.panels,
126+ current = navigator.currentTopLevel,
127+ editing = editing,
128+ suiteType = suiteType,
129+ onSelect = { route -> navigator.switchTo(route) },
130+ onEdit = { navigator.editingPanels = true },
131+ onToggleHidden = { key, hidden -> navigator.setPanelHidden(key, hidden) },
132+ onMove = { from, to -> navigator.movePanel(from, to) },
133+ )
134+ }
135+ },
136+ navigationSuiteType = suiteType,
96137 state = suiteState,
97- navigationSuiteItems = {
98- TOP_LEVEL_DESTINATIONS .forEach { destination ->
99- item(
100- selected = navigator.currentTopLevel == destination.route,
101- onClick = { navigator.switchTo(destination.route) },
102- icon = { Icon (destination.icon, contentDescription = null ) },
103- // The label doubles as the item's accessibility name, so the icon above
104- // carries no contentDescription of its own — otherwise TalkBack announces
105- // every selected tab twice.
106- label = { Text (stringResource(destination.labelRes)) },
138+ primaryActionContent = {
139+ if (editing) PanelEditDone (onDone = { navigator.editingPanels = false })
140+ },
141+ ) {
142+ Box (Modifier .fillMaxSize()) {
143+ NavDisplay (
144+ backStack = navigator.backStack,
145+ onBack = { navigator.back() },
146+ // Naming any decorator replaces NavDisplay's default, which is the
147+ // saveable-state one alone, so it is repeated here; the scene-setup decorator
148+ // NavDisplay applies internally is untouched. The ViewModel one is what this
149+ // list is for: it scopes a ViewModelStore per entry, so opening the scope
150+ // editor for a second module builds a second ViewModel instead of reusing the
151+ // first (they would otherwise share one default key under the activity's
152+ // store).
153+ entryDecorators =
154+ listOf (
155+ rememberSaveableStateHolderNavEntryDecorator(),
156+ rememberViewModelStoreNavEntryDecorator(),
157+ ),
158+ entryProvider = entryProvider { registerRoutes(navigator) },
107159 )
160+ // Last child of the Box so it draws over the destination, and inside the app window
161+ // rather than in one of its own: parasitically this app is com.android.shell, which
162+ // must never ask for SYSTEM_ALERT_WINDOW. It follows the same rule the container
163+ // does — present at the root of a panel, gone on a detail screen that has its own
164+ // back affordance.
165+ if (floating && ! editing && atRoot) {
166+ FloatingPanelNav (
167+ panels = navigator.panels,
168+ current = navigator.currentTopLevel,
169+ onSelect = { route -> navigator.switchTo(route) },
170+ )
171+ }
108172 }
109- },
110- ) {
111- NavDisplay (
112- backStack = navigator.backStack,
113- onBack = { navigator.back() },
114- // Naming any decorator replaces NavDisplay's default, which is the saveable-state
115- // one alone, so it is repeated here; the scene-setup decorator NavDisplay applies
116- // internally is untouched. The ViewModel one is what this list is for: it scopes a
117- // ViewModelStore per entry, so opening the scope editor for a second module builds
118- // a second ViewModel instead of reusing the first (they would otherwise share one
119- // default key under the activity's store).
120- entryDecorators =
121- listOf (
122- rememberSaveableStateHolderNavEntryDecorator(),
123- rememberViewModelStoreNavEntryDecorator(),
124- ),
125- entryProvider = entryProvider { registerRoutes(navigator) },
126- )
127173 }
174+
175+ // After the scaffold on purpose. Back callbacks are dispatched last-registered-first and
176+ // BackHandler registers from an effect, which run in composition order, so this one
177+ // outranks the handler NavDisplay installs and edit mode ends before the stack is touched.
178+ BackHandler (enabled = editing) { navigator.editingPanels = false }
128179 }
129180}
130181
182+ /* *
183+ * Every destination, registered.
184+ *
185+ * All four panels keep their entry whether or not the reader has hidden them. A saved stack names
186+ * its keys by class, and entryProvider throws for one it was never given, so dropping the
187+ * registration of a hidden panel would turn a stale saved stack into a crash.
188+ */
131189private fun EntryProviderScope<NavKey>.registerRoutes (navigator : Navigator ) {
132190 entry<TopLevelRoute .Home > {
133191 HomeScreen (
0 commit comments