commit b9c585699c51c315e3093ffab5862f636febdcb7 Author: yumoqing Date: Mon May 18 08:17:44 2026 +0800 initial commit: bricks-mp project diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..e9ea1a5 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,28 @@ +# Bricks Multiplatform - 设计文档 + +## 架构 +JSON 描述 → BricksParser → BricksWidget 树 → BricksRenderer → Compose UI + +## 组件映射 (Phase 1) +| Bricks Widget | Compose | +|--------------|---------| +| Text | Text() | +| Title1-6 | Text() + fontSize/fontWeight | +| HBox/FHBox | Row() + Arrangement | +| VBox/FVBox | Column() + Arrangement | +| Filler/HFiller/VFiller | Spacer() + weight | +| ResponsiveBox | BoxWithConstraints() | +| KeyinText | OutlinedTextField() | +| Input | TextField() + type | +| Running | CircularProgressIndicator() | + +## 事件系统 +actiontype: urlwidget/method/script/registerfunction/event → ActionDispatcher + +## 技术栈 +Kotlin 2.1, Compose Multiplatform 1.7.3, Ktor 3.0, kotlinx.serialization, Coroutines + +## 平台支持 +- Android (minSdk 24) +- iOS (X64/Arm64/SimulatorArm64) +- Desktop (Windows/Linux/macOS) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..693448b --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1 @@ +// Top-level build file diff --git a/desktopApp/src/main/kotlin/com/bricks/Main.kt b/desktopApp/src/main/kotlin/com/bricks/Main.kt new file mode 100644 index 0000000..1e2e377 --- /dev/null +++ b/desktopApp/src/main/kotlin/com/bricks/Main.kt @@ -0,0 +1,101 @@ +package com.bricks + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.bricks.mp.core.BricksContext +import com.bricks.mp.core.BricksParser +import com.bricks.mp.core.BricksApp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Bricks MP - Desktop", + state = rememberWindowState(width = 1200.dp, height = 800.dp) + ) { + BricksDesktopApp() + } +} + +@Composable +@Preview +fun BricksDesktopApp() { + val context = remember { BricksContext() } + var jsonInput by remember { mutableStateOf(SAMPLE_JSON) } + var rootWidget by remember { mutableStateOf(null) } + + Column(modifier = Modifier.fillMaxSize()) { + // JSON 输入区 + Row(modifier = Modifier.fillMaxWidth().weight(0.3f)) { + OutlinedTextField( + value = jsonInput, + onValueChange = { jsonInput = it }, + modifier = Modifier.fillMaxSize().padding(8.dp), + label = { Text("Bricks JSON") }, + textStyle = androidx.compose.ui.text.TextStyle(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace) + ) + } + + // 解析按钮 + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Button(onClick = { + try { + rootWidget = BricksParser.parse(jsonInput) + } catch (e: Exception) { + println("Parse error: ${e.message}") + } + }) { + Text("Parse & Render") + } + } + + // 渲染区 + Surface(modifier = Modifier.fillMaxWidth().weight(0.6f)) { + if (rootWidget != null) { + BricksApp(rootWidget!!) + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Enter Bricks JSON and click Parse") + } + } + } + } +} + +val SAMPLE_JSON = """ +{ + "widgettype": "VBox", + "options": { + "text": "Hello Bricks MP" + }, + "subwidgets": [ + { + "widgettype": "Title1", + "options": { + "text": "Welcome to Bricks" + } + }, + { + "widgettype": "Text", + "options": { + "text": "Cross-platform JSON-driven UI" + } + }, + { + "widgettype": "KeyinText", + "options": { + "placeholder": "Type here...", + "label": "Input" + } + } + ] +} +""".trimIndent() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..c3458bb --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,24 @@ +[versions] +compose = "1.7.3" +compose-plugin = "1.7.3" +kotlin = "2.1.0" +ktor = "3.0.3" +serialization = "1.7.3" +coroutines = "1.9.0" +coil = "3.0.4" + +[libraries] +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } + +[plugins] +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..a1e927f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,7 @@ +rootProject.name = "bricks-mp" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +pluginManagement { + repositories { google(); mavenCentral(); gradlePluginPortal() } +} +plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } +include(":shared", ":desktopApp") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..f279c66 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,61 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.jetbrains.compose) + alias(libs.plugins.serialization) +} + +kotlin { + androidTarget() + jvm("desktop") + listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "BricksShared" + isStatic = true + } + } + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") } + + sourceSets { + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(libs.kotlinx.serialization.json) + implementation(libs.ktor.client.core) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.coil.compose) + implementation(libs.coil.network) + } + desktopMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.ktor.client.okhttp) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } + } +} + +android { + namespace = "com.bricks.mp" + compileSdk = 35 + defaultConfig { minSdk = 24 } +} + +compose.desktop { + application { + mainClass = "com.bricks.MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "bricks-mp" + packageVersion = "0.1.0" + } + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/actions/ActionDispatcher.kt b/shared/src/commonMain/kotlin/com/bricks/mp/actions/ActionDispatcher.kt new file mode 100644 index 0000000..cd6596a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/actions/ActionDispatcher.kt @@ -0,0 +1,78 @@ +package com.bricks.mp.actions + +import com.bricks.mp.core.BricksBind +import com.bricks.mp.core.BricksContext +import com.bricks.mp.core.BricksHttp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.serialization.json.* + +/** + * 事件分发器 - 处理 actiontype: urlwidget/method/script/registerfunction/event + */ +class ActionDispatcher( + private val context: BricksContext, + private val http: BricksHttp, + private val scope: CoroutineScope +) { + + private val registeredFunctions = mutableMapOf Unit>() + + /** + * 注册回调函数 (registerfunction) + */ + fun registerFunction(name: String, func: () -> Unit) { + registeredFunctions[name] = func + } + + /** + * 分发事件 + */ + fun dispatch(bind: BricksBind) { + when (bind.actiontype) { + "urlwidget" -> handleUrlWidget(bind) + "method" -> handleMethod(bind) + "script" -> handleScript(bind) + "registerfunction" -> handleRegisterFunction(bind) + "event" -> handleEvent(bind) + else -> println("[Bricks] Unknown actiontype: ${bind.actiontype}") + } + } + + private fun handleUrlWidget(bind: BricksBind) { + scope.launch { + val url = bind.url ?: return@launch + val fullUrl = context.entireUrl(url) + try { + val result = http.getJson(fullUrl) + // 加载新的 widget 并更新 UI + println("[Bricks] urlwidget loaded: $fullUrl") + } catch (e: Exception) { + println("[Bricks] urlwidget error: ${e.message}") + } + } + } + + private fun handleMethod(bind: BricksBind) { + // 调用客户端方法 + println("[Bricks] method called: ${bind.methodname}") + } + + private fun handleScript(bind: BricksBind) { + scope.launch { + // 服务端脚本调用 + val script = bind.script ?: return@launch + println("[Bricks] script: $script") + } + } + + private fun handleRegisterFunction(bind: BricksBind) { + val name = bind.target ?: return + registeredFunctions[name]?.invoke() + } + + private fun handleEvent(bind: BricksBind) { + println("[Bricks] event: ${bind.event} -> ${bind.actiontype}") + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksContext.kt b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksContext.kt new file mode 100644 index 0000000..c4f6875 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksContext.kt @@ -0,0 +1,42 @@ +package com.bricks.mp.core + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * 全局上下文 - 管理应用状态、session、变量 + */ +class BricksContext { + + private val _appState = MutableStateFlow>(emptyMap()) + val appState: StateFlow> = _appState.asStateFlow() + + private val _sessionData = MutableStateFlow>(emptyMap()) + val sessionData: StateFlow> = _sessionData.asStateFlow() + + var baseUrl: String = "" + var authToken: String = "" + + fun setAppState(key: String, value: Any) { + val current = _appState.value.toMutableMap() + current[key] = value + _appState.value = current + } + + fun getAppState(key: String): Any? = _appState.value[key] + + fun setSessionData(key: String, value: Any) { + val current = _sessionData.value.toMutableMap() + current[key] = value + _sessionData.value = current + } + + /** + * 解析 entire_url - 拼接 baseUrl + */ + fun entireUrl(path: String): String { + val cleanPath = path.trimStart('/') + return if (baseUrl.endsWith("/")) "$baseUrl$cleanPath" else "$baseUrl/$cleanPath" + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksHttp.kt b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksHttp.kt new file mode 100644 index 0000000..9103c36 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksHttp.kt @@ -0,0 +1,47 @@ +package com.bricks.mp.core + +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.serialization.json.* + +/** + * HTTP 客户端 - 对应 JS 版 HttpJson/HttpText + */ +class BricksHttp(private val context: BricksContext) { + + private val client = HttpClient() + + suspend fun getJson(url: String, params: Map = emptyMap()): JsonObject { + val response = client.get(url) { + url { params.forEach { (k, v) -> parameters.append(k, v) } } + if (context.authToken.isNotEmpty()) { + header(HttpHeaders.Authorization, "Bearer ${context.authToken}") + } + } + return Json.parseToJsonElement(response.bodyAsText()).jsonObject + } + + suspend fun postJson(url: String, body: JsonObject): JsonObject { + val response = client.post(url) { + contentType(ContentType.Application.Json) + setBody(body.toString()) + if (context.authToken.isNotEmpty()) { + header(HttpHeaders.Authorization, "Bearer ${context.authToken}") + } + } + return Json.parseToJsonElement(response.bodyAsText()).jsonObject + } + + suspend fun getText(url: String, params: Map = emptyMap()): String { + val response = client.get(url) { + url { params.forEach { (k, v) -> parameters.append(k, v) } } + } + return response.bodyAsText() + } + + fun close() { + client.close() + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksParser.kt b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksParser.kt new file mode 100644 index 0000000..5c43c12 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksParser.kt @@ -0,0 +1,56 @@ +package com.bricks.mp.core + +import kotlinx.serialization.json.* + +/** + * JSON 解析器 - 将 bricks JSON 解析为 BricksWidget 树 + */ +object BricksParser { + + /** + * 从 JSON 字符串解析 Widget 树 + */ + fun parse(jsonString: String): BricksWidget { + val json = Json { ignoreUnknownKeys = true; coerceInputValues = true } + val element = json.parseToJsonElement(jsonString) + return parseElement(element) + } + + /** + * 从 JsonElement 递归解析 + */ + private fun parseElement(element: JsonElement): BricksWidget { + val obj = element.jsonObject + val widgettype = obj["widgettype"]?.jsonPrimitive?.content ?: "Text" + + val options = obj["options"]?.jsonObject?.mapValues { it.value } ?: emptyMap() + + val subwidgets = obj["subwidgets"]?.jsonArray?.map { parseElement(it) } ?: emptyList() + + val binds = obj["binds"]?.jsonArray?.map { bindEl -> + val b = bindEl.jsonObject + BricksBind( + event = b["event"]?.jsonPrimitive?.content, + actiontype = b["actiontype"]?.jsonPrimitive?.content, + target = b["target"]?.jsonPrimitive?.content, + methodname = b["methodname"]?.jsonPrimitive?.content, + script = b["script"]?.jsonPrimitive?.content, + url = b["url"]?.jsonPrimitive?.content, + data = (b["data"]?.jsonObject?.mapValues { it.value } ?: emptyMap()) + ) + } ?: emptyList() + + return BricksWidget(widgettype, options, subwidgets, binds) + } + + /** + * 模板变量替换 - 将 {{var}} 替换为实际值 + */ + fun resolveTemplate(template: String, data: Map): String { + var result = template + for ((key, value) in data) { + result = result.replace("{{$key}}", value) + } + return result + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksRenderer.kt b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksRenderer.kt new file mode 100644 index 0000000..a37c061 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksRenderer.kt @@ -0,0 +1,68 @@ +package com.bricks.mp.core + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import com.bricks.mp.core.BricksWidget +import com.bricks.mp.widgets.* + +/** + * 递归渲染引擎 - 将 BricksWidget 树渲染为 Compose UI + */ +@Composable +fun RenderWidget(widget: BricksWidget) { + when (widget.widgettype) { + // 文本 + "Text" -> RenderTextWidget(widget) + "Title1", "Title2", "Title3", "Title4", "Title5", "Title6" -> RenderTitleWidget(widget) + + // 布局 + "HBox", "FHBox", "VBox", "FVBox", "Filler", "HFiller", "VFiller", "ResponsiveBox" -> RenderLayoutWidget(widget) + + // 输入 + "KeyinText" -> RenderKeyinTextWidget(widget) + "Input" -> RenderInputWidget(widget) + "Tooltip" -> RenderTooltipWidget(widget) + + // TODO: Phase 2 组件 + "Image", "Icon", "StatedIcon", "BlankIcon" -> RenderPlaceholder(widget, "Image/Icon") + "Menu", "Popup", "PopupWindow", "Modal", "ModalForm" -> RenderPlaceholder(widget, "Menu/Dialog") + "VScrollPanel", "HScrollPanel" -> RenderPlaceholder(widget, "Scroll") + "Splitter" -> RenderPlaceholder(widget, "Splitter") + "Running" -> RenderRunningWidget(widget) + + // TODO: Phase 3 组件 + "Html", "MarkdownViewer", "LlmOut" -> RenderPlaceholder(widget, widget.widgettype) + + // 默认: 渲染子组件 + else -> { + widget.subwidgets.forEach { child -> RenderWidget(child) } + } + } +} + +@Composable +private fun RenderPlaceholder(widget: BricksWidget, name: String) { + androidx.compose.material3.Text( + text = "[${widget.widgettype}: $name - TODO]", + modifier = Modifier.padding(8.dp) + ) + widget.subwidgets.forEach { child -> RenderWidget(child) } +} + +@Composable +fun RenderRunningWidget(widget: BricksWidget) { + androidx.compose.material3.CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) +} + +/** + * 渲染整个 Widget 树 + */ +@Composable +fun BricksApp(rootWidget: BricksWidget) { + Column(modifier = Modifier.fillMaxSize()) { + RenderWidget(rootWidget) + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksWidget.kt b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksWidget.kt new file mode 100644 index 0000000..4322912 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/core/BricksWidget.kt @@ -0,0 +1,51 @@ +package com.bricks.mp.core + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* + +/** + * Bricks Widget 数据模型 - 与 JS 版 bricks 完全一致的 JSON 结构 + */ +@Serializable +data class BricksWidget( + val widgettype: String, + val options: Map = emptyMap(), + val subwidgets: List = emptyList(), + val binds: List = emptyList() +) + +@Serializable +data class BricksBind( + val event: String? = null, + val actiontype: String? = null, + val target: String? = null, + val data: Map = emptyMap(), + val methodname: String? = null, + val script: String? = null, + val url: String? = null +) + +/** + * 解析 options 中的常用字段 + */ +object WidgetOptions { + fun getString(options: Map, key: String, default: String = ""): String { + val el = options[key] + return if (el is JsonPrimitive) el.contentOrNull ?: default else default + } + + fun getInt(options: Map, key: String, default: Int = 0): Int { + val el = options[key] + return if (el is JsonPrimitive) el.intOrNull ?: default else default + } + + fun getBoolean(options: Map, key: String, default: Boolean = false): Boolean { + val el = options[key] + return if (el is JsonPrimitive) el.booleanOrNull ?: default else default + } + + fun getList(options: Map, key: String): List { + val el = options[key] + return if (el is JsonArray) el.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } else emptyList() + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/utils/BricksExtensions.kt b/shared/src/commonMain/kotlin/com/bricks/mp/utils/BricksExtensions.kt new file mode 100644 index 0000000..ba6550f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/utils/BricksExtensions.kt @@ -0,0 +1,29 @@ +package com.bricks.mp.utils + +/** + * 工具函数 - 对应 JS 版 bricks 的 extend/obj_fmtstr 等 + */ + +/** + * 对象深拷贝合并 (对应 bricks.extend) + */ +fun Map.extend(other: Map): Map = this + other + +/** + * 模板字符串替换 (对应 bricks.obj_fmtstr) + * 支持 ${var} 和 {{var}} 两种格式 + */ +fun String.fmt(vars: Map): String { + var result = this + for ((key, value) in vars) { + result = result.replace("\${{$key}}", value?.toString() ?: "") + .replace("$${key}$", value?.toString() ?: "") + } + return result +} + +/** + * 获取 widget ID (从 options 中提取) + */ +fun getWidgetId(options: Map): String = + (options["id"] as? String) ?: "" diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/widgets/InputWidgets.kt b/shared/src/commonMain/kotlin/com/bricks/mp/widgets/InputWidgets.kt new file mode 100644 index 0000000..b74f33d --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/widgets/InputWidgets.kt @@ -0,0 +1,69 @@ +package com.bricks.mp.widgets + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.bricks.mp.core.BricksWidget +import com.bricks.mp.core.WidgetOptions + +/** + * 输入组件 + * KeyinText -> TextField, Input -> 动态类型 + */ +@Composable +fun RenderKeyinTextWidget(widget: BricksWidget) { + var text by remember { mutableStateOf(WidgetOptions.getString(widget.options, "value", "")) } + val placeholder = WidgetOptions.getString(widget.options, "placeholder", "") + val label = WidgetOptions.getString(widget.options, "label", "") + + OutlinedTextField( + value = text, + onValueChange = { text = it }, + placeholder = { if (placeholder.isNotEmpty()) Text(placeholder) }, + label = { if (label.isNotEmpty()) Text(label) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp) + ) +} + +@Composable +fun RenderInputWidget(widget: BricksWidget) { + var text by remember { mutableStateOf(WidgetOptions.getString(widget.options, "value", "")) } + val inputType = WidgetOptions.getString(widget.options, "type", "text") + + when (inputType) { + "password" -> { + var visible by remember { mutableStateOf(false) } + OutlinedTextField( + value = text, + onValueChange = { text = it }, + visualTransformation = if (!visible) androidx.compose.ui.text.input.PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + "number" -> { + OutlinedTextField( + value = text, + onValueChange = { if (it.all { c -> c.isDigit() || c == '.' }) text = it }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + else -> { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } +} + +@Composable +fun RenderTooltipWidget(widget: BricksWidget) { + // Simplified tooltip - in real app would use TooltipBox + val text = WidgetOptions.getString(widget.options, "text", "") + Text(text, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/widgets/LayoutWidgets.kt b/shared/src/commonMain/kotlin/com/bricks/mp/widgets/LayoutWidgets.kt new file mode 100644 index 0000000..0bc1eba --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/widgets/LayoutWidgets.kt @@ -0,0 +1,72 @@ +package com.bricks.mp.widgets + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.bricks.mp.core.BricksWidget +import com.bricks.mp.core.WidgetOptions + +/** + * 布局组件 + * HBox -> Row, VBox -> Column, Filler -> Spacer + */ +@Composable +fun RenderLayoutWidget(widget: BricksWidget) { + when (widget.widgettype) { + "HBox", "FHBox" -> { + val align = when (WidgetOptions.getString(widget.options, "align", "start")) { + "center" -> Alignment.CenterVertically + "end" -> Alignment.Bottom + else -> Alignment.Top + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = WidgetOptions.getInt(widget.options, "padding", 0).dp), + verticalAlignment = align, + horizontalArrangement = when (widget.widgettype) { + "FHBox" -> Arrangement.SpaceBetween + else -> Arrangement.Start + } + ) { + widget.subwidgets.forEach { child -> RenderWidget(child) } + } + } + "VBox", "FVBox" -> { + val align = when (WidgetOptions.getString(widget.options, "align", "start")) { + "center" -> Alignment.CenterHorizontally + "end" -> Alignment.End + else -> Alignment.Start + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = WidgetOptions.getInt(widget.options, "padding", 0).dp), + horizontalAlignment = align, + verticalArrangement = when (widget.widgettype) { + "FVBox" -> Arrangement.SpaceBetween + else -> Arrangement.Top + } + ) { + widget.subwidgets.forEach { child -> RenderWidget(child) } + } + } + "Filler", "HFiller" -> { + Spacer(modifier = Modifier.weight(1f)) + } + "VFiller" -> { + Spacer(modifier = Modifier.height(WidgetOptions.getInt(widget.options, "height", 16).dp)) + } + "ResponsiveBox" -> { + BoxWithConstraints { + if (maxWidth > 600.dp) { + Row { widget.subwidgets.forEach { RenderWidget(it) } } + } else { + Column { widget.subwidgets.forEach { RenderWidget(it) } } + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/bricks/mp/widgets/TextWidgets.kt b/shared/src/commonMain/kotlin/com/bricks/mp/widgets/TextWidgets.kt new file mode 100644 index 0000000..00449ff --- /dev/null +++ b/shared/src/commonMain/kotlin/com/bricks/mp/widgets/TextWidgets.kt @@ -0,0 +1,54 @@ +package com.bricks.mp.widgets + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bricks.mp.core.BricksWidget +import com.bricks.mp.core.WidgetOptions + +/** + * 文本组件渲染 + * 映射: Text -> Text, Title1-6 -> Text with different sizes + */ +@Composable +fun RenderTextWidget(widget: BricksWidget) { + val text = WidgetOptions.getString(widget.options, "text", "") + val i18n = WidgetOptions.getBoolean(widget.options, "i18n", false) + val otext = WidgetOptions.getString(widget.options, "otext", text) + + val displayText = if (i18n && otext.isNotEmpty()) otext else text + + Text( + text = displayText, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.bodyLarge + ) +} + +@Composable +fun RenderTitleWidget(widget: BricksWidget) { + val text = WidgetOptions.getString(widget.options, "text", "") + val (fontSize, fontWeight) = when (widget.widgettype) { + "Title1" -> 32.sp to FontWeight.Bold + "Title2" -> 28.sp to FontWeight.Bold + "Title3" -> 24.sp to FontWeight.SemiBold + "Title4" -> 20.sp to FontWeight.SemiBold + "Title5" -> 18.sp to FontWeight.Medium + "Title6" -> 16.sp to FontWeight.Medium + else -> 20.sp to FontWeight.SemiBold + } + + Text( + text = text, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp), + fontSize = fontSize, + fontWeight = fontWeight, + style = MaterialTheme.typography.headlineLarge.copy(fontSize = fontSize, fontWeight = fontWeight) + ) +}