Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,56 @@
Facilities and components for writing desktop applications with
the [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) framework.

## Default look and feel

Chords applications use a compact Material 3 desktop theme by default. It
provides neutral work surfaces, semantic light and dark color schemes,
restrained corner radii, compact typography, and shared dimensions for common
controls, navigation, tables, dialogs, and supporting panes. The dark scheme is
selected from the operating system appearance observed at application startup.
Changes to the system appearance while the application is running are not
observed automatically; override `ApplicationTheme` when the application needs
a live theme switch.

The standard Material values are available through `MaterialTheme`. Desktop
values that Material does not define are available through
[`ChordsTheme`](src/main/kotlin/io/spine/chords/core/styling/ChordsTheme.kt):

```kotlin
val rowHeight = ChordsTheme.dimensions.tableRowHeight
val hoverAlpha = ChordsTheme.interaction.hoveredStateAlpha
```

An application can replace the theme in one place by overriding
`Application.ApplicationTheme`:

```kotlin
@Composable
override fun ApplicationTheme(content: @Composable () -> Unit) {
ChordsTheme(
colorScheme = myColorScheme,
typography = myTypography,
shapes = myShapes,
dimensions = ChordsDimensions(
controlHeight = 48.dp,
tableRowHeight = 44.dp
),
content = content
)
}
```

Component properties take precedence over theme values. Class-based
components can also be customized application-wide with `sharedDefaults`.
The effective order is: instance properties, shared component defaults, Chords
desktop tokens, and finally Material theme values.

Text inputs and selectors expose their text style, shape, modifier, and colors.
Dropdowns expose popup shape, elevations, item height, padding, and selection
colors. Tables expose content padding, container/header/row colors, and row
heights. Dialogs, lightweight windows, and wizards expose their unique sizing,
spacing, surface, shape, border, and elevation values.

## Using Spine Chords Core in a Gradle project

Add a dependency to the library as follows:
Expand Down Expand Up @@ -99,7 +149,8 @@ In addition to components, the library includes such facilities:

- Extension functions to address common tasks or current shortcomings in
Compose, like ensuring the usual focus traversal with the Tab key for text
fields (see [Modifier.moveFocusOnTab()](src/main/kotlin/io/spine/chords/core/primitive/TextFieldExts.kt)).
fields (see
[Modifier.moveFocusOnTab()](src/main/kotlin/io/spine/chords/core/primitive/TextFieldExts.kt)).

- **Some simple components** that address common needs like
[CheckboxWithText](src/main/kotlin/io/spine/chords/core/primitive/CheckboxWithText.kt),
Expand Down
113 changes: 91 additions & 22 deletions core/src/main/kotlin/io/spine/chords/core/DropdownListBox.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

