Skip to content

Commit eccdbbc

Browse files
authored
Merge pull request #162 from SpineEventEngine/modern-default-look-and-feel.
Modernize the default Chords look and feel.
2 parents 8669121 + 7402e88 commit eccdbbc

51 files changed

Lines changed: 4876 additions & 417 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/README.md

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,73 @@
33
Facilities and components for writing desktop applications with
44
the [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) framework.
55

6+
## Default look and feel
7+
8+
Chords applications use a compact Material 3 desktop theme by default. It
9+
provides neutral work surfaces, semantic light and dark color schemes,
10+
restrained corner radii, regular-weight typography, and shared dimensions for common
11+
controls, navigation, tables, dialogs, and supporting panes. The dark scheme is
12+
selected from the operating system appearance observed at application startup.
13+
Changes to the system appearance while the application is running are not
14+
observed automatically; override `ApplicationTheme` when the application needs
15+
a live theme switch.
16+
17+
The standard Material values are available through `MaterialTheme`. Desktop
18+
values that Material does not define are available through
19+
[`ChordsTheme`](src/main/kotlin/io/spine/chords/core/styling/ChordsTheme.kt):
20+
21+
```kotlin
22+
val rowHeight = ChordsTheme.dimensions.tableRowHeight
23+
val hoverAlpha = ChordsTheme.interaction.hoveredStateAlpha
24+
```
25+
26+
An application can replace the theme in one place by overriding
27+
`Application.ApplicationTheme`:
28+
29+
```kotlin
30+
@Composable
31+
override fun ApplicationTheme(content: @Composable () -> Unit) {
32+
ChordsTheme(
33+
colorScheme = myColorScheme,
34+
typography = myTypography,
35+
shapes = myShapes,
36+
dimensions = ChordsDimensions(
37+
controlHeight = 48.dp,
38+
tableRowHeight = 44.dp
39+
),
40+
content = content
41+
)
42+
}
43+
```
44+
45+
Component properties take precedence over theme values. Class-based
46+
components can also be customized application-wide with `sharedDefaults`.
47+
The effective order is: instance properties, shared component defaults, Chords
48+
desktop tokens, and finally Material theme values.
49+
50+
Menus, dialogs, and tooltips use `ChordsTheme.overlayColor` and a visible outline to separate
51+
layers in dark themes. Custom popup panels can use
52+
[`PopupSurface`](src/main/kotlin/io/spine/chords/core/layout/PopupSurface.kt); Material
53+
dropdown menus can apply `Modifier.popupAppearance()` from the same file.
54+
Filled actions use
55+
[`PrimaryButton`](src/main/kotlin/io/spine/chords/core/primitive/PrimaryButton.kt);
56+
outlined actions use
57+
[`SecondaryButton`](src/main/kotlin/io/spine/chords/core/primitive/SecondaryButton.kt).
58+
For actions represented by a single icon, use
59+
[`CircularIconButton`](src/main/kotlin/io/spine/chords/core/primitive/CircularIconButton.kt).
60+
These buttons provide hover and keyboard focus feedback in the active window.
61+
[`EmptyState`](src/main/kotlin/io/spine/chords/core/layout/EmptyState.kt) supplies the illustration,
62+
caption, and alignment for empty work areas.
63+
[`ScrollableColumn`](src/main/kotlin/io/spine/chords/core/layout/ScrollableColumn.kt) keeps long
64+
details reachable within a bounded pane. Each component's KDoc includes usage examples.
65+
Confirmation prompts share `ChordsTheme.confirmationTextStyle`.
66+
67+
Text inputs and selectors expose their text style, shape, modifier, and colors.
68+
Dropdowns expose popup shape, elevations, item height, padding, and selection
69+
colors. Tables expose content padding, container/header/row colors, and row
70+
heights. Dialogs, lightweight windows, and wizards expose their unique sizing,
71+
spacing, surface, shape, border, and elevation values.
72+
673
## Using Spine Chords Core in a Gradle project
774

875
Add a dependency to the library as follows:
@@ -99,7 +166,8 @@ In addition to components, the library includes such facilities:
99166

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

