Skip to content
Merged
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
20 changes: 10 additions & 10 deletions client/src/main/kotlin/io/spine/chords/client/EntityChooser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public abstract class EntityChooser<
> : DropdownSelector<I>() {

/**
* An enumeration of class's type parameters.
* An enumeration of the class's type parameters.
*/
@Suppress("unused" /* All type parameters are listed for
completeness despite only a part of them might be used via this enum. */)
Expand All @@ -92,10 +92,8 @@ public abstract class EntityChooser<
private val entityStatesByIds: HashMap<I, E> = hashMapOf()

/**
* A function which has to be implemented to provide a value of
* type `Class[E]` for the component which is being implemented.
*
* E.g., if [E] is a `User` class, it would just specify `User::class.java`.
* Identifies a type specified for the [E] type parameter by the
* implementation of the actual component's instance.
*/
@OptIn(ExperimentalStdlibApi::class)
private val entityStateClass: Class<E> get() {
Expand All @@ -108,11 +106,13 @@ public abstract class EntityChooser<
"The V type parameter (entity value type) cannot be a star <*> projection. "
}

// We only have the entity state type as an abstract `Type` value at
// runtime, so we have no other choice than to explicitly cast it to
// Class<E> here (it's safe as long as `TypeParameters` enum is kept
// up to date).
@Suppress("UNCHECKED_CAST")
@Suppress(
// We only have the entity state type as an abstract `Type` value at
// runtime, so we have no other choice than to explicitly cast it to
// Class<E> here (it's safe as long as `TypeParameters` enum is kept
// up to date).
"UNCHECKED_CAST"
)
return entityStateType.javaType as Class<E>
}

Expand Down
3 changes: 2 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
*/

import io.spine.internal.dependency.Kotest
import io.spine.internal.dependency.Kotlin
import io.spine.internal.dependency.Material3
import io.spine.internal.dependency.Voyager