package io.spine.chords.core

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand All @@ -46,10 +47,10 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.ShapeDefaults.ExtraSmall
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
Expand Down Expand Up @@ -78,6 +79,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Offset.Companion.Zero
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.Key.Companion.DirectionDown
import androidx.compose.ui.input.key.Key.Companion.DirectionUp
import androidx.compose.ui.input.key.Key.Companion.Escape
Expand Down Expand Up @@ -113,6 +115,7 @@ import io.spine.chords.core.keyboard.KeyRange
import io.spine.chords.core.keyboard.key
import io.spine.chords.core.keyboard.matches
import io.spine.chords.core.primitive.VerticalScrollbar
import io.spine.chords.core.styling.ChordsTheme
import java.awt.event.KeyEvent.CHAR_UNDEFINED
import java.lang.Character.UnicodeBlock
import java.lang.Character.UnicodeBlock.SPECIALS
Expand Down Expand Up @@ -288,6 +291,41 @@ public class DropdownListBox<I> : Component() {
*/
public var unfocusInvoker: (() -> Unit)? = null

/**
* Minimum item height, or `null` to use the current Chords theme value.
*/
public var itemMinHeight: Dp? by mutableStateOf(null)

/**
* Vertical content padding, or `null` to use the current Chords theme value.
*/
public var listContentPadding: Dp? by mutableStateOf(null)

/**
* The popup shape, or `null` to use the current Material small shape.
*/
public var listShape: Shape? by mutableStateOf(null)

/**
* The popup's tonal elevation.
*/
public var listTonalElevation: Dp by mutableStateOf(0.dp)

/**
* The popup's shadow elevation.
*/
public var listShadowElevation: Dp by mutableStateOf(8.dp)

/**
* Selected item background, or `null` to use the theme selection color.
*/
public var selectedItemColor: Color? by mutableStateOf(null)

/**
* Keyboard-preselected item background, or `null` to use the theme hover color.
*/
public var preselectedItemColor: Color? by mutableStateOf(null)

/**
* A density of the screen, it is used when calculating which part
* of drop-down list should be visible to user.
Expand Down Expand Up @@ -384,11 +422,6 @@ public class DropdownListBox<I> : Component() {
*/
private var totalItemsHeight by mutableStateOf(0.dp)

/**
* Vertical padding of drop-down list content.
*/
private val listVerticalPadding = 8.dp

/**
* Heights of none item in drop-down list.
*/
Expand Down Expand Up @@ -867,7 +900,10 @@ public class DropdownListBox<I> : Component() {
* to be used.
*/
@Composable
@Suppress("LongMethod") // Keeps selection ordering and item rendering in one list pass.
private fun BoxScope.DropdownListContent() {
val resolvedItemMinHeight =
itemMinHeight ?: ChordsTheme.dimensions.dropdownItemHeight
Column(
modifier = Modifier
.width(MinWidth)
Expand All @@ -883,10 +919,13 @@ public class DropdownListBox<I> : Component() {
DropdownListNoneItem(
text = noneItemText,
color = if (preselectedItemIndex == -1) {
colorScheme.primary.copy(alpha = 0.1f)
preselectedItemColor ?: colorScheme.primary.copy(
alpha = ChordsTheme.interaction.hoveredStateAlpha
)
} else {
null
},
itemMinHeight = resolvedItemMinHeight,
onMeasureHeight = { measuredHeight ->
noneItemHeight = measuredHeight
},
Expand All @@ -898,11 +937,13 @@ public class DropdownListBox<I> : Component() {
items.forEachIndexed { index, item ->
val color = when (index) {
selectedItemIndex -> {
colorScheme.primary.copy(alpha = 0.2f)
selectedItemColor ?: colorScheme.primaryContainer
}

preselectedItemIndex -> {
colorScheme.primary.copy(alpha = 0.1f)
preselectedItemColor ?: colorScheme.primary.copy(
alpha = ChordsTheme.interaction.hoveredStateAlpha
)
}

else -> {
Expand All @@ -915,13 +956,17 @@ public class DropdownListBox<I> : Component() {
onMeasureHeight = { measuredHeight ->
itemHeights[index] = measuredHeight
},
color = color
color = color,
itemMinHeight = resolvedItemMinHeight
) {
itemContent(item)
}
}
} else {
DropdownListNoItems(content = noItemsContent)
DropdownListNoItems(
itemMinHeight = resolvedItemMinHeight,
content = noItemsContent
)
}
}
VerticalScrollbar(scrollState) {
Expand Down Expand Up @@ -961,13 +1006,20 @@ public class DropdownListBox<I> : Component() {
properties = PopupProperties(focusable = searchSelectionEnabled),
onPreviewKeyEvent = { handleKeyEventWhenDropdownExpanded(it) }
) {
Surface(shape = ExtraSmall, tonalElevation = 3.0.dp, shadowElevation = 3.0.dp) {
val contentPadding = listContentPadding ?: ChordsTheme.dimensions.spacingXSmall
Surface(
shape = listShape ?: MaterialTheme.shapes.small,
color = colorScheme.surface,
tonalElevation = listTonalElevation,
shadowElevation = listShadowElevation,
border = BorderStroke(1.dp, colorScheme.outlineVariant)
) {
visibleListHeight = min(
totalItemsHeight, listAvailableHeight - listVerticalPadding * 2
totalItemsHeight, listAvailableHeight - contentPadding * 2
)
Box(
modifier = Modifier
.padding(vertical = listVerticalPadding)
.padding(vertical = contentPadding)
.height(visibleListHeight)
) {
if (scrollPositionRequested != null) {
Expand Down Expand Up @@ -1160,6 +1212,8 @@ private class DropdownListBoxScopeImpl(
* callback that is invoked when item is positioned.
* @param color
* the background color of drop-down list item.
* @param itemMinHeight
* the minimum height of the item.
* @param content
* content to be displayed inside drop-down list item.
*/
Expand All @@ -1168,6 +1222,7 @@ private fun DropdownListItem(
onClick: () -> Unit,
onMeasureHeight: (Int) -> Unit,
color: Color?,
itemMinHeight: Dp,
content: @Composable () -> Unit
) {
val itemHeight = remember { mutableStateOf(0) }
Expand All @@ -1179,7 +1234,7 @@ private fun DropdownListItem(
onClick = onClick
)
.fillMaxWidth()
.heightIn(48.dp)
.heightIn(itemMinHeight)
.onGloballyPositioned {
val height = it.size.height
if (height != itemHeight.value) {
Expand All @@ -1197,22 +1252,29 @@ private fun DropdownListItem(
/**
* The drop-down list without items.
*
* @param itemMinHeight
* the minimum height of the item.
* @param content
* the content to be shown when drop-down list doesn't have any items.
*/
@Composable
private fun DropdownListNoItems(content: @Composable (() -> Unit)) {
private fun DropdownListNoItems(
itemMinHeight: Dp,
content: @Composable (() -> Unit)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(48.dp)
.padding(horizontal = 12.dp)
.heightIn(itemMinHeight)
.padding(horizontal = ChordsTheme.dimensions.spacingMedium)
.background(Transparent),
verticalAlignment = CenterVertically,
horizontalArrangement = Center
) {
StyledContent(
contentColor = colorScheme.secondary.copy(alpha = 0.5f),
contentColor = colorScheme.onSurfaceVariant.copy(
alpha = ChordsTheme.interaction.disabledContentAlpha
),
textStyle = typography.titleSmall,
content = content
)
Expand All @@ -1226,6 +1288,8 @@ private fun DropdownListNoItems(content: @Composable (() -> Unit)) {
* the text to be displayed for drop-down list none item.
* @param color
* the background color of drop-down list none item.
* @param itemMinHeight
* the minimum height of the item.
* @param onMeasureHeight
* callback that is invoked when item is positioned.
* @param onClick
Expand All @@ -1235,6 +1299,7 @@ private fun DropdownListNoItems(content: @Composable (() -> Unit)) {
private fun DropdownListNoneItem(
text: String = "<None>",
color: Color? = null,
itemMinHeight: Dp,
onMeasureHeight: (Int) -> Unit,
onClick: () -> Unit
) {
Expand All @@ -1254,16 +1319,20 @@ private fun DropdownListNoneItem(
}
}
.fillMaxWidth()
.heightIn(48.dp)
.heightIn(itemMinHeight)
.background(color ?: Transparent),
verticalAlignment = CenterVertically
) {
StyledContent(
contentColor = colorScheme.secondary.copy(alpha = 0.5f),
contentColor = colorScheme.onSurfaceVariant.copy(
alpha = ChordsTheme.interaction.disabledContentAlpha
),
content = {
Text(
text = text,
modifier = Modifier.padding(horizontal = 12.dp)
modifier = Modifier.padding(
horizontal = ChordsTheme.dimensions.spacingMedium
)
)
}
)
Expand Down
Loading
Loading