Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
f9e9584
commit 18951e1
Showing
2 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
234 changes: 234 additions & 0 deletions
234
component/src/desktopMain/kotlin/org/pushingpixels/aurora/component/AuroraBreadcrumbBar.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
}) | ||
} |
94 changes: 94 additions & 0 deletions
94
demo/src/desktopMain/kotlin/org/pushingpixels/aurora/demo/AuroraBreadcrumbBarDemo.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
) | ||
} | ||
} | ||
|
||
|
||
|
||
|
||
|