@@ -57,6 +57,7 @@ import androidx.compose.foundation.lazy.LazyColumn
5757import androidx.compose.foundation.lazy.grid.GridCells
5858import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
5959import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
60+ import androidx.compose.material3.BottomAppBar
6061import androidx.compose.material3.AlertDialog
6162import androidx.compose.material3.Button
6263import androidx.compose.material3.ButtonDefaults
@@ -419,7 +420,7 @@ private fun RenderResolved(node: ViewNode) {
419420// Sheets and alerts ride as hidden children; the presentation layer shows
420421// them, so the normal child loop skips them.
421422private fun ViewNode.isPresentation (): Boolean =
422- type == " Sheet" || type == " Alert" || type == " ConfirmationDialog"
423+ type == " Sheet" || type == " Alert" || type == " ConfirmationDialog" || type == " ToolbarItem "
423424
424425@Composable
425426private fun RenderChildren (node : ViewNode ) {
@@ -1308,22 +1309,49 @@ private fun RenderNavStack(node: ViewNode) {
13081309 val depth = node.children.size
13091310 // cache rendered screens by index so a popped screen can animate out
13101311 val topIndex = depth - 1
1312+ val screen = node.children.getOrNull(topIndex)
1313+ val toolbar = screen?.children?.filter { it.type == " ToolbarItem" }.orEmpty()
1314+ fun placed (vararg names : String ) = toolbar.filter { (it.string(" placement" ) ? : " automatic" ) in names }
1315+ val leading = placed(" navigationBarLeading" )
1316+ // an unplaced item goes to the trailing side, as it does on iOS
1317+ val trailing = placed(" navigationBarTrailing" , " automatic" )
1318+ val principal = placed(" principal" )
1319+ val bottom = placed(" bottomBar" )
1320+
13111321 Scaffold (
13121322 topBar = {
13131323 val title = titles.getOrNull(topIndex).orEmpty()
1314- if (title.isNotEmpty() || depth > 1 ) {
1324+ if (title.isNotEmpty() || depth > 1 || toolbar.isNotEmpty() ) {
13151325 TopAppBar (
1316- title = { Text (title) },
1326+ title = {
1327+ // a principal item replaces the title outright
1328+ if (principal.isNotEmpty()) principal.forEach { RenderToolbarItem (it) }
1329+ else Text (title)
1330+ },
13171331 navigationIcon = {
1318- if (depth > 1 ) {
1319- IconButton (onClick = { onPop?.let { SwiftBridge .sink.invokeVoid(it) } }) {
1320- Icon (Icons .AutoMirrored .Filled .ArrowBack , contentDescription = " Back" )
1332+ Row (verticalAlignment = Alignment .CenterVertically ) {
1333+ if (depth > 1 ) {
1334+ IconButton (onClick = { onPop?.let { SwiftBridge .sink.invokeVoid(it) } }) {
1335+ Icon (Icons .AutoMirrored .Filled .ArrowBack , contentDescription = " Back" )
1336+ }
13211337 }
1338+ leading.forEach { RenderToolbarItem (it) }
13221339 }
13231340 },
1341+ actions = { trailing.forEach { RenderToolbarItem (it) } },
13241342 )
13251343 }
13261344 },
1345+ bottomBar = {
1346+ if (bottom.isNotEmpty()) {
1347+ BottomAppBar {
1348+ Row (
1349+ verticalAlignment = Alignment .CenterVertically ,
1350+ modifier = Modifier .fillMaxWidth().padding(horizontal = 12 .dp),
1351+ ) { bottom.forEach { RenderToolbarItem (it) } }
1352+ }
1353+ }
1354+ },
13271355 ) { padding ->
13281356 AnimatedContent (
13291357 targetState = topIndex,
@@ -1420,6 +1448,12 @@ private fun RenderTabView(node: ViewNode) {
14201448 }
14211449}
14221450
1451+ // A toolbar item's content, lifted out of the screen body into the bar.
1452+ @Composable
1453+ private fun RenderToolbarItem (item : ViewNode ) {
1454+ item.children.forEach { RenderChild (it) }
1455+ }
1456+
14231457// Sheets and alerts ride as hidden children of a screen node; present them
14241458// when the screen carries the corresponding flag.
14251459@OptIn(ExperimentalMaterial3Api ::class )
0 commit comments