Expand All @@ -34,7 +35,7 @@ plugins {

dependencies {
api(Voyager.navigator)
implementation(project(":runtime"))
implementation(Kotlin.reflect)
implementation(compose.desktop.currentOs)
implementation(Material3.Desktop.lib)
testImplementation(Kotest.runnerJUnit5)
Expand Down
88 changes: 74 additions & 14 deletions core/src/main/kotlin/io/spine/chords/core/Component.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.spine.chords.core.appshell.Props
import kotlin.reflect.javaType
import kotlin.reflect.full.allSupertypes
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -180,14 +182,12 @@ import kotlinx.coroutines.launch
* perspective this would just be a `someProp` property that can be configured
* with any `String` value.
*
* Here's an example of creating an input component that allows entering
* Here's an example of creating an input component that displays
* a string value:
*
* ```kotlin
* public class HelloComponent : Component() {
* public companion object : ComponentSetup<HelloComponent>({
* HelloComponent()
* })
* public companion object : ComponentSetup<HelloComponent>()
*
* public var name: String by mutableStateOf("")
*
Expand Down Expand Up @@ -234,9 +234,8 @@ import kotlinx.coroutines.launch
*
* ```kotlin
* public class StyledHelloComponent : Component() {
* public companion object : ComponentSetup<StyledHelloComponent>({
* StyledHelloComponent() // <-- Note child class name. ^^^
* })
* public companion object : ComponentSetup<StyledHelloComponent>()
* // Note child class name here ^^^
*
* // Add some more component customization properties...
* public var style: TextStyle by mutableStateOf(MaterialTheme.typography.bodyMedium)
Expand Down Expand Up @@ -345,7 +344,7 @@ import kotlinx.coroutines.launch
* Here's an example:
* ```
* public class HelloComponent : Component() {
* public companion object : ComponentSetup<HelloComponent>({ HelloComponent() })
* public companion object : ComponentSetup<HelloComponent>()
*
* public lateinit var name: String
*
Expand Down Expand Up @@ -838,7 +837,7 @@ public abstract class Component : DefaultPropsOwnerBase() {
* @see ComponentSetup
*/
public abstract class AbstractComponentSetup(
protected val createInstance: (() -> Component)? = null
protected var createInstance: (() -> Component)? = null
) {

/**
Expand Down Expand Up @@ -946,14 +945,75 @@ public abstract class AbstractComponentSetup(
* should be instantiated and rendered via this companion object).
*
* @constructor Creates a companion object for a component of type [C].
* @param createInstance A lambda that should create a component's instance of
* type [C] with the given properties configuration callback.
* @param createInstance An optional lambda that should create a component's
* instance of type [C]. In most cases (when a component has the recommended
* no-args constructor), there's no need to specify this parameter.
* @see AbstractComponentSetup
*/
public open class ComponentSetup<C: Component>(
createInstance: () -> C
public abstract class ComponentSetup<
C : Component // Keep the list of type parameters in sync with the `TypeParameters` enum!
>(
createInstance: (() -> C)? = null
) : AbstractComponentSetup(createInstance) {

/**
* An enumeration of the class's type parameters.
*/
private enum class TypeParameters(val index: Int) {

/**
* The component type parameter (the class's `C` parameter).
*/
COMPONENT_TYPE(0),
}

init {
if (this.createInstance == null) {
this.createInstance = {
createInstanceByTypeParameter()
}
}
}

/**
* Creates an instance of a component by its type parameter [C], which is
* detected automatically from the class instance's implementation.
*/
private fun createInstanceByTypeParameter(): C {
val cls = componentClass
val defaultConstructor = cls.declaredConstructors.find { it.parameters.isEmpty() }
checkNotNull(defaultConstructor) {
"A component must have a no-args constructor: $cls"
}
@Suppress("UNCHECKED_CAST")
return defaultConstructor.newInstance() as C
}

/**
* Identifies a type specified for the [C] type parameter by the companion
* object that implements this class.
*/
@OptIn(ExperimentalStdlibApi::class)
private val componentClass: Class<C> get() {
val parameterizedEntityChooserType = this::class.allSupertypes.first {
it.classifier == ComponentSetup::class
}
val entityStateType =
parameterizedEntityChooserType.arguments[TypeParameters.COMPONENT_TYPE.index].type
checkNotNull(entityStateType) {
"The C type parameter (component type) cannot be a star <*> projection."
}

@Suppress(
// We only have the component type as an abstract `Type` value at
// runtime, so we have no other choice than to explicitly cast it to
// Class<E> here (it's safe as long as `TypeParameters` enum is kept
// up to date).
"UNCHECKED_CAST"
)
return entityStateType.javaType as Class<C>
}

/**
* Declares an instance of component of type [C] with the respective
* property value specifications.
Expand All @@ -965,7 +1025,7 @@ public open class ComponentSetup<C: Component>(
* Once an instance is created, it is saved using [remember] and is reused
* for subsequent recompositions.
*
* @param props A lambda that is invoked in context of the component's
* @param props A lambda that is invoked in the context of the component's
* instance, which should configure its properties in a way that is needed
* for this component's instance. It is invoked before each recomposition
* of the component.
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/kotlin/io/spine/chords/core/DropdownSelector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
Expand Down Expand Up @@ -125,6 +127,12 @@ public abstract class DropdownSelector<I> : InputComponent<I>() {
*/
public var modifier: Modifier by mutableStateOf(Modifier)

/**
* A [TextFieldColors] instance, which defines the color scheme for
* the selector's field.
*/
public lateinit var fieldColors: TextFieldColors

/**
* Indicates whether the drop-down menu is expanded or not.
*/
Expand Down Expand Up @@ -193,6 +201,9 @@ public abstract class DropdownSelector<I> : InputComponent<I>() {

@Composable
override fun content() {
if (!::fieldColors.isInitialized) {
fieldColors = TextFieldDefaults.colors()
}
val fieldText = getFieldText(searchString)

SideEffect {
Expand Down Expand Up @@ -236,6 +247,7 @@ public abstract class DropdownSelector<I> : InputComponent<I>() {
}
},
textStyle = fieldTextStyle,
colors = fieldColors,
modifier = modifier
.focusRequester(this@DropdownSelector.focusRequester)
.moveFocusOnTab()
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/io/spine/chords/core/InputComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -236,15 +236,15 @@ public abstract class InputComponent<V> : FocusableComponent() {
* implementation to ensure that the value specified in this property is
* displayed as needed.
*/
public open var externalValidationMessage: State<String?>? = null
public var externalValidationMessage: State<String?>? = null

/**
* A callback, which is invoked every time when a component transitions
* between an empty and dirty state (in any direction). A callback has
* an argument of `true`, when the new state is dirty, and `false` when it
* becomes empty.
*/
public open var onDirtyStateChange: ((Boolean) -> Unit)? = null
public var onDirtyStateChange: ((Boolean) -> Unit)? = null

/**
* A context that contains any data that affects input components that
Expand Down
17 changes: 15 additions & 2 deletions core/src/main/kotlin/io/spine/chords/core/InputField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
Expand All @@ -50,6 +52,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.input.VisualTransformation.Companion.None
import io.spine.chords.core.InputReviser.Companion.maxLength
import io.spine.chords.core.keyboard.KeyRange.Companion.Digit
import io.spine.chords.core.keyboard.KeyRange.Companion.Whitespace
import io.spine.chords.core.keyboard.matches
Expand Down Expand Up @@ -306,6 +309,12 @@ public open class InputField<V> : InputComponent<V>() {
*/
public var textStyle: TextStyle? by mutableStateOf(null)

/**
* A [TextFieldColors] instance, which defines the color scheme for
* this field.
*/
public lateinit var colors: TextFieldColors

/**
* An [InputReviser] that can be specified to modify the user's input before
* it is applied to the input field.
Expand Down Expand Up @@ -367,13 +376,13 @@ public open class InputField<V> : InputComponent<V>() {
protected open var multiline: Boolean by mutableStateOf(false)

/**
* Minimum number of visible lines
* Minimum number of visible lines of text
* (applicable only if [multiline] == `true`).
*/
protected open var minLines: Int by mutableStateOf(1)

/**
* Maximum number of visible lines
* Maximum number of visible lines of text
* (applicable only if [multiline] == `true`).
*/
protected open var maxLines: Int by mutableStateOf(MAX_VALUE)
Expand Down Expand Up @@ -443,6 +452,9 @@ public open class InputField<V> : InputComponent<V>() {

@Composable
override fun content(): Unit = recompositionWorkaround {
if (!::colors.isInitialized) {
colors = TextFieldDefaults.colors()
}
val textStyle = textStyle ?: LocalTextStyle.current
val rawTextContent = getRawTextContent()

Expand Down Expand Up @@ -477,6 +489,7 @@ public open class InputField<V> : InputComponent<V>() {
maxLines = if (multiline) maxLines else 1,
enabled = enabled,
textStyle = textStyle,
colors = colors,
modifier = modifier(modifier)
.focusRequester(focusRequester)
.preventWidthAutogrowing()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.Navigator
import io.spine.chords.runtime.safeCast

/**
* The main screen of the application.
Expand Down Expand Up @@ -88,5 +87,5 @@ public class MainScreen(
* Returns the currently selected view.
*/
internal val currentView: AppView
get() = viewNavigator.lastItem.safeCast<AppView>()
get() = viewNavigator.lastItem as AppView
}
2 changes: 1 addition & 1 deletion core/src/main/kotlin/io/spine/chords/core/layout/Wizard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public abstract class Wizard : Component() {
onFinishClick = {
handleFinishClick(currentPage)
},
onCancelClick = { onCloseRequest?.invoke() },
onCancelClick = { close() },
isOnFirstPage = isOnFirstPage(),
isOnLastPage = isOnLastPage(),
submitting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import io.spine.chords.core.InputReviser
* An [InputField] implementation that allows entering `String` values.
*/
public class StringField : InputField<String>() {
public companion object : ComponentSetup<StringField>({ StringField() })
public companion object : ComponentSetup<StringField>()

public override var inputReviser: InputReviser?
get() = super.inputReviser
Expand Down
Loading
Loading