diff --git a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/AuroraBreadcrumbBar.kt b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/AuroraBreadcrumbBar.kt index f9cf8438..3dee6a05 100644 --- a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/AuroraBreadcrumbBar.kt +++ b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/AuroraBreadcrumbBar.kt @@ -18,9 +18,7 @@ package org.pushingpixels.aurora.component import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.painter.Painter @@ -32,6 +30,7 @@ import androidx.compose.ui.text.resolveDefaults import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import org.pushingpixels.aurora.common.AuroraInternalApi import org.pushingpixels.aurora.common.withAlpha @@ -45,11 +44,31 @@ import org.pushingpixels.aurora.theming.* @OptIn(AuroraInternalApi::class) @Composable -fun AuroraBreadcrumbBar( - commands: List, +fun AuroraBreadcrumbBar( + contentProvider: BreadcrumbBarContentProvider, presentationModel: BreadcrumbBarPresentationModel = BreadcrumbBarPresentationModel(), modifier: Modifier ) { + val initialized = remember { mutableStateOf(false) } + + // The currently shown path of breadcrumb items. + val shownPath: MutableList> = remember { mutableStateListOf() } + // For each item in "shownPath" this has the list of path choices - to be displayed + // in the popup for that particular item. The first item in this list has path choices + // for the root. + val shownPathChoices: MutableList>> = remember { mutableStateListOf() } + + val contentModel = remember { BreadcrumbBarContentModel(shownPath) } + if (!initialized.value) { + LaunchedEffect(null) { + coroutineScope { + val rootPathChoices = contentProvider.getPathChoices(emptyList()) + shownPathChoices.add(rootPathChoices) + } + initialized.value = true + } + } + val colors = AuroraSkin.colors val decorationAreaType = AuroraSkin.decorationAreaType val density = LocalDensity.current @@ -87,7 +106,12 @@ fun AuroraBreadcrumbBar( backgroundAppearanceStrategy = presentationModel.backgroundAppearanceStrategy, iconActiveFilterStrategy = presentationModel.iconActiveFilterStrategy, iconEnabledFilterStrategy = presentationModel.iconEnabledFilterStrategy, - iconDisabledFilterStrategy = presentationModel.iconDisabledFilterStrategy + iconDisabledFilterStrategy = presentationModel.iconDisabledFilterStrategy, + popupMenuPresentationModel = CommandPopupMenuPresentationModel( + menuIconActiveFilterStrategy = presentationModel.iconActiveFilterStrategy, + menuIconEnabledFilterStrategy = presentationModel.iconEnabledFilterStrategy, + menuIconDisabledFilterStrategy = presentationModel.iconDisabledFilterStrategy, + ) ) val contentLayoutManager = contentPresentationModel.presentationState.createLayoutManager( layoutDirection = layoutDirection, @@ -199,6 +223,39 @@ fun AuroraBreadcrumbBar( } }) + val commands = derivedStateOf { + val rootChoices = if (shownPathChoices.isNotEmpty()) shownPathChoices[0] else null + val rootSecondaryContentModel = rootChoices?.let { + CommandMenuContentModel( + group = CommandGroup( + title = "", + commands = it.map { rootChoice -> + Command(text = rootChoice.displayName, + icon = rootChoice.icon, + action = { + shownPath.clear() + shownPath.add(rootChoice) + }) + } + ) + ) + } + + listOf( + Command( + text = "root", + icon = null, + action = {}, + secondaryContentModel = rootSecondaryContentModel + ) + ) + shownPath.map { + Command(text = it.displayName, + icon = it.icon, + action = { println("Act on ${it.data}") } + ) + } + } + Layout(modifier = modifier.fillMaxWidth(), content = { // Leftwards scroller @@ -217,7 +274,7 @@ fun AuroraBreadcrumbBar( Box(modifier = Modifier.horizontalScroll(stateHorizontal)) { Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) { - for (command in commands) { + for (command in commands.value) { CommandButtonProjection( contentModel = command, presentationModel = contentPresentationModel @@ -269,17 +326,29 @@ fun AuroraBreadcrumbBar( // How much space does the scrollable content need? var boxRequiredWidth = 0.0f var boxHeight = 0 - for (command in commands) { + if (commands.value.isNotEmpty()) { + for (command in commands.value) { + val commandPreLayoutInfo = + contentLayoutManager.getPreLayoutInfo( + command, + contentPresentationModel + ) + val commandSize = contentLayoutManager.getPreferredSize( + command, contentPresentationModel, commandPreLayoutInfo + ) + boxRequiredWidth += commandSize.width + boxHeight = commandSize.height.toInt() + } + } else { + val forSizing = Command(text = "sample", action = {}) val commandPreLayoutInfo = contentLayoutManager.getPreLayoutInfo( - command, + forSizing, contentPresentationModel ) - val commandSize = contentLayoutManager.getPreferredSize( - command, contentPresentationModel, commandPreLayoutInfo - ) - boxRequiredWidth += commandSize.width - boxHeight = commandSize.height.toInt() + boxHeight = contentLayoutManager.getPreferredSize( + forSizing, contentPresentationModel, commandPreLayoutInfo + ).height.toInt() } // Do we need to show the scrollers? If available width from constraints is enough diff --git a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/BreadcrumbBarModels.kt b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/BreadcrumbBarModels.kt index b39be274..0a4cde0b 100644 --- a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/BreadcrumbBarModels.kt +++ b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/BreadcrumbBarModels.kt @@ -15,8 +15,127 @@ */ package org.pushingpixels.aurora.component.model +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.painter.Painter import org.pushingpixels.aurora.theming.BackgroundAppearanceStrategy import org.pushingpixels.aurora.theming.IconFilterStrategy +import java.io.InputStream +import java.util.* + +/** + * A single item in the breadcrumb bar model. + */ +data class BreadcrumbItem(val displayName: String, val icon: Painter?, val data: T) + +/** + * Content provider for a breadcrumb bar. + */ +interface BreadcrumbBarContentProvider { + /** + * Returns the choice elements that correspond to the specified path. If the + * path is empty, `null` should be returned. If path is + * `null`, the "root" elements should be returned + * + * @param path Breadcrumb bar path. + * @return The choice elements that correspond to the specified path + */ + suspend fun getPathChoices(path: List>): List> + + /** + * Returns the leaf elements that correspond to the specified path. If the + * path is empty, `null` should be returned. If path is + * `null`, leaf content of the "root" elements should be returned. Most probably, if + * your root is more than one element, you should be returning null in here. + * + * @param path Breadcrumb bar path. + * @return The leaf elements that correspond to the specified path + */ + suspend fun getLeaves(path: List>): List> + + /** + * Returns the input stream with the leaf content. Some implementations may + * return `null` if this is not applicable. + * + * @param leaf Leaf. + * @return Input stream with the leaf content. May be `null` if + * this is not applicable. + */ + suspend fun getLeafContent(leaf: T): InputStream? { + return null + } +} + +/** + * Model for the breadcrumb bar component. + */ +class BreadcrumbBarContentModel(val items: MutableList>) { + /** + * Returns the index of the specified item. + * + * @param item Item. + * @return Index of the item if it is in the model or -1 if it is not. + */ + fun indexOf(item: BreadcrumbItem): Int { + return items.indexOf(item) + } + + /** + * Removes the last item in this model. + */ + fun removeLast() { + items.removeLast() + } + + /** + * Resets this model, removing all the items. + */ + fun reset() { + items.clear() + } + + /** + * Returns the number of items in this model. + * + * @return Number of items in this model. + */ + val itemCount: Int = items.size + + /** + * Returns the model item at the specified index. + * + * @param index Item index. + * @return The model item at the specified index. Will return + * `null` if the index is negative or larger than the + * number of items. + */ + fun getItem(index: Int): BreadcrumbItem? { + if (index < 0) return null + return if (index >= itemCount) null else items[index] + } + + /** + * Replaces the current item list with the specified list. + * + * @param items New contents of the model. + */ + fun replace(items: List>) { + this.items.clear() + for (i in items.indices) { + this.items.add(items[i]) + } + } + + /** + * Adds the specified item at the end of the path. + * + * @param item Item to add. + */ + fun add(item: BreadcrumbItem) { + items.add(item) + } +} + data class BreadcrumbBarPresentationModel( val presentationState: CommandButtonPresentationState = CommandButtonPresentationState.Medium, diff --git a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/CommandPopupMenuPresentationModel.kt b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/CommandPopupMenuPresentationModel.kt index 7e351e55..18dbdd8b 100644 --- a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/CommandPopupMenuPresentationModel.kt +++ b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/model/CommandPopupMenuPresentationModel.kt @@ -22,12 +22,16 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import org.pushingpixels.aurora.component.layout.CommandButtonLayoutManager import org.pushingpixels.aurora.component.layout.CommandButtonLayoutManagerMedium +import org.pushingpixels.aurora.theming.IconFilterStrategy import org.pushingpixels.aurora.theming.PopupPlacementStrategy data class CommandPopupMenuPresentationModel( val panelPresentationModel: CommandPanelPresentationModel? = null, val menuPresentationState: CommandButtonPresentationState = DefaultCommandPopupMenuPresentationState, + val menuIconActiveFilterStrategy: IconFilterStrategy = IconFilterStrategy.Original, + val menuIconEnabledFilterStrategy: IconFilterStrategy = IconFilterStrategy.Original, + val menuIconDisabledFilterStrategy: IconFilterStrategy = IconFilterStrategy.ThemedFollowColorScheme, val menuContentPadding: PaddingValues = CommandButtonSizingConstants.CompactButtonContentPadding, val maxVisibleMenuCommands: Int = 0, diff --git a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/utils/CommandMenuPopupContent.kt b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/utils/CommandMenuPopupContent.kt index 5ed3e524..c79bc89a 100644 --- a/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/utils/CommandMenuPopupContent.kt +++ b/component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/utils/CommandMenuPopupContent.kt @@ -111,6 +111,9 @@ internal fun displayPopupContent( // the popup menu presentation model configured on the top-level presentation model val regularButtonPresentationModel = CommandButtonPresentationModel( presentationState = presentationModel.menuPresentationState, + iconActiveFilterStrategy = presentationModel.menuIconActiveFilterStrategy, + iconEnabledFilterStrategy = presentationModel.menuIconEnabledFilterStrategy, + iconDisabledFilterStrategy = presentationModel.menuIconDisabledFilterStrategy, popupPlacementStrategy = presentationModel.popupPlacementStrategy, backgroundAppearanceStrategy = BackgroundAppearanceStrategy.Flat, horizontalAlignment = HorizontalAlignment.Leading, @@ -506,6 +509,9 @@ private fun PopupGeneralContent( // the popup menu presentation model configured on the top-level presentation model val menuButtonPresentationModel = CommandButtonPresentationModel( presentationState = menuPresentationModel.menuPresentationState, + iconActiveFilterStrategy = menuPresentationModel.menuIconActiveFilterStrategy, + iconEnabledFilterStrategy = menuPresentationModel.menuIconEnabledFilterStrategy, + iconDisabledFilterStrategy = menuPresentationModel.menuIconDisabledFilterStrategy, forceAllocateSpaceForIcon = atLeastOneButtonHasIcon, popupPlacementStrategy = menuPresentationModel.popupPlacementStrategy, backgroundAppearanceStrategy = BackgroundAppearanceStrategy.Flat, diff --git a/demo/src/desktopMain/kotlin/org/pushingpixels/aurora/demo/AuroraBreadcrumbBarDemo.kt b/demo/src/desktopMain/kotlin/org/pushingpixels/aurora/demo/AuroraBreadcrumbBarDemo.kt index 1dce5610..eb0a8ae6 100644 --- a/demo/src/desktopMain/kotlin/org/pushingpixels/aurora/demo/AuroraBreadcrumbBarDemo.kt +++ b/demo/src/desktopMain/kotlin/org/pushingpixels/aurora/demo/AuroraBreadcrumbBarDemo.kt @@ -26,8 +26,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.rememberWindowState +import kotlinx.coroutines.delay import org.pushingpixels.aurora.component.AuroraBreadcrumbBar +import org.pushingpixels.aurora.component.model.BreadcrumbBarContentProvider import org.pushingpixels.aurora.component.model.BreadcrumbBarPresentationModel +import org.pushingpixels.aurora.component.model.BreadcrumbItem import org.pushingpixels.aurora.component.model.Command import org.pushingpixels.aurora.demo.svg.material.* import org.pushingpixels.aurora.demo.svg.radiance_menu @@ -36,6 +39,7 @@ import org.pushingpixels.aurora.window.AuroraApplicationScope import org.pushingpixels.aurora.window.AuroraDecorationArea import org.pushingpixels.aurora.window.AuroraWindow import org.pushingpixels.aurora.window.auroraApplication +import java.io.InputStream fun main() = auroraApplication { val state = rememberWindowState( @@ -60,29 +64,52 @@ fun main() = auroraApplication { @Composable fun AuroraApplicationScope.BreadcrumbContent(auroraSkinDefinition: MutableState) { - val icons = arrayOf( - account_box_24px(), - apps_24px(), - backup_24px(), - devices_other_24px(), - help_24px(), - keyboard_capslock_24px(), - location_on_24px(), - perm_device_information_24px(), - storage_24px() + val topContent = listOf( + BreadcrumbItem("account", account_box_24px(), "account activated"), + BreadcrumbItem("apps", apps_24px(), "apps activated"), + BreadcrumbItem("backup", backup_24px(), "backup activated"), + BreadcrumbItem("devices", devices_other_24px(), "devices activated"), + BreadcrumbItem("help", help_24px(), "help activated"), + BreadcrumbItem("keyboard", keyboard_capslock_24px(), "keyboard activated"), + BreadcrumbItem("location", location_on_24px(), "location activated"), + BreadcrumbItem("permissions", perm_device_information_24px(), "permission activated"), + BreadcrumbItem("storage", storage_24px(), "storage activated") ) - val commands = icons.map { - Command( - text = "sample", - icon = it, - action = { println("Activated!") } - ) + val secondaryContent = listOf( + BreadcrumbItem("bold", format_bold_black_24dp(), ""), + BreadcrumbItem("italic", format_italic_black_24dp(), ""), + BreadcrumbItem("strikethrough", format_strikethrough_black_24dp(), ""), + BreadcrumbItem("underlined", format_underlined_black_24dp(), ""), + ) + + val contentProvider: BreadcrumbBarContentProvider = object : BreadcrumbBarContentProvider { + override suspend fun getPathChoices(path: List>): List> { + // Sample delay to emulate slow loading of content + delay(500) + if (path.isEmpty()) { + return topContent + } + if (path.size == 1) { + return secondaryContent + } + return emptyList() + } + + override suspend fun getLeaves(path: List>): List> { + // Sample delay to emulate slow loading of content + delay(500) + return emptyList() + } + + override suspend fun getLeafContent(leaf: String): InputStream? { + return null + } } Column(modifier = Modifier.fillMaxSize()) { AuroraDecorationArea(decorationAreaType = DecorationAreaType.Header) { AuroraBreadcrumbBar( - commands = commands, + contentProvider = contentProvider, presentationModel = BreadcrumbBarPresentationModel( iconActiveFilterStrategy = IconFilterStrategy.ThemedFollowText, iconEnabledFilterStrategy = IconFilterStrategy.ThemedFollowText,