Skip to content

Commit

Permalink
First drop for breadcrumb bar
Browse files Browse the repository at this point in the history
Put on-demand horizontal scrolling in place around a row of command buttons. Nothing in terms of real content configuration yet.

For #8
  • Loading branch information
kirill-grouchnikov committed Jan 3, 2022
1 parent f9e9584 commit 18951e1
Show file tree
Hide file tree
Showing 2 changed files with 328 additions and 0 deletions.
@@ -0,0 +1,234 @@
/*
* Copyright 2020-2022 Aurora, Kirill Grouchnikov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontLoader
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.resolveDefaults
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.pushingpixels.aurora.common.AuroraInternalApi
import org.pushingpixels.aurora.component.model.*
import org.pushingpixels.aurora.component.projection.CommandButtonProjection
import org.pushingpixels.aurora.component.utils.drawArrow
import org.pushingpixels.aurora.theming.BackgroundAppearanceStrategy
import org.pushingpixels.aurora.theming.LocalTextStyle
import org.pushingpixels.aurora.theming.PopupPlacementStrategy

@OptIn(AuroraInternalApi::class)
@Composable
fun AuroraBreadcrumbBar(commands: List<Command>, modifier: Modifier) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val textStyle = LocalTextStyle.current
val resourceLoader = LocalFontLoader.current

val resolvedTextStyle = remember { resolveDefaults(textStyle, layoutDirection) }

val scrollerPresentationModel = CommandButtonPresentationModel(
presentationState = CommandButtonPresentationState.Small,
contentPadding = PaddingValues(2.dp),
actionFireTrigger = ActionFireTrigger.OnRollover,
autoRepeatAction = true,
autoRepeatInitialInterval = 250L,
autoRepeatSubsequentInterval = 100L
)
val scrollerLayoutManager = scrollerPresentationModel.presentationState.createLayoutManager(
layoutDirection = layoutDirection,
density = density,
textStyle = resolvedTextStyle,
resourceLoader = resourceLoader
)

val contentPresentationModel = CommandButtonPresentationModel(
presentationState = CommandButtonPresentationState.Medium,
backgroundAppearanceStrategy = BackgroundAppearanceStrategy.Flat
)
val contentLayoutManager = contentPresentationModel.presentationState.createLayoutManager(
layoutDirection = layoutDirection,
density = density,
textStyle = resolvedTextStyle,
resourceLoader = resourceLoader
)

val stateHorizontal = rememberScrollState(0)
val scope = rememberCoroutineScope()
val scrollAmount = 12.dp.value * density.density

val leftScrollerCommand = Command(text = "",
icon = object : Painter() {
override val intrinsicSize: Size = Size.Unspecified

override fun DrawScope.onDraw() {
drawArrow(
drawScope = this,
width = ComboBoxSizingConstants.DefaultComboBoxArrowHeight.toPx(),
height = ComboBoxSizingConstants.DefaultComboBoxArrowWidth.toPx(),
strokeWidth = 2.0.dp.toPx(),
direction = PopupPlacementStrategy.Startward,
layoutDirection = layoutDirection,
color = Color.Red
)
}
},
isActionEnabled = (stateHorizontal.value > 0),
action = {
scope.launch {
stateHorizontal.scrollTo(
(stateHorizontal.value - scrollAmount.toInt()).coerceAtLeast(0)
)
}
})
val rightScrollerCommand = Command(text = "",
icon = object : Painter() {
override val intrinsicSize: Size = Size.Unspecified

override fun DrawScope.onDraw() {
drawArrow(
drawScope = this,
width = ComboBoxSizingConstants.DefaultComboBoxArrowHeight.toPx(),
height = ComboBoxSizingConstants.DefaultComboBoxArrowWidth.toPx(),
strokeWidth = 2.0.dp.toPx(),
direction = PopupPlacementStrategy.Endward,
layoutDirection = layoutDirection,
color = Color.Red
)
}
},
isActionEnabled = (stateHorizontal.value < stateHorizontal.maxValue),
action = {
scope.launch {
stateHorizontal.scrollTo(
(stateHorizontal.value + scrollAmount.toInt()).coerceAtMost(
stateHorizontal.maxValue
)
)
}
})

Layout(modifier = modifier.fillMaxWidth(),
content = {
// Leftwards scroller
CommandButtonProjection(
contentModel = leftScrollerCommand,
presentationModel = scrollerPresentationModel
).project()

Box(modifier = Modifier.horizontalScroll(stateHorizontal)) {
Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
for (command in commands) {
CommandButtonProjection(
contentModel = command,
presentationModel = contentPresentationModel
).project()
}
}
}

// Rightwards scroller
CommandButtonProjection(
contentModel = rightScrollerCommand,
presentationModel = scrollerPresentationModel
).project()

},
measurePolicy = { measurables, constraints ->
val leftScrollerMeasurable = measurables[0]
val contentMeasurable = measurables[1]
val rightScrollerMeasurable = measurables[2]

val leftScrollerPreLayoutInfo =
scrollerLayoutManager.getPreLayoutInfo(
leftScrollerCommand,
scrollerPresentationModel
)
val leftScrollerSize = scrollerLayoutManager.getPreferredSize(
leftScrollerCommand, scrollerPresentationModel, leftScrollerPreLayoutInfo
)

val rightScrollerPreLayoutInfo =
scrollerLayoutManager.getPreLayoutInfo(
rightScrollerCommand,
scrollerPresentationModel
)
val rightScrollerSize = scrollerLayoutManager.getPreferredSize(
rightScrollerCommand, scrollerPresentationModel, rightScrollerPreLayoutInfo
)

var boxRequiredWidth = 0.0f
var boxHeight = 0
for (command in commands) {
val commandPreLayoutInfo =
contentLayoutManager.getPreLayoutInfo(
command,
contentPresentationModel
)
val commandSize = contentLayoutManager.getPreferredSize(
command, contentPresentationModel, commandPreLayoutInfo
)
boxRequiredWidth += commandSize.width
boxHeight = commandSize.height.toInt()
}

val needScrollers = (boxRequiredWidth > constraints.maxWidth)
val contentWidth = if (needScrollers) constraints.maxWidth -
leftScrollerSize.width - rightScrollerSize.width
else constraints.maxWidth

val leftScrollerPlaceable = leftScrollerMeasurable.measure(
Constraints.fixed(
width = if (needScrollers) leftScrollerSize.width.toInt() else 0,
height = boxHeight
)
)
val rightScrollerPlaceable = rightScrollerMeasurable.measure(
Constraints.fixed(
width = if (needScrollers) rightScrollerSize.width.toInt() else 0,
height = boxHeight
)
)
val contentPlaceable = contentMeasurable.measure(
Constraints.fixed(contentWidth.toInt(), boxHeight)
)

layout(width = constraints.maxWidth, height = boxHeight) {
if (leftScrollerPlaceable.width > 0) {
leftScrollerPlaceable.placeRelative(0, 0)
}
if (rightScrollerPlaceable.width > 0) {
rightScrollerPlaceable.placeRelative(
constraints.maxWidth - rightScrollerPlaceable.width,
0
)
}
contentPlaceable.placeRelative(leftScrollerPlaceable.width, 0)
}
})
}
@@ -0,0 +1,94 @@
/*
* Copyright 2020-2022 Aurora, Kirill Grouchnikov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pushingpixels.aurora.demo

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
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 org.pushingpixels.aurora.common.AuroraInternalApi
import org.pushingpixels.aurora.component.AuroraBreadcrumbBar
import org.pushingpixels.aurora.component.model.Command
import org.pushingpixels.aurora.demo.svg.radiance_menu
import org.pushingpixels.aurora.demo.svg.tango.*
import org.pushingpixels.aurora.theming.IconFilterStrategy
import org.pushingpixels.aurora.theming.marinerSkin
import org.pushingpixels.aurora.window.AuroraApplicationScope
import org.pushingpixels.aurora.window.AuroraWindow
import org.pushingpixels.aurora.window.auroraApplication

fun main() = auroraApplication {
val state = rememberWindowState(
placement = WindowPlacement.Floating,
position = WindowPosition.Aligned(Alignment.Center),
size = DpSize(400.dp, 200.dp)
)
val skin = mutableStateOf(marinerSkin())

AuroraWindow(
skin = skin,
title = "Aurora Demo",
icon = radiance_menu(),
iconFilterStrategy = IconFilterStrategy.ThemedFollowText,
state = state,
undecorated = true,
onCloseRequest = ::exitApplication,
) {
BreadcrumbContent()
}
}

@Composable
fun AuroraApplicationScope.BreadcrumbContent() {
val icons = arrayOf(
accessories_text_editor(),
computer(),
drive_harddisk(),
emblem_system(),
font_x_generic(),
help_browser(),
media_floppy(),
preferences_desktop_locale_2(),
user_home()
)
val commands = icons.map {
Command(
text = "sample",
icon = it,
action = { println("Activated!") }
)
}

Box(modifier = Modifier.fillMaxSize()) {
AuroraBreadcrumbBar(
commands = commands,
modifier = Modifier.fillMaxWidth()
)
}
}





0 comments on commit 18951e1

Please sign in to comment.