Skip to content

Commit de2be1a

Browse files
committed
feat: implement support for custom visualizers
1 parent 4d78d53 commit de2be1a

File tree

9 files changed

+140
-108
lines changed

9 files changed

+140
-108
lines changed

modules/config-impl/build.gradle.kts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@
2424
* <https://polyfrost.org/legal/oneconfig/additional-terms>
2525
*/
2626

27+
plugins {
28+
alias(libs.plugins.kotlin.compose)
29+
}
30+
2731
dependencies {
2832
compileOnly("dev.deftu:omnicore-1.8.9-forge:${libs.versions.omnicore.get()}")
2933
implementation(libs.bundles.nightconfig)
3034
api(project(":modules:config"))
3135
api(project(":modules:poly-compose"))
32-
}
33-
36+
api(libs.jetbrains.compose.runtime)
37+
}

modules/config-impl/src/main/kotlin/org/polyfrost/oneconfig/api/config/v1/KtConfig.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,41 +57,41 @@ open class KtConfig(id: String, title: String, category: Category, icon: String?
5757
* create a new delegate for the given property.
5858
*/
5959
@JvmSynthetic
60-
protected inline fun <reified T> property(def: T? = null, name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null, visualizer: Class<out Visualizer>) =
60+
protected inline fun <reified T> property(def: T? = null, name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null, visualizer: Visualizer) =
6161
Provider(def, name, description, category, subcategory, T::class.java, visualizer)
6262

6363
@JvmSynthetic
6464
protected fun switch(def: Boolean = false, name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null) =
65-
Provider(def, name, description, category, subcategory, Boolean::class.java, Visualizer.SwitchVisualizer::class.java)
65+
Provider(def, name, description, category, subcategory, Boolean::class.java, Visualizer.SwitchVisualizer())
6666

6767
@JvmSynthetic
6868
protected fun color(def: PolyColor = PolyColor.rgba(0, 0, 0, 255), name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null) =
69-
Provider(def, name, description, category, subcategory, PolyColor::class.java, Visualizer.ColorVisualizer::class.java)
69+
Provider(def, name, description, category, subcategory, PolyColor::class.java, Visualizer.ColorVisualizer())
7070

7171
@JvmSynthetic
7272
protected fun slider(min: Float = 0f, max: Float = 0f, def: Float = 0f, name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null) =
73-
Provider(def, name, description, category, subcategory, Float::class.java, Visualizer.SliderVisualizer::class.java) {
73+
Provider(def, name, description, category, subcategory, Float::class.java, Visualizer.SliderVisualizer()) {
7474
addMetadata("min", min)
7575
addMetadata("max", max)
7676
}
7777

7878
@JvmSynthetic
7979
protected fun text(def: String = "", name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null) =
80-
Provider(def, name, description, category, subcategory, String::class.java, Visualizer.TextVisualizer::class.java)
80+
Provider(def, name, description, category, subcategory, String::class.java, Visualizer.TextVisualizer())
8181
// TODO: binds, why is this part of polyui in the first place?
8282
// @JvmSynthetic
8383
// protected fun keybind(def: PolyBind? = null, name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null) =
8484
// Provider(def, name, description, category, subcategory, PolyBind::class.java, Visualizer.KeybindVisualizer::class.java)
8585

8686
@JvmSynthetic
8787
protected fun radiobutton(options: Array<String>, def: Int = 0, name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null) =
88-
Provider(def, name, description, category, subcategory, Int::class.java, Visualizer.RadioVisualizer::class.java) {
88+
Provider(def, name, description, category, subcategory, Int::class.java, Visualizer.RadioVisualizer()) {
8989
addMetadata("options", options)
9090
}
9191

9292
@JvmSynthetic
9393
protected fun dropdown(options: Array<String>, def: Int = 0, name: String? = null, description: String? = null, category: String? = null, subcategory: String? = null) =
94-
Provider(def, name, description, category, subcategory, Int::class.java, Visualizer.DropdownVisualizer::class.java) {
94+
Provider(def, name, description, category, subcategory, Int::class.java, Visualizer.DropdownVisualizer()) {
9595
addMetadata("options", options)
9696
}
9797

@@ -118,7 +118,7 @@ open class KtConfig(id: String, title: String, category: Category, icon: String?
118118
private val category: String?,
119119
private val subcategory: String?,
120120
private val type: Class<T>,
121-
private val visualizer: Class<out Visualizer>,
121+
private val visualizer: Visualizer,
122122
private val extra: (Property<T>.() -> Unit)? = null
123123
) : PropertyDelegateProvider<KtConfig, ReadWriteProperty<KtConfig, T>> {
124124
override operator fun provideDelegate(thisRef: KtConfig, property: KProperty<*>): ReadWriteProperty<KtConfig, T> {

modules/config-impl/src/main/kotlin/org/polyfrost/oneconfig/api/config/v1/Visualizer.kt

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,47 +26,91 @@
2626

2727
package org.polyfrost.oneconfig.api.config.v1
2828

29+
import androidx.compose.runtime.Composable
30+
2931
/**
30-
* Visualizers are procedures that take a property, and return a drawable that represents it.
32+
* Visualizers are procedures that take a [Property] and create a composable to represent it.
33+
*
34+
* To create a custom visualizer, implement this interface or pass a lambda:
35+
* ```kotlin
36+
* val myVisualizer = Visualizer { prop -> Text(prop.get().toString()) }
37+
* ```
38+
*
39+
* The built-in visualizer token classes (e.g. [SwitchVisualizer]) delegate their rendering
40+
* to implementations registered via [register], which are provided by the UI layer.
41+
* There is no distinction between built-in and third-party visualizers, both use the same API.
3142
*/
3243
@Suppress("UNCHECKED_CAST")
33-
interface Visualizer {
44+
fun interface Visualizer {
45+
46+
@Composable
47+
fun visualize(prop: Property<*>)
48+
3449
class ButtonVisualizer : Visualizer {
50+
@Composable override fun visualize(prop: Property<*>) { Visualizer[ButtonVisualizer::class.java]?.visualize(prop) }
3551
}
3652

3753
class ColorVisualizer : Visualizer {
54+
@Composable override fun visualize(prop: Property<*>) { Visualizer[ColorVisualizer::class.java]?.visualize(prop) }
3855
}
3956

4057
class DropdownVisualizer : Visualizer {
58+
@Composable override fun visualize(prop: Property<*>) { Visualizer[DropdownVisualizer::class.java]?.visualize(prop) }
4159
}
4260

4361
class KeybindVisualizer : Visualizer {
62+
@Composable override fun visualize(prop: Property<*>) { Visualizer[KeybindVisualizer::class.java]?.visualize(prop) }
4463
}
4564

4665
class InfoVisualizer : Visualizer {
66+
@Composable override fun visualize(prop: Property<*>) { Visualizer[InfoVisualizer::class.java]?.visualize(prop) }
4767
}
4868

4969
class DraggableListVisualizer : Visualizer {
70+
@Composable override fun visualize(prop: Property<*>) { Visualizer[DraggableListVisualizer::class.java]?.visualize(prop) }
5071
}
5172

5273
class MultiSelectDropdownVisualizer : Visualizer {
74+
@Composable override fun visualize(prop: Property<*>) { Visualizer[MultiSelectDropdownVisualizer::class.java]?.visualize(prop) }
5375
}
5476

5577
class NumberVisualizer : Visualizer {
78+
@Composable override fun visualize(prop: Property<*>) { Visualizer[NumberVisualizer::class.java]?.visualize(prop) }
5679
}
5780

5881
class RadioVisualizer : Visualizer {
82+
@Composable override fun visualize(prop: Property<*>) { Visualizer[RadioVisualizer::class.java]?.visualize(prop) }
5983
}
6084

6185
class SliderVisualizer : Visualizer {
86+
@Composable override fun visualize(prop: Property<*>) { Visualizer[SliderVisualizer::class.java]?.visualize(prop) }
6287
}
6388

6489
class SwitchVisualizer : Visualizer {
90+
@Composable override fun visualize(prop: Property<*>) { Visualizer[SwitchVisualizer::class.java]?.visualize(prop) }
6591
}
6692

6793
class CheckboxVisualizer : Visualizer {
94+
@Composable override fun visualize(prop: Property<*>) { Visualizer[CheckboxVisualizer::class.java]?.visualize(prop) }
6895
}
6996

7097
class TextVisualizer : Visualizer {
98+
@Composable override fun visualize(prop: Property<*>) { Visualizer[TextVisualizer::class.java]?.visualize(prop) }
99+
}
100+
101+
companion object {
102+
private val registry = HashMap<Class<out Visualizer>, Visualizer>()
103+
104+
/**
105+
* Register a rendering implementation for a built-in [Visualizer] token class.
106+
* Called by the UI layer on startup to wire up the built-in visualizer types.
107+
*
108+
* This will overwrite the existing visualizer if there is one already registered.
109+
*/
110+
fun register(cls: Class<out Visualizer>, impl: Visualizer) {
111+
registry[cls] = impl
112+
}
113+
114+
operator fun get(cls: Class<out Visualizer>): Visualizer? = registry[cls]
71115
}
72116
}

modules/config-impl/src/main/kotlin/org/polyfrost/oneconfig/api/config/v1/dsl/ConfigDSL.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,63 +32,63 @@ class ConfigDSL(id: String? = null, title: String? = null, description: String?
3232

3333
fun switch(default: Boolean): Prop<Boolean> {
3434
val prop = Prop(default, Boolean::class.java)
35-
prop["visualizer"] = Visualizer.SwitchVisualizer::class.java
35+
prop["visualizer"] = Visualizer.SwitchVisualizer()
3636
tree.put(prop.property)
3737
return prop
3838
}
3939

4040
fun checkbox(default: Boolean): Prop<Boolean> {
4141
val prop = Prop(default, Boolean::class.java)
42-
prop["visualizer"] = Visualizer.CheckboxVisualizer::class.java
42+
prop["visualizer"] = Visualizer.CheckboxVisualizer()
4343
tree.put(prop.property)
4444
return prop
4545
}
4646

4747
fun button(function: Runnable): RunnableProp {
4848
val prop = RunnableProp(function)
49-
prop["visualizer"] = Visualizer.ButtonVisualizer::class.java
49+
prop["visualizer"] = Visualizer.ButtonVisualizer()
5050
tree.put(prop.property)
5151
return prop
5252
}
5353

5454
fun color(default: PolyColor): ColorProp {
5555
val prop = ColorProp(default)
56-
prop["visualizer"] = Visualizer.ColorVisualizer::class.java
56+
prop["visualizer"] = Visualizer.ColorVisualizer()
5757
tree.put(prop.property)
5858
return prop
5959
}
6060

6161
fun text(default: String): TextProp {
6262
val prop = TextProp(default)
63-
prop["visualizer"] = Visualizer.TextVisualizer::class.java
63+
prop["visualizer"] = Visualizer.TextVisualizer()
6464
tree.put(prop.property)
6565
return prop
6666
}
6767

6868
fun slider(default: Float): FloatProp {
6969
val prop = FloatProp(default)
70-
prop["visualizer"] = Visualizer.SliderVisualizer::class.java
70+
prop["visualizer"] = Visualizer.SliderVisualizer()
7171
tree.put(prop.property)
7272
return prop
7373
}
7474

7575
fun slider(default: Int): IntProp {
7676
val prop = IntProp(default)
77-
prop["visualizer"] = Visualizer.SliderVisualizer::class.java
77+
prop["visualizer"] = Visualizer.SliderVisualizer()
7878
tree.put(prop.property)
7979
return prop
8080
}
8181

8282
fun number(default: Float): FloatProp {
8383
val prop = FloatProp(default)
84-
prop["visualizer"] = Visualizer.NumberVisualizer::class.java
84+
prop["visualizer"] = Visualizer.NumberVisualizer()
8585
tree.put(prop.property)
8686
return prop
8787
}
8888

8989
fun number(default: Int): IntProp {
9090
val prop = IntProp(default)
91-
prop["visualizer"] = Visualizer.NumberVisualizer::class.java
91+
prop["visualizer"] = Visualizer.NumberVisualizer()
9292
tree.put(prop.property)
9393
return prop
9494
}
@@ -102,30 +102,30 @@ class ConfigDSL(id: String? = null, title: String? = null, description: String?
102102

103103
fun dropdown(defaultIndex: Int, vararg options: String): Prop<Int> {
104104
val prop = Prop(defaultIndex, Int::class.java)
105-
prop["visualizer"] = Visualizer.DropdownVisualizer::class.java
105+
prop["visualizer"] = Visualizer.DropdownVisualizer()
106106
prop["options"] = options
107107
tree.put(prop.property)
108108
return prop
109109
}
110110

111111
inline fun <reified T : Enum<*>> dropdown(default: T): Prop<T> {
112112
val prop = Prop(default, T::class.java)
113-
prop["visualizer"] = Visualizer.DropdownVisualizer::class.java
113+
prop["visualizer"] = Visualizer.DropdownVisualizer()
114114
tree.put(prop.property)
115115
return prop
116116
}
117117

118118
fun radiobutton(defaultIndex: Int, vararg options: String): Prop<Int> {
119119
val prop = Prop(defaultIndex, Int::class.java)
120-
prop["visualizer"] = Visualizer.RadioVisualizer::class.java
120+
prop["visualizer"] = Visualizer.RadioVisualizer()
121121
prop["options"] = options
122122
tree.put(prop.property)
123123
return prop
124124
}
125125

126126
inline fun <reified T : Enum<*>> radiobutton(default: T): Prop<T> {
127127
val prop = Prop(default, T::class.java)
128-
prop["visualizer"] = Visualizer.RadioVisualizer::class.java
128+
prop["visualizer"] = Visualizer.RadioVisualizer()
129129
tree.put(prop.property)
130130
return prop
131131
}

modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/OneConfigInterface.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import org.polyfrost.oneconfig.internal.ui.shell.Lifecycle
2424
import org.polyfrost.oneconfig.internal.ui.shell.LocalNavController
2525
import org.polyfrost.oneconfig.internal.ui.shell.OCViewModelStoreOwner
2626
import org.polyfrost.oneconfig.internal.ui.shell.Shell
27+
import org.polyfrost.oneconfig.internal.ui.api.settings.BuiltinVisualizers
2728
import org.polyfrost.oneconfig.internal.ui.themes.Theme
2829
import org.polyfrost.oneconfig.internal.ui.themes.ThemeRegistry
2930
import org.polyfrost.oneconfig.internal.ui.themes.UIBranding
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.polyfrost.oneconfig.internal.ui.api.settings
2+
3+
import org.polyfrost.oneconfig.api.config.v1.Visualizer
4+
import org.polyfrost.oneconfig.internal.ui.components.settings.BooleanOption
5+
import org.polyfrost.oneconfig.internal.ui.components.settings.ButtonOption
6+
import org.polyfrost.oneconfig.internal.ui.components.settings.ColorOption
7+
import org.polyfrost.oneconfig.internal.ui.components.settings.DropdownOption
8+
import org.polyfrost.oneconfig.internal.ui.components.settings.KeybindOption
9+
import org.polyfrost.oneconfig.internal.ui.components.settings.NumberOption
10+
import org.polyfrost.oneconfig.internal.ui.components.settings.RadioButtonOption
11+
import org.polyfrost.oneconfig.internal.ui.components.settings.SliderOption
12+
import org.polyfrost.oneconfig.internal.ui.components.settings.TextOption
13+
14+
internal object BuiltinVisualizers {
15+
fun register() {
16+
Visualizer.register(Visualizer.SwitchVisualizer::class.java, Visualizer { prop ->
17+
BooleanOption(BooleanOptionData(prop, BooleanOptionData.Style.Switch))
18+
})
19+
Visualizer.register(Visualizer.CheckboxVisualizer::class.java, Visualizer { prop ->
20+
BooleanOption(BooleanOptionData(prop, BooleanOptionData.Style.Checkbox))
21+
})
22+
Visualizer.register(Visualizer.SliderVisualizer::class.java, Visualizer { prop ->
23+
SliderOption(SliderOptionData(prop))
24+
})
25+
Visualizer.register(Visualizer.NumberVisualizer::class.java, Visualizer { prop ->
26+
NumberOption(NumberOptionData(prop))
27+
})
28+
Visualizer.register(Visualizer.TextVisualizer::class.java, Visualizer { prop ->
29+
TextOption(TextOptionData(prop))
30+
})
31+
Visualizer.register(Visualizer.DropdownVisualizer::class.java, Visualizer { prop ->
32+
DropdownOption(DropdownOptionData(prop))
33+
})
34+
Visualizer.register(Visualizer.RadioVisualizer::class.java, Visualizer { prop ->
35+
RadioButtonOption(RadioButtonOptionData(prop))
36+
})
37+
Visualizer.register(Visualizer.ColorVisualizer::class.java, Visualizer { prop ->
38+
ColorOption(ColorOptionData(prop))
39+
})
40+
Visualizer.register(Visualizer.KeybindVisualizer::class.java, Visualizer { prop ->
41+
KeybindOption(KeybindOptionData(prop))
42+
})
43+
Visualizer.register(Visualizer.ButtonVisualizer::class.java, Visualizer { prop ->
44+
ButtonOption(ButtonOptionData(prop))
45+
})
46+
}
47+
}

modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/settings/OptionData.kt

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.polyfrost.oneconfig.internal.ui.api.settings
22

33
import org.polyfrost.oneconfig.api.config.v1.Property
4-
import org.polyfrost.oneconfig.api.config.v1.Visualizer
54

65
sealed class OptionData(val prop: Property<*>) {
76
val title: String get() = prop.title ?: prop.id ?: ""
@@ -64,50 +63,3 @@ class ButtonOptionData(prop: Property<*>) : OptionData(prop) {
6463

6564
class InfoOptionData(prop: Property<*>) : OptionData(prop)
6665

67-
fun optionDataFrom(prop: Property<*>): OptionData? {
68-
val vis = prop.getMetadata<Class<*>>("visualizer")
69-
if (vis != null) {
70-
return when {
71-
Visualizer.SwitchVisualizer::class.java.isAssignableFrom(vis) ->
72-
BooleanOptionData(prop, BooleanOptionData.Style.Switch)
73-
Visualizer.CheckboxVisualizer::class.java.isAssignableFrom(vis) ->
74-
BooleanOptionData(prop, BooleanOptionData.Style.Checkbox)
75-
Visualizer.SliderVisualizer::class.java.isAssignableFrom(vis) ->
76-
SliderOptionData(prop)
77-
Visualizer.NumberVisualizer::class.java.isAssignableFrom(vis) ->
78-
NumberOptionData(prop)
79-
Visualizer.TextVisualizer::class.java.isAssignableFrom(vis) ->
80-
TextOptionData(prop)
81-
Visualizer.DropdownVisualizer::class.java.isAssignableFrom(vis) ->
82-
DropdownOptionData(prop)
83-
Visualizer.RadioVisualizer::class.java.isAssignableFrom(vis) ->
84-
RadioButtonOptionData(prop)
85-
Visualizer.ColorVisualizer::class.java.isAssignableFrom(vis) ->
86-
ColorOptionData(prop)
87-
Visualizer.KeybindVisualizer::class.java.isAssignableFrom(vis) ->
88-
KeybindOptionData(prop)
89-
Visualizer.ButtonVisualizer::class.java.isAssignableFrom(vis) ->
90-
ButtonOptionData(prop)
91-
Visualizer.InfoVisualizer::class.java.isAssignableFrom(vis) ->
92-
InfoOptionData(prop)
93-
else -> null
94-
}
95-
}
96-
// Type-based fallback when no visualizer metadata is set
97-
val type = prop.type ?: return null
98-
return when {
99-
type == Boolean::class.java || type == Boolean::class.javaPrimitiveType ->
100-
BooleanOptionData(prop, BooleanOptionData.Style.Switch)
101-
type == String::class.java ->
102-
TextOptionData(prop)
103-
type.isEnum ->
104-
DropdownOptionData(prop)
105-
Number::class.java.isAssignableFrom(type) ||
106-
type == Int::class.javaPrimitiveType ||
107-
type == Float::class.javaPrimitiveType ||
108-
type == Double::class.javaPrimitiveType ||
109-
type == Long::class.javaPrimitiveType ->
110-
NumberOptionData(prop)
111-
else -> null
112-
}
113-
}

0 commit comments

Comments
 (0)