104172
- **Some simple components** that address common needs like
105173
[CheckboxWithText](src/main/kotlin/io/spine/chords/core/primitive/CheckboxWithText.kt),

core/src/main/kotlin/io/spine/chords/core/DropdownListBox.kt

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,10 @@ import androidx.compose.foundation.text.BasicTextField
4646
import androidx.compose.foundation.verticalScroll
4747
import androidx.compose.material3.ExperimentalMaterial3Api
4848
import androidx.compose.material3.LocalContentColor
49+
import androidx.compose.material3.MaterialTheme
4950
import androidx.compose.material3.MaterialTheme.colorScheme
5051
import androidx.compose.material3.MaterialTheme.typography
5152
import androidx.compose.material3.ProvideTextStyle
52-
import androidx.compose.material3.ShapeDefaults.ExtraSmall
53-
import androidx.compose.material3.Surface
5453
import androidx.compose.material3.Text
5554
import androidx.compose.material3.TextField
5655
import androidx.compose.material3.TextFieldDefaults
@@ -78,6 +77,7 @@ import androidx.compose.ui.geometry.Offset
7877
import androidx.compose.ui.geometry.Offset.Companion.Zero
7978
import androidx.compose.ui.graphics.Color
8079
import androidx.compose.ui.graphics.Color.Companion.Transparent
80+
import androidx.compose.ui.graphics.Shape
8181
import androidx.compose.ui.input.key.Key.Companion.DirectionDown
8282
import androidx.compose.ui.input.key.Key.Companion.DirectionUp
8383
import androidx.compose.ui.input.key.Key.Companion.Escape
@@ -112,7 +112,9 @@ import io.spine.chords.core.keyboard.KeyModifiers.Companion.Shift
112112
import io.spine.chords.core.keyboard.KeyRange
113113
import io.spine.chords.core.keyboard.key
114114
import io.spine.chords.core.keyboard.matches
115+
import io.spine.chords.core.layout.PopupSurface
115116
import io.spine.chords.core.primitive.VerticalScrollbar
117+
import io.spine.chords.core.styling.ChordsTheme
116118
import java.awt.event.KeyEvent.CHAR_UNDEFINED
117119
import java.lang.Character.UnicodeBlock
118120
import java.lang.Character.UnicodeBlock.SPECIALS
@@ -288,6 +290,41 @@ public class DropdownListBox<I> : Component() {
288290
*/
289291
public var unfocusInvoker: (() -> Unit)? = null
290292

293+
/**
294+
* Minimum item height, or `null` to use the current Chords theme value.
295+
*/
296+
public var itemMinHeight: Dp? by mutableStateOf(null)
297+
298+
/**
299+
* Vertical content padding, or `null` to use the current Chords theme value.
300+
*/
301+
public var listContentPadding: Dp? by mutableStateOf(null)
302+
303+
/**
304+
* The popup shape, or `null` to use the current Material small shape.
305+
*/
306+
public var listShape: Shape? by mutableStateOf(null)
307+
308+
/**
309+
* The popup's tonal elevation.
310+
*/
311+
public var listTonalElevation: Dp by mutableStateOf(0.dp)
312+
313+
/**
314+
* The popup's shadow elevation.
315+
*/
316+
public var listShadowElevation: Dp by mutableStateOf(8.dp)
317+
318+
/**
319+
* Selected item background, or `null` to use the theme selection color.
320+
*/
321+
public var selectedItemColor: Color? by mutableStateOf(null)
322+
323+
/**
324+
* Keyboard-preselected item background, or `null` to use the theme hover color.
325+
*/
326+
public var preselectedItemColor: Color? by mutableStateOf(null)
327+
291328
/**
292329
* A density of the screen, it is used when calculating which part
293330
* of drop-down list should be visible to user.
@@ -384,11 +421,6 @@ public class DropdownListBox<I> : Component() {
384421
*/
385422
private var totalItemsHeight by mutableStateOf(0.dp)
386423

387-
/**
388-
* Vertical padding of drop-down list content.
389-
*/
390-
private val listVerticalPadding = 8.dp
391-
392424
/**
393425
* Heights of none item in drop-down list.
394426
*/
@@ -867,7 +899,10 @@ public class DropdownListBox<I> : Component() {
867899
* to be used.
868900
*/
869901
@Composable
902+
@Suppress("LongMethod") // Keeps selection ordering and item rendering in one list pass.
870903
private fun BoxScope.DropdownListContent() {
904+
val resolvedItemMinHeight =
905+
itemMinHeight ?: ChordsTheme.dimensions.dropdownItemHeight
871906
Column(
872907
modifier = Modifier
873908
.width(MinWidth)
@@ -883,10 +918,13 @@ public class DropdownListBox<I> : Component() {
883918
DropdownListNoneItem(
884919
text = noneItemText,
885920
color = if (preselectedItemIndex == -1) {
886-
colorScheme.primary.copy(alpha = 0.1f)
921+
preselectedItemColor ?: colorScheme.primary.copy(
922+
alpha = ChordsTheme.interaction.hoveredStateAlpha
923+
)
887924
} else {
888925
null
889926
},
927+
itemMinHeight = resolvedItemMinHeight,
890928
onMeasureHeight = { measuredHeight ->
891929
noneItemHeight = measuredHeight
892930
},
@@ -898,11 +936,13 @@ public class DropdownListBox<I> : Component() {
898936
items.forEachIndexed { index, item ->
899937
val color = when (index) {
900938
selectedItemIndex -> {
901-
colorScheme.primary.copy(alpha = 0.2f)
939+
selectedItemColor ?: colorScheme.primaryContainer
902940
}
903941

904942
preselectedItemIndex -> {
905-
colorScheme.primary.copy(alpha = 0.1f)
943+
preselectedItemColor ?: colorScheme.primary.copy(
944+
alpha = ChordsTheme.interaction.hoveredStateAlpha
945+
)
906946
}
907947

908948
else -> {
@@ -915,13 +955,17 @@ public class DropdownListBox<I> : Component() {
915955
onMeasureHeight = { measuredHeight ->
916956
itemHeights[index] = measuredHeight
917957
},
918-
color = color
958+
color = color,
959+
itemMinHeight = resolvedItemMinHeight
919960
) {
920961
itemContent(item)
921962
}
922963
}
923964
} else {
924-
DropdownListNoItems(content = noItemsContent)
965+
DropdownListNoItems(
966+
itemMinHeight = resolvedItemMinHeight,
967+
content = noItemsContent
968+
)
925969
}
926970
}
927971
VerticalScrollbar(scrollState) {
@@ -961,13 +1005,18 @@ public class DropdownListBox<I> : Component() {
9611005
properties = PopupProperties(focusable = searchSelectionEnabled),
9621006
onPreviewKeyEvent = { handleKeyEventWhenDropdownExpanded(it) }
9631007
) {
964-
Surface(shape = ExtraSmall, tonalElevation = 3.0.dp, shadowElevation = 3.0.dp) {
1008+
val contentPadding = listContentPadding ?: ChordsTheme.dimensions.spacingXSmall
1009+
PopupSurface(
1010+
shape = listShape ?: MaterialTheme.shapes.small,
1011+
tonalElevation = listTonalElevation,
1012+
shadowElevation = listShadowElevation
1013+
) {
9651014
visibleListHeight = min(
966-
totalItemsHeight, listAvailableHeight - listVerticalPadding * 2
1015+
totalItemsHeight, listAvailableHeight - contentPadding * 2
9671016
)
9681017
Box(
9691018
modifier = Modifier
970-
.padding(vertical = listVerticalPadding)
1019+
.padding(vertical = contentPadding)
9711020
.height(visibleListHeight)
9721021
) {
9731022
if (scrollPositionRequested != null) {
@@ -1160,6 +1209,8 @@ private class DropdownListBoxScopeImpl(
11601209
* callback that is invoked when item is positioned.
11611210
* @param color
11621211
* the background color of drop-down list item.
1212+
* @param itemMinHeight
1213+
* the minimum height of the item.
11631214
* @param content
11641215
* content to be displayed inside drop-down list item.
11651216
*/
@@ -1168,6 +1219,7 @@ private fun DropdownListItem(
11681219
onClick: () -> Unit,
11691220
onMeasureHeight: (Int) -> Unit,
11701221
color: Color?,
1222+
itemMinHeight: Dp,
11711223
content: @Composable () -> Unit
11721224
) {
11731225
val itemHeight = remember { mutableStateOf(0) }
@@ -1179,7 +1231,7 @@ private fun DropdownListItem(
11791231
onClick = onClick
11801232
)
11811233
.fillMaxWidth()
1182-
.heightIn(48.dp)
1234+
.heightIn(itemMinHeight)
11831235
.onGloballyPositioned {
11841236
val height = it.size.height
11851237
if (height != itemHeight.value) {
@@ -1197,22 +1249,29 @@ private fun DropdownListItem(
11971249
/**
11981250
* The drop-down list without items.
11991251
*
1252+
* @param itemMinHeight
1253+
* the minimum height of the item.
12001254
* @param content
12011255
* the content to be shown when drop-down list doesn't have any items.
12021256
*/
12031257
@Composable
1204-
private fun DropdownListNoItems(content: @Composable (() -> Unit)) {
1258+
private fun DropdownListNoItems(
1259+
itemMinHeight: Dp,
1260+
content: @Composable (() -> Unit)
1261+
) {
12051262
Row(
12061263
modifier = Modifier
12071264
.fillMaxWidth()
1208-
.heightIn(48.dp)
1209-
.padding(horizontal = 12.dp)
1265+
.heightIn(itemMinHeight)
1266+
.padding(horizontal = ChordsTheme.dimensions.spacingMedium)
12101267
.background(Transparent),
12111268
verticalAlignment = CenterVertically,
12121269
horizontalArrangement = Center
12131270
) {
12141271
StyledContent(
1215-
contentColor = colorScheme.secondary.copy(alpha = 0.5f),
1272+
contentColor = colorScheme.onSurfaceVariant.copy(
1273+
alpha = ChordsTheme.interaction.disabledContentAlpha
1274+
),
12161275
textStyle = typography.titleSmall,
12171276
content = content
12181277
)
@@ -1226,6 +1285,8 @@ private fun DropdownListNoItems(content: @Composable (() -> Unit)) {
12261285
* the text to be displayed for drop-down list none item.
12271286
* @param color
12281287
* the background color of drop-down list none item.
1288+
* @param itemMinHeight
1289+
* the minimum height of the item.
12291290
* @param onMeasureHeight
12301291
* callback that is invoked when item is positioned.
12311292
* @param onClick
@@ -1235,6 +1296,7 @@ private fun DropdownListNoItems(content: @Composable (() -> Unit)) {
12351296
private fun DropdownListNoneItem(
12361297
text: String = "<None>",
12371298
color: Color? = null,
1299+
itemMinHeight: Dp,
12381300
onMeasureHeight: (Int) -> Unit,
12391301
onClick: () -> Unit
12401302
) {
@@ -1254,16 +1316,20 @@ private fun DropdownListNoneItem(
12541316
}
12551317
}
12561318
.fillMaxWidth()
1257-
.heightIn(48.dp)
1319+
.heightIn(itemMinHeight)
12581320
.background(color ?: Transparent),
12591321
verticalAlignment = CenterVertically
12601322
) {
12611323
StyledContent(
1262-
contentColor = colorScheme.secondary.copy(alpha = 0.5f),
1324+
contentColor = colorScheme.onSurfaceVariant.copy(
1325+
alpha = ChordsTheme.interaction.disabledContentAlpha
1326+
),
12631327
content = {
12641328
Text(
12651329
text = text,
1266-
modifier = Modifier.padding(horizontal = 12.dp)
1330+
modifier = Modifier.padding(
1331+
horizontal = ChordsTheme.dimensions.spacingMedium
1332+
)
12671333
)
12681334
}
12691335
)

0 commit comments

Comments
 (0)