Return to Home Page

Pane Manager

The Pane Manager is the Slot-based layout that builds up the entire HMI’s “scaffold”. The PaneManager serves as the root node in the HMI Window

The PaneManager Composable has the following API, which reveals the parts of the HMI:

@Composable
fun PaneManager(
    //Children should not use max size.
    banner: @Composable (() -> Unit)?,

    //Children can use fillMaxSize, and it just works.
    sideSplit: @Composable (() -> Unit)?,
    darkenBackgroundOnSideSplitDisplay : Boolean = false,
    sideSplitVisible: Boolean,

    //Children should not use max size.
    bottomPanel: @Composable (() -> Unit)?,

    //This is for notifications
    //Children should use max size
    topPopIn: @Composable (() -> Unit)?,
    topPopInVisible: Boolean,

    //Children should use max size.
    mainContent: @Composable () -> Unit,

    mainContentOverlay : (@Composable () -> Unit)? = null
) {
...
}

The PaneManagerDebugger window is a great way to see the PaneManager in action.

Here we see the slots for the notification and side bar. The reason for the wild gradient is so that I could tell whether the main content area was being scaled or truncated as the banner and bottomBar areas were enabled/disabled.

Slots for notification and sidebar

This screenshot shows the Notification body filling in the notification slot, as well as the Model Menu overlay with some dummy data. The Chip orientation for the modal menu is set to NW (top left). Notification and modal menu

Providing “screen parts” for the PaneManager

Given that the PaneManager can take in @Composable () -> Unit lambdas, how does this allow the PaneManager to actually show content in the HMI Window?

The trick is that each lamda is fed in from a Flow<@Composable () -> Unit> in the MenuWindow.

This is also where all the global CompositionLocals are set up. Composition Local docs

@Composable
private fun rootContent() {

    //TODO, a KnobListener needs to be a CompositionLocal passed down all the way through
    //TODO so we can avoid chains of passing it in as a screen parameter.
    //TODO this is the root node of the composition so it's a pretty good place to put it.
    //TODO https://developer.android.com/jetpack/compose/compositionlocal

    val dummyKnobListenerService = KnobListenerService(MutableSharedFlow())
    val providedKnobListenerService = remember { mutableStateOf(realKnobListenerService) }

    ThemeWrapper.ThemedUiWrapper(
        themeConfigurationStorage.getTheme().collectAsState(themeConfigurationStorage.getStoredTheme()).value
    ) {
        PaneManager(
            banner = null,
            sideSplit = {
                modalMenuService.sidePaneOverlay.collectAsState().value.let {
                    if (it.ui != null) {
                        providedKnobListenerService.value = dummyKnobListenerService
                        CompositionLocalProvider(
                            MenuWindowKnobListener provides realKnobListenerService
                        ) {
                            it.ui?.invoke()
                        }
                    } else {
                        providedKnobListenerService.value = realKnobListenerService
                    }
                }
            },
            darkenBackgroundOnSideSplitDisplay = modalMenuService.sidePaneOverlay.collectAsState().value.darkenBackground,
            sideSplitVisible = modalMenuService.sidePaneOverlay.collectAsState().value.ui != null,
            bottomPanel = {
                val scope = rememberCoroutineScope()
                scope.launch {
                    bottomBarClock.updateValues()
                }

                BmwFullScreenBottomBar(
                    date = bottomBarClock.dateFlow.collectAsState().value,
                    time = bottomBarClock.timeFlow.collectAsState().value,
                )
            },
            topPopIn = {
                notificationHub.currentNotification.collectAsState().value?.toView()
            },
            topPopInVisible = notificationHub.currentNotificationIsVisible.collectAsState(false).value,
            mainContent = {
                Box(
                    Modifier.fillMaxSize()
                ) {
                    val currentNode = navigator.mainContentScreen.collectAsState()

                    logger.d("MenuWindow", currentNode.value.node.thisClass.canonicalName)
                    logger.d("MenuWindow", currentNode.value.incomingResult.toString())


                    CompositionLocalProvider(
                        MenuWindowKnobListener provides providedKnobListenerService.value
                    ) {
                        with(currentNode.value) {
                            node.provideMainContent().invoke(incomingResult)
                        }
                    }

                }
            },
            mainContentOverlay = {
                modalMenuService.modalMenuOverlay.collectAsState().value.let {
                    if (it != null) {
                        providedKnobListenerService.value = dummyKnobListenerService
                        it.invoke()
                    } else {
                        providedKnobListenerService.value = realKnobListenerService
                    }
                }
            }
        )
    }
}

There exists a dummyKnobListenerService and a realKnobListenerService, which are switched out so that the knob turn events don’t update the main menu and the modal overlay menu at the same time.

Return to Top