From dadb963109fabd60d319139fdac239c80b243716 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 11:11:14 +0000 Subject: [PATCH 1/3] Switch the Android host to KotlinHost and the real LUI Kotlin backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump the lui pin to 264aa9f67da0d0d3e413f9dfe970785e7286f23c (logseq/lui#135; re-pin to the merge SHA once that PR lands). - Rename every FlutterHost reference in shared/ to KotlinHost and android/flutter -> android/kotlin in extension profiles, fingerprints, and all flutter_* identifiers/comments; host code 4 now maps to KotlinHost (matching lui's components_bridge.ml table: 1 Web, 2 SwiftUI, 4 Kotlin, 6 GPUI), so iOS passes host code 2 and Android 4. - shared/: bare 'reactive' calls rewritten to Signal.map — the new lui.ppx expands bare reactive to dyn in element position while ~p:(reactive ..) labeled forms are kept for prop signals. - Delete the vendored android/lui module; :lui now resolves to ../lui/platform/android/lui via module include, the same consumption model as lui's examples/components/android. - LuiDispatch handles the widened LuiEvent sealed class (pointer, context-menu, load events have no chat FFI export and are dropped, matching upstream dispatchToBridge). - Disable :lui test tasks in this build; the backend's paparazzi tests need its standalone AGP 8.6.1 build. Verification: dune build @shared/native/runtest (719 tests), ./gradlew assembleDebug assembleProfile test all green against the real backend; profile APK runs on the chat_test emulator with a fresh x86_64 liblogseq_chat_core.so and renders the auth screen. --- android/app/build.gradle.kts | 17 +- .../chat/runtime/LogseqChatNativeBridge.kt | 9 +- .../com/logseq/chat/runtime/LuiDispatch.kt | 14 + .../chat/ui/extensions/ChatExtensions.kt | 8 +- android/build.gradle.kts | 8 + android/lui/build.gradle.kts | 47 - .../lui/src/main/kotlin/dev/lui/LuiBackend.kt | 135 --- .../lui/src/main/kotlin/dev/lui/LuiEvent.kt | 32 - .../src/main/kotlin/dev/lui/LuiExtension.kt | 92 -- .../lui/src/main/kotlin/dev/lui/LuiIcons.kt | 107 --- .../lui/src/main/kotlin/dev/lui/LuiNode.kt | 32 - .../lui/src/main/kotlin/dev/lui/LuiProps.kt | 174 ---- .../src/main/kotlin/dev/lui/LuiRenderers.kt | 819 ------------------ .../lui/src/main/kotlin/dev/lui/LuiTheme.kt | 150 ---- android/settings.gradle.kts | 28 +- .../LGChatNavigationExtension.swift | 4 +- .../LGChatOutlinerEditorExtension.swift | 2 +- .../LogseqChat/LGChatRichBlockExtension.swift | 2 +- apple/Sources/LogseqChat/LGChatRuntime.swift | 2 +- .../LogseqChatTests/LGChatRendererTests.swift | 2 +- logseq_chat.opam | 2 +- shared/src/logseq_chat/core/host_update.mli | 2 +- shared/src/logseq_chat/native_bridge.ml | 8 +- shared/src/logseq_chat/view.ml | 8 +- shared/src/logseq_chat/view_base.ml | 14 +- shared/src/logseq_chat/view_base.mli | 4 +- shared/src/logseq_chat/view_composer.ml | 20 +- shared/src/logseq_chat/view_flashcards.ml | 4 +- shared/src/logseq_chat/view_graphs.ml | 36 +- shared/src/logseq_chat/view_graphs.mli | 6 +- shared/src/logseq_chat/view_outliner.ml | 126 +-- shared/src/logseq_chat/view_screens.ml | 60 +- shared/src/logseq_chat/view_settings.ml | 46 +- shared/src/logseq_chat/view_sidebar.ml | 16 +- shared/test/logseq_chat/app_test.ml | 296 +++---- .../test/logseq_chat/lui_projection_test.ml | 6 +- 36 files changed, 381 insertions(+), 1957 deletions(-) delete mode 100644 android/lui/build.gradle.kts delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiBackend.kt delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiEvent.kt delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiExtension.kt delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiIcons.kt delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiNode.kt delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiProps.kt delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiRenderers.kt delete mode 100644 android/lui/src/main/kotlin/dev/lui/LuiTheme.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a23c6f1b..68f6d318 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -117,21 +117,10 @@ tasks.named("preBuild") { -// When the vendored :lui module is used (no ../lui/platform/android -// composite build), resolve the dev.lui:lui coordinate to it. -if (rootProject.findProject(":lui") != null) { - configurations.all { - resolutionStrategy.dependencySubstitution { - substitute(module("dev.lui:lui")).using(project(":lui")) - } - } -} - dependencies { - // Resolved to ../lui/platform/android when the Kotlin LUI backend - // checkout is present (composite build substitution), otherwise to - // the vendored :lui module — see settings.gradle.kts. - implementation("dev.lui:lui") + // The real Kotlin LUI backend, included from + // ../lui/platform/android/lui — see settings.gradle.kts. + implementation(project(":lui")) implementation(platform("androidx.compose:compose-bom:2025.09.00")) implementation("androidx.compose.ui:ui") diff --git a/android/app/src/main/kotlin/com/logseq/chat/runtime/LogseqChatNativeBridge.kt b/android/app/src/main/kotlin/com/logseq/chat/runtime/LogseqChatNativeBridge.kt index 5119e4e8..4fd406d6 100644 --- a/android/app/src/main/kotlin/com/logseq/chat/runtime/LogseqChatNativeBridge.kt +++ b/android/app/src/main/kotlin/com/logseq/chat/runtime/LogseqChatNativeBridge.kt @@ -34,13 +34,12 @@ internal class LogseqChatNativeBridge( if (patch.isNotEmpty()) onPatch(patch) } - // platform 3 = AndroidOS, host 3 = the Android host slot the core - // renders for (the legacy Android host kind in - // shared/src/logseq_chat/native_bridge.ml; the Kotlin backend takes - // over that host profile). + // platform 3 = AndroidOS, host 4 = KotlinHost (see the host-code table + // in shared/src/logseq_chat/native_bridge.ml, matching lui's + // components_bridge.ml). fun initialize( platformCode: Int = 3, - hostCode: Int = 3, + hostCode: Int = 4, authenticationCode: Int = 1, ): Long { apply(decode(NativeCore.luiInitialize(platformCode, hostCode, authenticationCode))) diff --git a/android/app/src/main/kotlin/com/logseq/chat/runtime/LuiDispatch.kt b/android/app/src/main/kotlin/com/logseq/chat/runtime/LuiDispatch.kt index b2701564..33ae4af7 100644 --- a/android/app/src/main/kotlin/com/logseq/chat/runtime/LuiDispatch.kt +++ b/android/app/src/main/kotlin/com/logseq/chat/runtime/LuiDispatch.kt @@ -28,6 +28,20 @@ internal fun dispatchLuiEvent(event: LuiEvent, native: LogseqChatNativeBridge) { extensionText(event.values), extensionValue(event.values), ) + // The chat core ABI exports no pointer-level, context-menu, + // press-modifier, or load entries (see + // shared/native/logseq_chat_core_ffi.h), so these events are + // dropped here — matching the upstream dispatchToBridge's + // treatment of events with no mobile export. + is LuiEvent.PressModifiers, + is LuiEvent.PressDetail, + is LuiEvent.PointerDown, + is LuiEvent.PointerUp, + is LuiEvent.PointerEnter, + is LuiEvent.PointerLeave, + is LuiEvent.ContextMenuPress, + is LuiEvent.Load, + -> Unit } } diff --git a/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt b/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt index 592549de..5efc898b 100644 --- a/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt +++ b/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt @@ -32,7 +32,7 @@ import dev.lui.LuiExtensionRegistry // extension hosts. Fingerprints are generated by the OCaml core and must // match byte-for-byte. private const val EDITOR_FINGERPRINT = - "lui-extension-v1|15:outliner-editor|profiles:android/flutter," + + "lui-extension-v1|15:outliner-editor|profiles:android/kotlin," + "ios/swiftui|standard-children:0|children:|" + "properties:18:caret-utf16-offset:int:required:none,5:title:string:" + "required:none,8:block-id:string:required:none|events:11:text-change[" + @@ -41,7 +41,7 @@ private const val EDITOR_FINGERPRINT = "required,5:title:string:required],9:backspace[16:selection-length:int:" + "required,5:title:string:required]" private const val RICH_FINGERPRINT = - "lui-extension-v1|22:outliner-block-content|profiles:android/flutter," + + "lui-extension-v1|22:outliner-block-content|profiles:android/kotlin," + "ios/swiftui|standard-children:0|children:|" + "properties:10:asset-type:string:required:none,10:local-path:string:" + "required:none,11:markup-json:string:required:none,12:is-completed:bool:" + @@ -51,14 +51,14 @@ private const val RICH_FINGERPRINT = "9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[" + "4:uuid:string:required]" private const val NAVIGATION_FINGERPRINT = - "lui-extension-v1|23:native-navigation-stack|profiles:android/flutter," + + "lui-extension-v1|23:native-navigation-stack|profiles:android/kotlin," + "ios/swiftui|standard-children:1|children:|" + "properties:26:composer-dismissal-enabled:bool:required:none," + "28:bottom-occupies-layout-space:bool:required:none,5:depth:int:required:none," + "5:title:string:required:none|events:16:dismiss-composer[],4:back[" + "5:count:int:required]" private const val SEARCH_FINGERPRINT = - "lui-extension-v1|26:native-search-presentation|profiles:android/flutter," + + "lui-extension-v1|26:native-search-presentation|profiles:android/kotlin," + "ios/swiftui|standard-children:1|children:|" + "properties:5:depth:int:required:none,5:query:string:required:none," + "5:title:string:required:none,9:presented:bool:required:none|events:" + diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 13a3403a..25afaeed 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -3,3 +3,11 @@ plugins { id("com.android.library") version "9.1.0" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.4.0" apply false } + +// :lui is the real LUI Kotlin backend included from ../lui/platform/android. +// Its own test suite (paparazzi screenshot tests) needs the standalone +// platform/android build — disable it here so ./gradlew test only runs +// the app's tests. +project(":lui").tasks.withType().configureEach { + enabled = false +} diff --git a/android/lui/build.gradle.kts b/android/lui/build.gradle.kts deleted file mode 100644 index 0e3332d6..00000000 --- a/android/lui/build.gradle.kts +++ /dev/null @@ -1,47 +0,0 @@ -plugins { - id("com.android.library") - - id("org.jetbrains.kotlin.plugin.compose") -} - -// Matches the dev.lui:lui coordinate the real platform/android backend -// publishes so Gradle dependency substitution can swap this module for -// the composite checkout unchanged. -group = "dev.lui" -version = "1.0.0" - -android { - namespace = "dev.lui" - compileSdk = 36 - - defaultConfig { - minSdk = 26 - } - - buildFeatures { - compose = true - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlin { - compilerOptions { - jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 - } - } -} - -dependencies { - implementation(platform("androidx.compose:compose-bom:2025.09.00")) - implementation("androidx.compose.ui:ui") - implementation("androidx.compose.foundation:foundation") - implementation("androidx.compose.foundation:foundation-layout") - implementation("androidx.compose.material3:material3") - implementation("androidx.compose.material:material-icons-extended") - implementation("androidx.compose.runtime:runtime") - implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.lifecycle:lifecycle-runtime-compose") -} diff --git a/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt b/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt deleted file mode 100644 index 5772d143..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiBackend.kt +++ /dev/null @@ -1,135 +0,0 @@ -package dev.lui - -import android.util.Log -import androidx.compose.runtime.Composable -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject - -// Retained node store + patch-batch applier for the LUI wire protocol -// ({"generation": N, "ops": [...]}), plus the Compose entry point that -// renders it. -// -// Op vocabulary (shared/src ... lui_wire.ml): -// create-node {id,kind} create-extension {id,identifier,fingerprint} -// drop-node {id} set-prop / remove-prop {id,property[,value]} -// set-extension-prop / remove-extension-prop -// insert-child {parent,child,index} -// remove-child {parent,child} -// move-child {parent,child,index} -class LuiBackend( - val onEvent: (LuiEvent) -> Unit = {}, - val extensions: LuiExtensionRegistry = LuiExtensionRegistry(), - val icons: LuiIconResolver = LuiIconResolver.DEFAULT, -) { - private val nodes = LinkedHashMap() - - var generation: Long = 0 - private set - - fun node(id: Long): LuiNode? = nodes[id] - - fun childrenOf(id: Long): List = - nodes[id]?.children?.mapNotNull(nodes::get) ?: emptyList() - - fun clear() { - nodes.clear() - generation = 0 - } - - // Applies a patch batch. Returns true when anything changed; a - // malformed batch is dropped and reported (the core sends "" on - // error — the caller filters that out before we get here). - fun applyJson(raw: String?): Boolean { - if (raw.isNullOrEmpty()) return false - val batch = try { - JSONObject(raw) - } catch (error: JSONException) { - Log.w(TAG, "ignoring undecodable patch batch", error) - return false - } - val ops = batch.optJSONArray("ops") ?: return false - batch.optLong("generation").takeIf { it > 0 }?.let { generation = it } - var changed = false - for (index in 0 until ops.length()) { - val op = ops.optJSONObject(index) ?: continue - changed = applyOp(op) || changed - } - return changed - } - - private fun applyOp(op: JSONObject): Boolean = when (op.optString("op")) { - "create-node" -> { - val id = op.getLong("id") - nodes[id] = LuiNode(id, op.getString("kind")) - true - } - "create-extension" -> { - val id = op.getLong("id") - LuiNode(id, "extension").also { - it.identifier = op.getString("identifier") - it.fingerprint = op.optString("fingerprint") - nodes[id] = it - } - true - } - "drop-node" -> dropSubtree(op.getLong("id")) != null - "set-prop" -> nodes[op.getLong("id")]?.let { - it.properties[op.getString("property")] = op.opt("value") - true - } ?: false - "remove-prop" -> nodes[op.getLong("id")]?.let { - it.properties.remove(op.getString("property")) - true - } ?: false - "set-extension-prop" -> nodes[op.getLong("id")]?.let { - it.properties[op.getString("property")] = op.opt("value") - true - } ?: false - "remove-extension-prop" -> nodes[op.getLong("id")]?.let { - it.properties.remove(op.getString("property")) - true - } ?: false - "insert-child" -> nodes[op.getLong("parent")]?.let { parent -> - val child = op.getLong("child") - val index = op.getInt("index").coerceIn(0, parent.children.size) - parent.children.remove(child) - parent.children.add(index, child) - true - } ?: false - "remove-child" -> nodes[op.getLong("parent")]?.let { - it.children.remove(op.getLong("child")) - } ?: false - "move-child" -> nodes[op.getLong("parent")]?.let { parent -> - val child = op.getLong("child") - parent.children.remove(child) - val index = op.getInt("index").coerceIn(0, parent.children.size) - parent.children.add(index, child) - true - } ?: false - else -> { - Log.w(TAG, "unknown patch op: ${op.optString("op")}") - false - } - } - - private fun dropSubtree(id: Long): LuiNode? { - val node = nodes.remove(id) ?: return null - for (child in node.children.toList()) dropSubtree(child) - return node - } - - @Composable - fun Content(rootId: Long) { - LuiRoot(backend = this, rootId = rootId) - } - - internal fun dispatch(event: LuiEvent) = onEvent(event) - - companion object { - private const val TAG = "LuiBackend" - } -} - -internal fun JSONArray.stringList(): List = - (0 until length()).mapNotNull { optString(it).takeIf(String::isNotEmpty) } diff --git a/android/lui/src/main/kotlin/dev/lui/LuiEvent.kt b/android/lui/src/main/kotlin/dev/lui/LuiEvent.kt deleted file mode 100644 index 24bd1868..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiEvent.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.lui - -// User/host events produced by rendered nodes, dispatched back into the -// shared core. Mirrors the event surface of the other LUI backends -// (platform/android, platform/apple). -sealed class LuiEvent { - abstract val node: Long - - data class Appear(override val node: Long) : LuiEvent() - data class Press(override val node: Long) : LuiEvent() - data class LongPress(override val node: Long) : LuiEvent() - data class DoublePress(override val node: Long) : LuiEvent() - data class Change(override val node: Long) : LuiEvent() - data class Submit(override val node: Long) : LuiEvent() - data class Dismiss(override val node: Long) : LuiEvent() - data class TextChanged(override val node: Long, val text: String) : LuiEvent() - data class ToggleChanged(override val node: Long, val checked: Boolean) : LuiEvent() - data class ValueChanged(override val node: Long, val value: Double) : LuiEvent() - data class Picked(override val node: Long, val payload: String) : LuiEvent() - data class VisibleRange(override val node: Long, val first: Long, val last: Long) : LuiEvent() - data class ScrollCompleted(override val node: Long, val token: Long, val outcome: String) : LuiEvent() - - // Extension host event: `values` mirrors the identifier/name-keyed - // value map the host collects; the app layer reduces it to the - // (text, value) pair the FFI boundary expects. - data class Extension( - override val node: Long, - val identifier: String, - val name: String, - val values: Map, - ) : LuiEvent() -} diff --git a/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt b/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt deleted file mode 100644 index 04594df8..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiExtension.kt +++ /dev/null @@ -1,92 +0,0 @@ -package dev.lui - -import androidx.compose.runtime.Composable - -// Extension host contract: the core creates `create-extension` nodes -// carrying an `identifier` (e.g. "outliner-editor") and a `fingerprint` -// for drift detection; the app registers composable builders per -// identifier. Properties arrive via set-extension-prop; host code emits -// named events with a values map the app reduces to the FFI (text, -// value) pair. -class LuiExtensionContext internal constructor( - val node: LuiNode, - private val backend: LuiBackend, -) { - val id: Long get() = node.id - val identifier: String get() = node.identifier.orEmpty() - - fun property(name: String): T? { - @Suppress("UNCHECKED_CAST") - return node.properties[name] as? T - } - - fun string(name: String): String? = node.string(name) - fun int(name: String): Int? = node.int(name) - fun double(name: String): Double? = node.double(name) - fun flag(name: String): Boolean = node.flag(name) - - fun emit(name: String, values: Map) { - backend.dispatch(LuiEvent.Extension(node.id, identifier, name, values)) - } - - fun emit(name: String, vararg values: Pair) = - emit(name, mapOf(*values)) - - // Renders the extension's standard children (extensions that accept - // them — e.g. native-navigation-stack's pushed content). - @Composable - fun Children() { - for (childId in node.children.toList()) { - androidx.compose.runtime.key(childId) { - LuiNodeView(backend, childId) - } - } - } -} - -fun interface LuiExtensionBuilder { - @Composable - fun Render(context: LuiExtensionContext) -} - -class LuiExtensionRegistry { - private data class Registration( - val fingerprint: String?, - val builder: LuiExtensionBuilder, - ) - - private val registrations = mutableMapOf() - - fun register( - identifier: String, - fingerprint: String? = null, - builder: LuiExtensionBuilder, - ) { - registrations[identifier] = Registration(fingerprint, builder) - } - - fun unregister(identifier: String) { - registrations.remove(identifier) - } - - // Null → render the unknown-extension placeholder. - // Mismatched fingerprints render an error surface instead of the - // component (same contract as the other backends). - fun resolve(node: LuiNode): Result { - val identifier = node.identifier ?: return Result.failure( - IllegalStateException("node ${node.id} is not an extension") - ) - val registration = registrations[identifier] - ?: return Result.failure(NoSuchElementException(identifier)) - val expected = registration.fingerprint - if (expected != null && expected != node.fingerprint) { - return Result.failure( - IllegalStateException( - "extension \"$identifier\" fingerprint mismatch: " + - "core sent \"${node.fingerprint}\", app registered \"$expected\"" - ) - ) - } - return Result.success(registration.builder) - } -} diff --git a/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt b/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt deleted file mode 100644 index 70d92fa3..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiIcons.kt +++ /dev/null @@ -1,107 +0,0 @@ -package dev.lui - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.automirrored.filled.Undo -import androidx.compose.material.icons.filled.* -import androidx.compose.ui.graphics.vector.ImageVector - -// Resolves the wire `icon` prop (a symbolic name such as -// "outliner-bullet" or "toolbar-sidebar") to an ImageVector. Apps can -// chain their own app-specific resolver in front of the default. -fun interface LuiIconResolver { - fun icon(name: String): ImageVector? - - fun or(other: LuiIconResolver): LuiIconResolver = LuiIconResolver { name -> - icon(name) ?: other.icon(name) - } - - companion object { - val DEFAULT = LuiIconResolver { name -> defaultIcons[name] } - - private val defaultIcons: Map = mapOf( - "add" to Icons.Filled.Add, - "add-circle" to Icons.Filled.AddCircle, - "arrow-back" to Icons.AutoMirrored.Filled.ArrowBack, - "arrow-forward" to Icons.AutoMirrored.Filled.ArrowForward, - "arrow-down" to Icons.Filled.ArrowDropDown, - "arrow-up" to Icons.Filled.KeyboardArrowUp, - "attach-file" to Icons.Filled.Add, - "audio-lines" to Icons.Filled.Star, - "audio-waveform" to Icons.Filled.Star, - "bell" to Icons.Filled.Notifications, - "bold" to Icons.Filled.Edit, - "bookmark" to Icons.Filled.Bookmark, - "brush" to Icons.Filled.Edit, - "calendar" to Icons.Filled.DateRange, - "camera" to Icons.Filled.Add, - "check" to Icons.Filled.Check, - "check-circle" to Icons.Filled.CheckCircle, - "chevron-down" to Icons.Filled.KeyboardArrowDown, - "chevron-left" to Icons.AutoMirrored.Filled.KeyboardArrowLeft, - "chevron-right" to Icons.AutoMirrored.Filled.KeyboardArrowRight, - "chevron-up" to Icons.Filled.KeyboardArrowUp, - "clock" to Icons.Filled.Star, - "close" to Icons.Filled.Close, - "cloud" to Icons.Filled.Star, - "code" to Icons.Filled.Edit, - "copy" to Icons.Filled.Add, - "dice" to Icons.Filled.Star, - "dots" to Icons.Filled.MoreVert, - "download" to Icons.Filled.Share, - "edit" to Icons.Filled.Edit, - "external-link" to Icons.Filled.Share, - "eye" to Icons.Filled.Star, - "file" to Icons.Filled.Add, - "flag" to Icons.Filled.Star, - "folder" to Icons.Filled.Star, - "hash" to Icons.Filled.Tag, - "heading" to Icons.Filled.Edit, - "history" to Icons.Filled.Star, - "home" to Icons.Filled.Home, - "image" to Icons.Filled.Star, - "info" to Icons.Filled.Info, - "italic" to Icons.Filled.Edit, - "link" to Icons.Filled.Share, - "list" to Icons.Filled.List, - "loader" to Icons.Filled.Refresh, - "lock" to Icons.Filled.Lock, - "logout" to Icons.Filled.ExitToApp, - "menu" to Icons.Filled.Menu, - "message" to Icons.Filled.Email, - "mic" to Icons.Filled.Edit, - "minus" to Icons.Filled.Clear, - "more-horizontal" to Icons.Filled.MoreVert, - "more-vertical" to Icons.Filled.MoreVert, - "paperclip" to Icons.Filled.Add, - "pause" to Icons.Filled.Star, - "pencil" to Icons.Filled.Edit, - "play" to Icons.Filled.PlayArrow, - "plus" to Icons.Filled.Add, - "refresh" to Icons.Filled.Refresh, - "search" to Icons.Filled.Search, - "send" to Icons.AutoMirrored.Filled.Send, - "settings" to Icons.Filled.Settings, - "share" to Icons.Filled.Share, - "sparkles" to Icons.Filled.Star, - "star" to Icons.Filled.Star, - "stop" to Icons.Filled.Close, - "sync" to Icons.Filled.Refresh, - "table" to Icons.Filled.List, - "tag" to Icons.Filled.Tag, - "task" to Icons.Filled.CheckCircle, - "trash" to Icons.Filled.Delete, - "undo" to Icons.AutoMirrored.Filled.Undo, - "upload" to Icons.Filled.Share, - "user" to Icons.Filled.Person, - "video" to Icons.Filled.PlayArrow, - "warning" to Icons.Filled.Warning, - "wifi" to Icons.Filled.Star, - "x" to Icons.Filled.Close, - ) - } -} diff --git a/android/lui/src/main/kotlin/dev/lui/LuiNode.kt b/android/lui/src/main/kotlin/dev/lui/LuiNode.kt deleted file mode 100644 index 54513cbd..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiNode.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.lui - -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateMapOf - -// Retained UI node mirror, one per `create-node`/`create-extension` op on -// the LUI wire. Properties and children are Compose state so applying a -// patch batch recomposes only the touched subtrees. -class LuiNode internal constructor( - val id: Long, - var kind: String, -) { - // Extension nodes carry `identifier`/`fingerprint` instead of a kind. - var identifier: String? = null - var fingerprint: String = "" - - val properties = mutableStateMapOf() - val children = mutableStateListOf() - - val isExtension: Boolean get() = identifier != null - - fun string(name: String): String? = properties[name] as? String - fun int(name: String): Int? = (properties[name] as? Number)?.toInt() - fun long(name: String): Long? = (properties[name] as? Number)?.toLong() - fun double(name: String): Double? = (properties[name] as? Number)?.toDouble() - fun bool(name: String): Boolean = (properties[name] as? Boolean) == true - - fun flag(name: String): Boolean = int(name) != 0 && properties.containsKey(name) - - fun enabled(name: String, default: Boolean = false): Boolean = - if (properties.containsKey(name)) int(name) != 0 else default -} diff --git a/android/lui/src/main/kotlin/dev/lui/LuiProps.kt b/android/lui/src/main/kotlin/dev/lui/LuiProps.kt deleted file mode 100644 index 80b118ad..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiProps.kt +++ /dev/null @@ -1,174 +0,0 @@ -package dev.lui - -import androidx.compose.foundation.border -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.aspectRatio - -// Wire property → Compose mapping shared by every renderer. Property -// names are the kebab-case identifiers of schema/components.json. - -internal fun LuiNode.visible(): Boolean = !properties.containsKey("visible") || int("visible") != 0 - -@Composable -internal fun LuiNode.colorProp(name: String): Color? = - LuiTheme.color(string(name)) - -@Composable -internal fun LuiNode.shape(): RoundedCornerShape = - RoundedCornerShape((int("corner-radius") ?: 0).dp) - -@Composable -internal fun LuiNode.styleModifier(): Modifier { - var modifier: Modifier = Modifier - int("width")?.let { modifier = modifier.width(it.dp) } - int("height")?.let { modifier = modifier.height(it.dp) } - int("min-width")?.let { modifier = modifier.widthIn(min = it.dp) } - int("max-width")?.let { modifier = modifier.widthIn(max = it.dp) } - int("min-height")?.let { modifier = modifier.heightIn(min = it.dp) } - int("max-height")?.let { modifier = modifier.heightIn(max = it.dp) } - when (string("sizing")) { - "fill" -> modifier = modifier.fillMaxWidth() - else -> {} - } - if (flag("fill-width")) modifier = modifier.fillMaxWidth() - if (flag("fill-height") || flag("fill")) modifier = modifier.fillMaxHeight() - if ((double("grow") ?: 0.0) > 0.0) modifier = modifier.fillMaxWidth() - int("padding")?.let { modifier = modifier.padding(it.dp) } - int("padding-horizontal")?.let { modifier = modifier.padding(horizontal = it.dp) } - int("padding-vertical")?.let { modifier = modifier.padding(vertical = it.dp) } - int("padding-top")?.let { modifier = modifier.padding(top = it.dp) } - int("padding-bottom")?.let { modifier = modifier.padding(bottom = it.dp) } - int("padding-start")?.let { modifier = modifier.padding(start = it.dp) } - int("padding-end")?.let { modifier = modifier.padding(end = it.dp) } - val opacity = double("opacity") - if (opacity != null) modifier = modifier.alpha(opacity.toFloat().coerceIn(0f, 1f)) - string("accessibility-identifier")?.let { modifier = modifier.testTag(it) } - string("accessibility-label")?.let { label -> - modifier = modifier.semantics { contentDescription = label } - } - return modifier -} - -@Composable -internal fun LuiNode.surfaceModifier(): Modifier { - var modifier = styleModifier() - val radius = shape() - colorProp("background")?.let { modifier = modifier.background(it, radius) } - val borderColor = colorProp("border-color") - val borderWidth = int("border-width") - if (borderColor != null && borderWidth != null && borderWidth > 0) { - modifier = modifier.border(borderWidth.dp, borderColor, radius) - } - if ((int("corner-radius") ?: 0) > 0 || flag("clip")) { - modifier = modifier.clip(radius) - } - return modifier -} - -internal fun LuiNode.gapDp() = (int("gap") ?: 0).dp - -internal fun LuiNode.mainAxisAlignment(): androidx.compose.foundation.layout.Arrangement.Horizontal = - when (string("main")) { - "center" -> androidx.compose.foundation.layout.Arrangement.Center - "end" -> androidx.compose.foundation.layout.Arrangement.End - "space-between" -> androidx.compose.foundation.layout.Arrangement.SpaceBetween - "space-around" -> androidx.compose.foundation.layout.Arrangement.SpaceAround - "space-evenly" -> androidx.compose.foundation.layout.Arrangement.SpaceEvenly - else -> androidx.compose.foundation.layout.Arrangement.spacedBy(gapDp()) - } - -internal fun LuiNode.mainAxisVertical(): androidx.compose.foundation.layout.Arrangement.Vertical = - when (string("main")) { - "center" -> androidx.compose.foundation.layout.Arrangement.Center - "end", "bottom" -> androidx.compose.foundation.layout.Arrangement.Bottom - "space-between" -> androidx.compose.foundation.layout.Arrangement.SpaceBetween - "space-around" -> androidx.compose.foundation.layout.Arrangement.SpaceAround - "space-evenly" -> androidx.compose.foundation.layout.Arrangement.SpaceEvenly - else -> androidx.compose.foundation.layout.Arrangement.spacedBy(gapDp()) - } - -internal fun LuiNode.crossAxisHorizontal(): androidx.compose.ui.Alignment.Horizontal = - when (string("cross")) { - "center" -> androidx.compose.ui.Alignment.CenterHorizontally - "end" -> androidx.compose.ui.Alignment.End - else -> androidx.compose.ui.Alignment.Start - } - -internal fun LuiNode.crossAxisVertical(): androidx.compose.ui.Alignment.Vertical = - when (string("cross")) { - "center" -> androidx.compose.ui.Alignment.CenterVertically - "end", "bottom" -> androidx.compose.ui.Alignment.Bottom - else -> androidx.compose.ui.Alignment.Top - } - -internal fun LuiNode.textAlign(): TextAlign? = when (string("text-alignment")) { - "center" -> TextAlign.Center - "end", "right" -> TextAlign.End - "start", "left" -> TextAlign.Start - "justify" -> TextAlign.Justify - else -> null -} - -@Composable -internal fun LuiNode.textStyle(): TextStyle { - val base = when (string("size")) { - "sm" -> MaterialTheme.typography.bodySmall - "lg" -> MaterialTheme.typography.titleMedium - "heading" -> MaterialTheme.typography.headlineSmall - "display" -> MaterialTheme.typography.displaySmall - "icon" -> MaterialTheme.typography.bodyMedium - else -> MaterialTheme.typography.bodyMedium - } - return base.copy( - color = colorProp("foreground") ?: MaterialTheme.colorScheme.onSurface, - fontWeight = when (string("weight")) { - "bold", "semibold" -> FontWeight.SemiBold - "medium" -> FontWeight.Medium - "light" -> FontWeight.Light - else -> base.fontWeight - }, - fontStyle = if (string("style") == "italic") FontStyle.Italic else base.fontStyle, - ) -} - -@Composable -internal fun LuiNode.headingStyle(): TextStyle { - val theme = MaterialTheme.typography - val level = int("level") ?: int("heading-level") ?: 1 - val base = when (level) { - 1 -> theme.headlineLarge - 2 -> theme.headlineMedium - 3 -> theme.titleLarge - 4 -> theme.titleMedium - 5 -> theme.titleSmall - else -> theme.labelLarge - } - return base.copy(color = colorProp("foreground") ?: MaterialTheme.colorScheme.onSurface) -} diff --git a/android/lui/src/main/kotlin/dev/lui/LuiRenderers.kt b/android/lui/src/main/kotlin/dev/lui/LuiRenderers.kt deleted file mode 100644 index 7d7dd2ac..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiRenderers.kt +++ /dev/null @@ -1,819 +0,0 @@ -package dev.lui - -import android.util.Log -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Badge -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.ModalDrawerSheet -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.RadioButton -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow -import androidx.compose.material3.Slider -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupProperties - -private const val TAG = "LuiRender" - -@Composable -internal fun LuiRoot(backend: LuiBackend, rootId: Long) { - Box( - Modifier - .fillMaxSize() - .semantics { testTagsAsResourceId = true } - ) { - val root = backend.node(rootId) - if (root != null) { - for (childId in root.children.toList()) { - key(childId) { LuiNodeView(backend, childId) } - } - } - } -} - -@Composable -private fun key(id: Long, content: @Composable () -> T) = - androidx.compose.runtime.key(id) { content() } - -@Composable -internal fun LuiNodeView(backend: LuiBackend, id: Long) { - val node = backend.node(id) ?: return - if (!node.visible()) return - LuiTheme.NodeScope(node) { - if (node.isExtension) { - ExtensionView(backend, node) - } else { - NodeView(backend, node) - } - } -} - -@Composable -private fun ExtensionView(backend: LuiBackend, node: LuiNode) { - val result = backend.extensions.resolve(node) - result.onSuccess { builder -> - builder.Render(LuiExtensionContext(node, backend)) - }.onFailure { error -> - Surface( - modifier = Modifier.padding(8.dp), - color = MaterialTheme.colorScheme.errorContainer, - shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp), - ) { - Text( - text = "Extension \"${node.identifier}\" unavailable: ${error.message}", - modifier = Modifier.padding(12.dp), - color = MaterialTheme.colorScheme.onErrorContainer, - style = MaterialTheme.typography.bodySmall, - ) - } - } -} - -@Composable -private fun Children(backend: LuiBackend, node: LuiNode) { - for (childId in node.children.toList()) { - key(childId) { LuiNodeView(backend, childId) } - } -} - -@Composable -private fun AppearOnce(backend: LuiBackend, node: LuiNode) { - if (node.enabled("appear-enabled")) { - LaunchedEffect(node.id) { backend.dispatch(LuiEvent.Appear(node.id)) } - } -} - -// Emits the press/long-press/double-press gestures a node opted into. -@Composable -private fun Modifier.gestures(backend: LuiBackend, node: LuiNode): Modifier { - val hasLong = node.enabled("long-press-enabled") - val hasDouble = node.enabled("double-press-enabled") - val hasPress = node.enabled("press-enabled") || node.kind == "button" || - node.kind == "toggle-button" || node.kind == "link" || - node.kind == "menu-item" || node.kind == "bottom-tab" || - node.kind == "list-item" || node.kind == "radio" - return if (hasPress || hasLong || hasDouble) { - combinedClickable( - onClick = { backend.dispatch(LuiEvent.Press(node.id)) }, - onLongClick = if (hasLong) { - { backend.dispatch(LuiEvent.LongPress(node.id)) } - } else null, - onDoubleClick = if (hasDouble) { - { backend.dispatch(LuiEvent.DoublePress(node.id)) } - } else null, - ) - } else this -} - -@Composable -private fun NodeView(backend: LuiBackend, node: LuiNode) { - AppearOnce(backend, node) - when (node.kind) { - "root", "box", "edge-inset", "view-that-fits" -> - Frame(backend, node) - "column" -> ColumnNode(backend, node) - "row" -> RowNode(backend, node) - "grid" -> GridNode(backend, node) - "stack", "overlay" -> StackNode(backend, node) - "scroll" -> ScrollNode(backend, node) - "list", "list-section" -> ListNode(backend, node) - "virtual-list" -> VirtualListNode(backend, node) - "spacer" -> SpacerNode(node) - "divider" -> HorizontalDivider( - modifier = node.styleModifier(), - color = node.colorProp("foreground") - ?: MaterialTheme.colorScheme.outlineVariant, - ) - "text", "label" -> TextNode(backend, node) - "heading" -> Text( - text = node.string("text") ?: node.string("title").orEmpty(), - modifier = node.styleModifier().gestures(backend, node), - style = node.headingStyle(), - ) - "paragraph" -> Text( - text = node.string("text").orEmpty(), - modifier = node.styleModifier().gestures(backend, node), - style = node.textStyle(), - ) - "kbd" -> Text( - text = node.string("text").orEmpty(), - modifier = node.styleModifier(), - style = node.textStyle().copy(fontFamily = FontFamily.Monospace), - ) - "icon" -> IconNode(backend, node) - "button", "toggle-button" -> ButtonNode(backend, node) - "link" -> Text( - text = node.string("text") ?: node.string("title").orEmpty(), - modifier = node.styleModifier().gestures(backend, node), - style = node.textStyle().copy( - color = MaterialTheme.colorScheme.primary, - ), - ) - "toggle", "switch" -> Switch( - checked = node.flag("checked"), - onCheckedChange = { backend.dispatch(LuiEvent.ToggleChanged(node.id, it)) }, - enabled = node.enabled("enabled", true), - modifier = node.styleModifier(), - ) - "checkbox" -> Checkbox( - checked = node.flag("checked"), - onCheckedChange = { backend.dispatch(LuiEvent.ToggleChanged(node.id, it)) }, - enabled = node.enabled("enabled", true), - modifier = node.styleModifier(), - ) - "radio" -> RadioButton( - selected = node.flag("checked") || node.flag("selected"), - onClick = { backend.dispatch(LuiEvent.Press(node.id)) }, - enabled = node.enabled("enabled", true), - modifier = node.styleModifier(), - ) - "radio-group", "button-group", "toggle-group" -> - ColumnNode(backend, node) - "slider" -> Slider( - value = (node.double("value") ?: 0.0).toFloat(), - onValueChange = { - backend.dispatch(LuiEvent.ValueChanged(node.id, it.toDouble())) - }, - enabled = node.enabled("enabled", true), - modifier = node.styleModifier(), - ) - "progress" -> LinearProgressIndicator( - progress = { (node.double("value") ?: 0.0).toFloat() }, - modifier = node.styleModifier().fillMaxWidth(), - ) - "spinner" -> CircularProgressIndicator( - modifier = node.styleModifier(), - color = node.colorProp("foreground") ?: MaterialTheme.colorScheme.primary, - ) - "text-field", "input", "secure-field", "textarea" -> - TextFieldNode(backend, node) - "search-field" -> TextFieldNode(backend, node) - "card", "panel", "bubble" -> CardNode(backend, node) - "alert" -> AlertNode(backend, node) - "toolbar", "status-bar", "breadcrumb" -> RowNode(backend, node) - "list-item" -> RowNode(backend, node) - "image", "file-image", "file-preview" -> ImageNode(backend, node) - "tabs", "bottom-tabs" -> TabsNode(backend, node) - "bottom-tab", "tab" -> ButtonNode(backend, node) - "menu-item" -> RowNode(backend, node) - "menu-trigger" -> MenuTrigger(backend, node) - "dropdown-menu", "context-menu" -> MenuNode(backend, node) - "dialog" -> DialogNode(backend, node) - "sheet" -> SheetNode(backend, node) - "drawer" -> DrawerNode(backend, node) - "popover", "tooltip" -> PopupNode(backend, node) - "toast" -> ToastNode(backend, node) - "tree" -> ColumnNode(backend, node) - "badge" -> BadgeNode(backend, node) - "br" -> Spacer(Modifier.height(4.dp)) - "media-surface" -> Surface( - modifier = node.styleModifier().fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - ) {} - "file-picker" -> FilePickerNode(backend, node) - "select", "combobox" -> SelectNode(backend, node) - "number-stepper" -> StepperNode(backend, node) - "stepper" -> StepperNode(backend, node) - "step" -> Frame(backend, node) - "accordion", "table", "table-row", "table-cell", "timeline", - "timeline-item", "resizable", "split", "swipe-actions", - "swipe-action", "input-group", "input-group-actions", - "pagination", "list-section-header", "list-section-footer", - "avatar", "segmented-control" -> FallbackContainer(backend, node) - else -> FallbackContainer(backend, node) - } -} - -@Composable -private fun Frame(backend: LuiBackend, node: LuiNode) { - Box( - modifier = node - .surfaceModifier() - .gestures(backend, node), - ) { - Children(backend, node) - } -} - -@Composable -private fun ColumnNode(backend: LuiBackend, node: LuiNode) { - Column( - modifier = node.surfaceModifier().gestures(backend, node), - verticalArrangement = node.mainAxisVertical(), - horizontalAlignment = node.crossAxisHorizontal(), - ) { - for (childId in node.children.toList()) { - val child = backend.node(childId) ?: continue - if (!child.visible()) continue - val grow = child.double("grow") - key(childId) { - if (grow != null && grow > 0) { - Box(Modifier.weight(grow.toFloat())) { LuiNodeView(backend, childId) } - } else { - LuiNodeView(backend, childId) - } - } - } - } -} - -@Composable -private fun RowNode(backend: LuiBackend, node: LuiNode) { - Row( - modifier = node.surfaceModifier().gestures(backend, node), - horizontalArrangement = node.mainAxisAlignment(), - verticalAlignment = node.crossAxisVertical(), - ) { - for (childId in node.children.toList()) { - val child = backend.node(childId) ?: continue - if (!child.visible()) continue - val grow = child.double("grow") - key(childId) { - if (grow != null && grow > 0) { - Box(Modifier.weight(grow.toFloat())) { LuiNodeView(backend, childId) } - } else { - LuiNodeView(backend, childId) - } - } - } - } -} - -@Composable -private fun GridNode(backend: LuiBackend, node: LuiNode) { - val columns = (node.int("columns") ?: 2).coerceAtLeast(1) - Column( - modifier = node.surfaceModifier().gestures(backend, node), - verticalArrangement = node.mainAxisVertical(), - ) { - val children = node.children.toList() - for (row in children.chunked(columns)) { - Row(horizontalArrangement = Arrangement.spacedBy(node.gapDp())) { - for (childId in row) { - key(childId) { - Box(Modifier.weight(1f)) { LuiNodeView(backend, childId) } - } - } - } - } - } -} - -@Composable -private fun StackNode(backend: LuiBackend, node: LuiNode) { - Box( - modifier = node.surfaceModifier().gestures(backend, node), - contentAlignment = when (node.string("cross-alignment")) { - "center" -> Alignment.Center - "end" -> Alignment.BottomEnd - else -> Alignment.TopStart - }, - ) { - Children(backend, node) - } -} - -@Composable -private fun ScrollNode(backend: LuiBackend, node: LuiNode) { - Column( - modifier = node - .surfaceModifier() - .gestures(backend, node) - .verticalScroll(rememberScrollState()), - verticalArrangement = node.mainAxisVertical(), - horizontalAlignment = node.crossAxisHorizontal(), - ) { - Children(backend, node) - } -} - -@Composable -private fun ListNode(backend: LuiBackend, node: LuiNode) { - Column( - modifier = node.surfaceModifier().gestures(backend, node), - verticalArrangement = node.mainAxisVertical(), - horizontalAlignment = node.crossAxisHorizontal(), - ) { - Children(backend, node) - } -} - -@Composable -private fun VirtualListNode(backend: LuiBackend, node: LuiNode) { - val listState = rememberLazyListState() - val trackRange = node.enabled("track-visible-range") - if (trackRange) { - LaunchedEffect(node.id) { - snapshotFlow { listState.firstVisibleItemIndex to listState.layoutInfo.visibleItemsInfo } - .collect { (first, visible) -> - val last = first + visible.size - 1 - backend.dispatch(LuiEvent.VisibleRange(node.id, first.toLong(), last.toLong())) - } - } - } - LazyColumn( - state = listState, - modifier = node.surfaceModifier().gestures(backend, node), - verticalArrangement = node.mainAxisVertical(), - horizontalAlignment = node.crossAxisHorizontal(), - ) { - for (childId in node.children.toList()) { - item(key = childId) { LuiNodeView(backend, childId) } - } - } -} - -@Composable -private fun SpacerNode(node: LuiNode) { - Spacer( - Modifier - .width((node.int("width") ?: 8).dp) - .height((node.int("height") ?: 8).dp) - ) -} - -@Composable -private fun TextNode(backend: LuiBackend, node: LuiNode) { - Text( - text = node.string("text") ?: node.string("title").orEmpty(), - modifier = node.styleModifier().gestures(backend, node), - style = node.textStyle(), - textAlign = node.textAlign(), - overflow = if (node.flag("wrap")) { - androidx.compose.ui.text.style.TextOverflow.Clip - } else { - androidx.compose.ui.text.style.TextOverflow.Ellipsis - }, - maxLines = node.int("max-lines") ?: Int.MAX_VALUE, - ) -} - -@Composable -private fun IconNode(backend: LuiBackend, node: LuiNode) { - val name = node.string("name") ?: node.string("icon") ?: return - val vector = backend.icons.icon(name) - if (vector != null) { - Icon( - imageVector = vector, - contentDescription = node.string("accessibility-label"), - tint = node.colorProp("foreground") ?: MaterialTheme.colorScheme.onSurface, - modifier = node.styleModifier().size((node.int("width") ?: 20).dp), - ) - } else { - Text( - text = name, - modifier = node.styleModifier(), - style = node.textStyle(), - ) - } -} - -@Composable -private fun ButtonNode(backend: LuiBackend, node: LuiNode) { - val variant = node.string("variant") ?: "filled" - val enabled = node.enabled("enabled", true) - val modifier = node.styleModifier() - val colors = ButtonDefaults.buttonColors( - containerColor = node.colorProp("background") ?: Color.Unspecified, - contentColor = node.colorProp("foreground") ?: Color.Unspecified, - ) - val content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit = { - node.string("icon")?.let { iconName -> - backend.icons.icon(iconName)?.let { - Icon(it, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(6.dp)) - } - } - node.string("text")?.let { Text(it) } - node.string("title")?.let { Text(it) } - Children(backend, node) - } - when (variant) { - "tonal", "secondary" -> FilledTonalButton( - onClick = { backend.dispatch(LuiEvent.Press(node.id)) }, - enabled = enabled, - modifier = modifier, - colors = colors, - content = content, - ) - "outline", "outlined" -> OutlinedButton( - onClick = { backend.dispatch(LuiEvent.Press(node.id)) }, - enabled = enabled, - modifier = modifier, - content = content, - ) - "ghost", "text" -> TextButton( - onClick = { backend.dispatch(LuiEvent.Press(node.id)) }, - enabled = enabled, - modifier = modifier, - content = content, - ) - else -> Button( - onClick = { backend.dispatch(LuiEvent.Press(node.id)) }, - enabled = enabled, - modifier = modifier, - colors = colors, - content = content, - ) - } -} - -@Composable -private fun TextFieldNode(backend: LuiBackend, node: LuiNode) { - val propValue = node.string("text") ?: "" - var fieldValue by remember(node.id) { mutableStateOf(TextFieldValue(propValue)) } - // Keep local IME state in sync when the core patches `value` back. - if (fieldValue.text != propValue && !node.enabled("editing")) { - fieldValue = fieldValue.copy(text = propValue) - } - val focusRequester = remember { FocusRequester() } - val onSubmit: () -> Unit = { backend.dispatch(LuiEvent.Submit(node.id)) } - val fieldModifier = node - .styleModifier() - .focusRequester(focusRequester) - .fillMaxWidth() - val commonKeyboard = KeyboardOptions( - keyboardType = when { - node.kind == "secure-field" -> KeyboardType.Password - else -> KeyboardType.Text - }, - ) - val actions = KeyboardActions(onDone = { onSubmit() }, onSend = { onSubmit() }) - val change: (TextFieldValue) -> Unit = { newValue -> - fieldValue = newValue - backend.dispatch(LuiEvent.TextChanged(node.id, newValue.text)) - } - val visualTransformation = if (node.kind == "secure-field") { - PasswordVisualTransformation() - } else { - androidx.compose.ui.text.input.VisualTransformation.None - } - if (node.kind == "search-field") { - OutlinedTextField( - value = fieldValue, - onValueChange = change, - modifier = fieldModifier, - enabled = node.enabled("enabled", true), - placeholder = node.string("placeholder")?.let { { Text(it) } }, - leadingIcon = { - backend.icons.icon("search")?.let { - Icon(it, contentDescription = null) - } - }, - singleLine = true, - keyboardOptions = commonKeyboard, - keyboardActions = actions, - visualTransformation = visualTransformation, - ) - } else { - TextField( - value = fieldValue, - onValueChange = change, - modifier = fieldModifier, - enabled = node.enabled("enabled", true), - placeholder = node.string("placeholder")?.let { { Text(it) } }, - label = node.string("label")?.let { { Text(it) } }, - singleLine = node.kind != "textarea", - minLines = if (node.kind == "textarea") 3 else 1, - keyboardOptions = commonKeyboard, - keyboardActions = actions, - visualTransformation = visualTransformation, - ) - } - if (node.enabled("autofocus")) { - LaunchedEffect(node.id) { focusRequester.requestFocus() } - } -} - -@Composable -private fun CardNode(backend: LuiBackend, node: LuiNode) { - Card( - modifier = node.styleModifier().gestures(backend, node), - shape = node.shape(), - colors = androidx.compose.material3.CardDefaults.cardColors( - containerColor = node.colorProp("background") ?: Color.Unspecified, - contentColor = node.colorProp("foreground") ?: Color.Unspecified, - ), - ) { - Column( - modifier = Modifier.padding((node.int("padding") ?: 0).dp), - verticalArrangement = node.mainAxisVertical(), - horizontalAlignment = node.crossAxisHorizontal(), - ) { - Children(backend, node) - } - } -} - -@Composable -private fun AlertNode(backend: LuiBackend, node: LuiNode) { - Surface( - modifier = node.styleModifier().gestures(backend, node), - shape = node.shape(), - color = node.colorProp("background") ?: MaterialTheme.colorScheme.errorContainer, - contentColor = node.colorProp("foreground") ?: MaterialTheme.colorScheme.onErrorContainer, - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = node.mainAxisVertical(), - ) { - node.string("title")?.let { - Text(it, style = MaterialTheme.typography.titleSmall) - } - node.string("description")?.let { - Text(it, style = MaterialTheme.typography.bodySmall) - } - Children(backend, node) - } - } -} - -@Composable -private fun ImageNode(backend: LuiBackend, node: LuiNode) { - // LUI image sources are app-relative paths or remote URLs; the core - // supplies `url`/`path` props. Coil is intentionally not a LUI - // dependency — apps register an "image" extension instead; here we - // render a labelled placeholder when no loader is present. - Surface( - modifier = node.styleModifier().gestures(backend, node), - shape = node.shape(), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - ) { - val label = node.string("accessibility-label") - ?: node.string("title") - ?: "image" - Box( - modifier = Modifier - .fillMaxWidth() - .height((node.int("height") ?: 120).dp), - contentAlignment = Alignment.Center, - ) { - Text(label, style = MaterialTheme.typography.bodySmall) - } - } -} - -@Composable -private fun TabsNode(backend: LuiBackend, node: LuiNode) { - // Children carry their own tab chrome; `active-index` marks the - // selected tab for button children that read `selected`. - Column( - modifier = node.surfaceModifier().gestures(backend, node), - ) { - Children(backend, node) - } -} - -@Composable -private fun MenuTrigger(backend: LuiBackend, node: LuiNode) { - Box { - Children(backend, node) - } -} - -@Composable -private fun MenuNode(backend: LuiBackend, node: LuiNode) { - if (!node.flag("open") && !node.enabled("open")) return - DropdownMenu( - expanded = true, - onDismissRequest = { backend.dispatch(LuiEvent.Dismiss(node.id)) }, - ) { - for (childId in node.children.toList()) { - val child = backend.node(childId) ?: continue - if (child.kind == "menu-item") { - DropdownMenuItem( - text = { - Text(child.string("title") ?: child.string("value").orEmpty()) - }, - onClick = { backend.dispatch(LuiEvent.Press(child.id)) }, - ) - } else { - key(childId) { LuiNodeView(backend, childId) } - } - } - } -} - -@Composable -private fun DialogNode(backend: LuiBackend, node: LuiNode) { - Dialog( - onDismissRequest = { backend.dispatch(LuiEvent.Dismiss(node.id)) }, - ) { - Surface( - modifier = node.styleModifier(), - shape = node.shape(), - color = node.colorProp("background") ?: MaterialTheme.colorScheme.surface, - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = node.mainAxisVertical(), - ) { - Children(backend, node) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SheetNode(backend: LuiBackend, node: LuiNode) { - ModalBottomSheet( - onDismissRequest = { backend.dispatch(LuiEvent.Dismiss(node.id)) }, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - containerColor = node.colorProp("background") - ?: MaterialTheme.colorScheme.surface, - ) { - Column( - modifier = node.styleModifier(), - verticalArrangement = node.mainAxisVertical(), - ) { - Children(backend, node) - } - } -} - -@Composable -private fun DrawerNode(backend: LuiBackend, node: LuiNode) { - // Rendered as a modal popup anchored to the start edge — the interim - // backend has no scaffold context to attach a real drawer to. - Popup( - alignment = Alignment.TopStart, - onDismissRequest = { backend.dispatch(LuiEvent.Dismiss(node.id)) }, - properties = PopupProperties(focusable = true), - ) { - ModalDrawerSheet( - modifier = node.styleModifier().fillMaxSize(0.9f), - ) { - Children(backend, node) - } - } -} - -@Composable -private fun PopupNode(backend: LuiBackend, node: LuiNode) { - Popup( - onDismissRequest = { backend.dispatch(LuiEvent.Dismiss(node.id)) }, - properties = PopupProperties(focusable = node.kind == "popover"), - ) { - Surface( - shape = node.shape(), - color = node.colorProp("background") ?: MaterialTheme.colorScheme.surfaceContainerHigh, - shadowElevation = 4.dp, - ) { - Box(Modifier.padding(8.dp)) { Children(backend, node) } - } - } -} - -@Composable -private fun ToastNode(backend: LuiBackend, node: LuiNode) { - Popup(alignment = Alignment.BottomCenter) { - Surface( - shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp), - color = node.colorProp("background") ?: MaterialTheme.colorScheme.inverseSurface, - contentColor = MaterialTheme.colorScheme.inverseOnSurface, - shadowElevation = 6.dp, - ) { - Box(Modifier.padding(horizontal = 16.dp, vertical = 10.dp)) { - Children(backend, node) - } - } - } -} - -@Composable -private fun BadgeNode(backend: LuiBackend, node: LuiNode) { - Badge(modifier = node.styleModifier()) { - Text(node.string("text") ?: node.string("title").orEmpty()) - } -} - -@Composable -private fun FilePickerNode(backend: LuiBackend, node: LuiNode) { - // The core opens pickers through effects; the node is a trigger. - Frame(backend, node) -} - -@Composable -private fun SelectNode(backend: LuiBackend, node: LuiNode) { - Frame(backend, node) -} - -@Composable -private fun StepperNode(backend: LuiBackend, node: LuiNode) { - Row( - modifier = node.styleModifier().gestures(backend, node), - verticalAlignment = Alignment.CenterVertically, - ) { - Children(backend, node) - } -} - -@Composable -private fun FallbackContainer(backend: LuiBackend, node: LuiNode) { - Log.d(TAG, "rendering '${node.kind}' as a plain container") - Column(modifier = node.surfaceModifier().gestures(backend, node)) { - Children(backend, node) - } -} diff --git a/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt b/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt deleted file mode 100644 index cf6b0162..00000000 --- a/android/lui/src/main/kotlin/dev/lui/LuiTheme.kt +++ /dev/null @@ -1,150 +0,0 @@ -package dev.lui - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.ui.graphics.Color -import org.json.JSONObject - -// Theme token scoping: nodes carry a `theme` prop — a JSON object mapping -// token names to either "#rrggbb[aa]" colors or {"light","dark"} adaptive -// pairs — and a `theme-mode` prop ("system"|"light"|"dark") that picks -// the effective brightness under this subtree. -data class LuiThemeTokens(val values: Map = emptyMap()) { - fun merge(raw: String?): LuiThemeTokens { - if (raw.isNullOrEmpty()) return this - val decoded = try { - JSONObject(raw) - } catch (_: Exception) { - return this - } - val merged = values.toMutableMap() - for (key in decoded.keys()) merged[key.lowercase()] = decoded.opt(key) - return LuiThemeTokens(merged) - } - - // Token value → color string for the active brightness. - fun resolve(name: String?, dark: Boolean): String? { - val value = values[name?.lowercase()] ?: return null - return when (value) { - is String -> value - is JSONObject -> (if (dark) value.optString("dark") else value.optString("light")) - .takeIf(String::isNotEmpty) - else -> null - } - } -} - -val LocalLuiThemeTokens = compositionLocalOf { LuiThemeTokens() } -val LocalLuiThemeDark = compositionLocalOf { false } - -object LuiTheme { - // Named colors fall back through the same palette the other LUI - // backends use (Material shadcn-style tokens, then base colors). - @Composable - @ReadOnlyComposable - fun color(name: String?, foreground: Boolean = false): Color? { - if (name == null) return null - val dark = LocalLuiThemeDark.current - val token = LocalLuiThemeTokens.current.resolve(name, dark) - token?.let { parseColor(it) }?.let { return it } - return schemeColor(name.lowercase(), MaterialTheme.colorScheme, foreground) - } - - fun parseColor(value: String?): Color? { - if (value == null) return null - var raw = value.removePrefix("#") - if (raw.length == 3 || raw.length == 4) { - raw = raw.toCharArray().joinToString("") { "$it$it" } - } - if (raw.length == 6) raw = "ff$raw" - else if (raw.length == 8) raw = raw.substring(6) + raw.substring(0, 6) - if (raw.length != 8) return null - val parsed = raw.toLongOrNull(16) ?: return null - return Color(parsed) - } - - private fun schemeColor(name: String, colors: ColorScheme, foreground: Boolean): Color? = - when (name) { - "transparent" -> Color.Transparent - "background" -> colors.surface - "foreground" -> colors.onSurface - "surface" -> colors.surface - "surface-container-lowest" -> colors.surfaceContainerLowest - "surface-container-low" -> colors.surfaceContainerLow - "surface-container" -> colors.surfaceContainer - "surface-container-high" -> colors.surfaceContainerHigh - "surface-container-highest" -> colors.surfaceContainerHighest - "surface-variant" -> colors.surfaceContainerHighest - "autocomplete-row-background" -> colors.surfaceContainerHigh - "glass" -> colors.surface.copy(alpha = 0.75f) - "glass-fallback" -> colors.surface.copy(alpha = 0.9f) - "bar" -> colors.surface.copy(alpha = 0.85f) - "primary" -> colors.primary - "primary-foreground" -> colors.onPrimary - "accent" -> if (foreground) colors.primary else colors.secondaryContainer - "accent-foreground" -> - if (foreground) colors.onPrimary else colors.onSecondaryContainer - "secondary" -> - if (foreground) colors.onSurfaceVariant else colors.secondaryContainer - "secondary-foreground" -> colors.onSecondaryContainer - "muted" -> colors.surfaceContainerHigh - "muted-foreground" -> colors.onSurfaceVariant - "destructive" -> colors.error - "destructive-foreground" -> colors.onError - "success" -> colors.tertiaryContainer - "success-foreground" -> colors.onTertiaryContainer - "warning" -> colors.secondaryContainer - "warning-foreground" -> colors.onSecondaryContainer - "error" -> colors.errorContainer - "error-foreground" -> colors.onErrorContainer - "border" -> colors.outlineVariant - "black" -> Color.Black - "white" -> Color.White - "red" -> Color.Red - "blue" -> Color.Blue - "green" -> Color.Green - "card" -> colors.surfaceContainerLowest - else -> Color.Transparent - } - - // Effective dark flag for a `theme-mode` prop value. - @Composable - fun darkForMode(mode: String?): Boolean = when (mode) { - "light" -> false - "dark" -> true - else -> isSystemInDarkTheme() - } - - @Composable - fun Provider(mode: String?, content: @Composable () -> Unit) { - val dark = darkForMode(mode) - MaterialTheme( - colorScheme = if (dark) darkColorScheme() else lightColorScheme(), - content = content, - ) - } - - @Composable - fun NodeScope(node: LuiNode, content: @Composable () -> Unit) { - val inherited = LocalLuiThemeTokens.current - val themeProp = node.string("theme") - val scoped = if (themeProp != null) inherited.merge(themeProp) else inherited - val mode = node.string("theme-mode") - val dark = if (mode != null) darkForMode(mode) else LocalLuiThemeDark.current - if (scoped !== inherited || mode != null) { - CompositionLocalProvider( - LocalLuiThemeTokens provides scoped, - LocalLuiThemeDark provides dark, - ) { content() } - } else { - content() - } - } -} diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 1a0e55ee..279967fc 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -6,24 +6,20 @@ pluginManagement { } } -// The Kotlin/Jetpack Compose LUI backend is developed in the logseq/lui -// checkout at ../lui (platform/android on branch devin/android-kotlin). -// When that build exists it is consumed as a composite build, mirroring -// how apple/ path-deps ../lui/platform/apple. Until it lands, the app -// builds against the interim dev.lui implementation vendored in :lui — -// it implements the documented dev.lui surface (LuiBackend patch/node -// store, LuiTheme, node renderers, LuiExtensionRegistry) so swapping it -// for the real backend is a settings-only change. -val luiCheckoutBuild = File(rootDir, "../../lui/platform/android") -val useCheckoutLui = luiCheckoutBuild.isDirectory && - File(luiCheckoutBuild, "settings.gradle.kts").isFile - -if (useCheckoutLui) { - includeBuild(luiCheckoutBuild) -} else { - include(":lui") +// The Kotlin/Jetpack Compose LUI backend is consumed as an included +// module from the logseq/lui checkout at ../lui/platform/android/lui — +// the same consumption model as lui's own +// examples/components/android app, mirroring how apple/ path-deps +// ../lui/platform/apple. The module resolves plugins from this build's +// classpath (AGP 9 + builtInKotlin). +val luiModuleDir = File(rootDir, "../../lui/platform/android/lui") +require(luiModuleDir.isDirectory && File(luiModuleDir, "build.gradle.kts").isFile) { + "Expected a logseq/lui checkout with platform/android/lui at ../lui" } +include(":lui") +project(":lui").projectDir = luiModuleDir + dependencyResolutionManagement { repositories { google() diff --git a/apple/Sources/LogseqChat/LGChatNavigationExtension.swift b/apple/Sources/LogseqChat/LGChatNavigationExtension.swift index a9bd1c7b..d335bac2 100644 --- a/apple/Sources/LogseqChat/LGChatNavigationExtension.swift +++ b/apple/Sources/LogseqChat/LGChatNavigationExtension.swift @@ -27,7 +27,7 @@ enum LGChatNavigationSurfacePolicy { @MainActor enum LGChatNavigationExtension { static let identifier = "native-navigation-stack" - static let fingerprint = "lui-extension-v1|23:native-navigation-stack|profiles:android/flutter,ios/swiftui|standard-children:1|children:|properties:26:composer-dismissal-enabled:bool:required:none,28:bottom-occupies-layout-space:bool:required:none,5:depth:int:required:none,5:title:string:required:none|events:16:dismiss-composer[],4:back[5:count:int:required]" + static let fingerprint = "lui-extension-v1|23:native-navigation-stack|profiles:android/kotlin,ios/swiftui|standard-children:1|children:|properties:26:composer-dismissal-enabled:bool:required:none,28:bottom-occupies-layout-space:bool:required:none,5:depth:int:required:none,5:title:string:required:none|events:16:dismiss-composer[],4:back[5:count:int:required]" static func register(in registry: LUIAppleExtensionRegistry) throws { try registry.register( @@ -97,7 +97,7 @@ private struct LGChatNavigationStack: View { @MainActor enum LGChatSearchPresentationExtension { static let identifier = "native-search-presentation" - static let fingerprint = "lui-extension-v1|26:native-search-presentation|profiles:android/flutter,ios/swiftui|standard-children:1|children:|properties:5:depth:int:required:none,5:query:string:required:none,5:title:string:required:none,9:presented:bool:required:none|events:13:query-changed[5:query:string:required],4:back[5:count:int:required],7:dismiss[]" + static let fingerprint = "lui-extension-v1|26:native-search-presentation|profiles:android/kotlin,ios/swiftui|standard-children:1|children:|properties:5:depth:int:required:none,5:query:string:required:none,5:title:string:required:none,9:presented:bool:required:none|events:13:query-changed[5:query:string:required],4:back[5:count:int:required],7:dismiss[]" static func register(in registry: LUIAppleExtensionRegistry) throws { try registry.register( diff --git a/apple/Sources/LogseqChat/LGChatOutlinerEditorExtension.swift b/apple/Sources/LogseqChat/LGChatOutlinerEditorExtension.swift index d3db7eab..653e91f3 100644 --- a/apple/Sources/LogseqChat/LGChatOutlinerEditorExtension.swift +++ b/apple/Sources/LogseqChat/LGChatOutlinerEditorExtension.swift @@ -4,7 +4,7 @@ import SwiftUI @MainActor enum LGChatOutlinerEditorExtension { static let identifier = "outliner-editor" - static let fingerprint = "lui-extension-v1|15:outliner-editor|profiles:android/flutter,ios/swiftui|standard-children:0|children:|properties:18:caret-utf16-offset:int:required:none,5:title:string:required:none,8:block-id:string:required:none|events:11:text-change[18:caret-utf16-offset:int:required,5:title:string:required],12:caret-change[18:caret-utf16-offset:int:required],6:return[18:caret-utf16-offset:int:required,5:title:string:required],9:backspace[16:selection-length:int:required,5:title:string:required]" + static let fingerprint = "lui-extension-v1|15:outliner-editor|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:18:caret-utf16-offset:int:required:none,5:title:string:required:none,8:block-id:string:required:none|events:11:text-change[18:caret-utf16-offset:int:required,5:title:string:required],12:caret-change[18:caret-utf16-offset:int:required],6:return[18:caret-utf16-offset:int:required,5:title:string:required],9:backspace[16:selection-length:int:required,5:title:string:required]" static func register(in registry: LUIAppleExtensionRegistry) throws { try registry.register( diff --git a/apple/Sources/LogseqChat/LGChatRichBlockExtension.swift b/apple/Sources/LogseqChat/LGChatRichBlockExtension.swift index de0e3be6..3e399e42 100644 --- a/apple/Sources/LogseqChat/LGChatRichBlockExtension.swift +++ b/apple/Sources/LogseqChat/LGChatRichBlockExtension.swift @@ -60,7 +60,7 @@ private struct OutlinerRowDropDelegate: DropDelegate { @MainActor private enum LGChatRichBlockExtension { static let identifier = "outliner-block-content" - static let fingerprint = "lui-extension-v1|22:outliner-block-content|profiles:android/flutter,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]" + static let fingerprint = "lui-extension-v1|22:outliner-block-content|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]" static func register( in registry: LUIAppleExtensionRegistry, diff --git a/apple/Sources/LogseqChat/LGChatRuntime.swift b/apple/Sources/LogseqChat/LGChatRuntime.swift index 639609cc..8b8b0f85 100644 --- a/apple/Sources/LogseqChat/LGChatRuntime.swift +++ b/apple/Sources/LogseqChat/LGChatRuntime.swift @@ -143,7 +143,7 @@ public final class LGChatRuntime { public func start( platformCode: Int, - hostCode: Int = 1, + hostCode: Int = 2, authenticationCode: Int = 0 ) throws { guard !isStarted else { return } diff --git a/apple/Tests/LogseqChatTests/LGChatRendererTests.swift b/apple/Tests/LogseqChatTests/LGChatRendererTests.swift index cfabe8c2..581c6af8 100644 --- a/apple/Tests/LogseqChatTests/LGChatRendererTests.swift +++ b/apple/Tests/LogseqChatTests/LGChatRendererTests.swift @@ -85,7 +85,7 @@ struct LGChatRendererTests { try renderer.apply(patchJSON: """ {"generation":1,"ops":[ {"op":"create-node","id":1,"kind":"root"}, - {"op":"create-extension","id":2,"identifier":"outliner-block-content","fingerprint":"lui-extension-v1|22:outliner-block-content|profiles:android/flutter,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]"}, + {"op":"create-extension","id":2,"identifier":"outliner-block-content","fingerprint":"lui-extension-v1|22:outliner-block-content|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]"}, {"op":"set-extension-prop","id":2,"property":"block-id","value":"block-a"}, {"op":"set-extension-prop","id":2,"property":"title","value":"Project"}, {"op":"set-extension-prop","id":2,"property":"markup-json","value":"[]"}, diff --git a/logseq_chat.opam b/logseq_chat.opam index 97a02388..cf4041a3 100644 --- a/logseq_chat.opam +++ b/logseq_chat.opam @@ -31,7 +31,7 @@ pin-depends: [ ["melange-transit-core.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] ["melange-transit-native.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] ["melange-transit-melange.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] - ["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#a2d1cfa"] + ["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#264aa9f67da0d0d3e413f9dfe970785e7286f23c"] ["drive.dev" "git+https://github.com/logseq/drive.git#7f407b07f18e68596cb0dcb3ba1781cb0572a2bb"] ["ocaml-signal.0.1.0" "git+https://github.com/logseq/ocaml-signal.git#868c1459f865b4ba3b227eb440dba40f1c812899"] ["mldoc.dev" "git+https://github.com/logseq/mldoc.git#553dea6ed8694352527a189747f787365469c9cb"] diff --git a/shared/src/logseq_chat/core/host_update.mli b/shared/src/logseq_chat/core/host_update.mli index a72139f9..915fee57 100644 --- a/shared/src/logseq_chat/core/host_update.mli +++ b/shared/src/logseq_chat/core/host_update.mli @@ -1,4 +1,4 @@ -(** Decoding of host-initiated UI updates sent as JSON from Swift/Flutter. *) +(** Decoding of host-initiated UI updates sent as JSON from Swift/Kotlin. *) type host_composer_asset = { uuid : string diff --git a/shared/src/logseq_chat/native_bridge.ml b/shared/src/logseq_chat/native_bridge.ml index 6310110d..394c5b69 100644 --- a/shared/src/logseq_chat/native_bridge.ml +++ b/shared/src/logseq_chat/native_bridge.ml @@ -16,7 +16,13 @@ let operating_system platform_code = | 3 -> AndroidOS | _ -> GenericOS -let host_kind host_code = if host_code = 3 then FlutterHost else SwiftUIHost +(* Matches the host-code table in lui's components_bridge.ml. *) +let host_kind = function + | 1 -> WebHost + | 2 -> SwiftUIHost + | 4 -> KotlinHost + | 6 -> GPUIHost + | _ -> GenericHost let app () = match !current_app with diff --git a/shared/src/logseq_chat/view.ml b/shared/src/logseq_chat/view.ml index 2de96ae4..a039041d 100644 --- a/shared/src/logseq_chat/view.ml +++ b/shared/src/logseq_chat/view.ml @@ -39,7 +39,7 @@ let outliner_editor_schema () = let open Lui_protocol in let open Lui_extension in Lui_extension.component "outliner-editor" - [ profile IOS SwiftUIHost; profile AndroidOS FlutterHost ] + [ profile IOS SwiftUIHost; profile AndroidOS KotlinHost ] false [] [ Lui_extension.property "block-id" StringScalar true None; @@ -70,7 +70,7 @@ let outliner_block_content_schema () = let open Lui_protocol in let open Lui_extension in Lui_extension.component "outliner-block-content" - [ profile IOS SwiftUIHost; profile AndroidOS FlutterHost ] + [ profile IOS SwiftUIHost; profile AndroidOS KotlinHost ] false [] [ Lui_extension.property "title" StringScalar true None; @@ -100,7 +100,7 @@ let native_navigation_stack_schema () = let open Lui_protocol in let open Lui_extension in Lui_extension.component "native-navigation-stack" - [ profile IOS SwiftUIHost; profile AndroidOS FlutterHost ] + [ profile IOS SwiftUIHost; profile AndroidOS KotlinHost ] true [] [ Lui_extension.property "depth" IntScalar true None; @@ -120,7 +120,7 @@ let native_search_presentation_schema () = let open Lui_protocol in let open Lui_extension in Lui_extension.component "native-search-presentation" - [ profile IOS SwiftUIHost; profile AndroidOS FlutterHost ] + [ profile IOS SwiftUIHost; profile AndroidOS KotlinHost ] true [] [ Lui_extension.property "presented" BoolScalar true None; diff --git a/shared/src/logseq_chat/view_base.ml b/shared/src/logseq_chat/view_base.ml index 365ed284..baee1684 100644 --- a/shared/src/logseq_chat/view_base.ml +++ b/shared/src/logseq_chat/view_base.ml @@ -859,7 +859,7 @@ let outliner_row_sync_failed_ (row : Model.outline_row) = row.sync_status = Some "failed" let outliner_row_list_item_press_enabled_ host (row : Model.outline_row) = - host <> FlutterHost || row.is_asset || row.opens_as_page + host <> KotlinHost || row.is_asset || row.opens_as_page let outliner_tag_identifier (tag : Model.sidebar_page) = "button.block-tag." ^ tag.uuid @@ -881,7 +881,7 @@ let composer_assets_present_ (current : Model.chat_model) = (* Matches the platform image-asset policy: extensions the Apple backend can thumbnail through CGImageSource (including PDF and TIFF first pages). The - Flutter image codec cannot decode PDF or TIFF, so those fall back to the + Kotlin image codec cannot decode PDF or TIFF, so those fall back to the document tile there. *) let composer_asset_is_image (context : Lui_ui.ui_context) (asset : Model.composer_asset) = @@ -893,11 +893,11 @@ let composer_asset_is_image (context : Lui_ui.ui_context) (String.length asset.local_path - index - 1)) | _ -> "" in - let flutter_image_codecs = - Lui_ui.host context = Lui_protocol.FlutterHost + let kotlin_image_codecs = + Lui_ui.host context = Lui_protocol.KotlinHost in List.mem extension - (if flutter_image_codecs then + (if kotlin_image_codecs then [ "png"; "jpg"; "jpeg"; "gif"; "webp"; "bmp"; "wbmp"; "heic"; "heif" ; "avif" ] else @@ -1357,10 +1357,10 @@ let active_app_node_routes (current : Model.chat_model) = let active_search_node_routes (current : Model.chat_model) = active_route_only (search_node_routes current) -let flutter_app_root_visible_ (current : Model.chat_model) = +let kotlin_app_root_visible_ (current : Model.chat_model) = (not current.search_open) && active_app_node_routes current = [] -let flutter_search_root_visible_ (current : Model.chat_model) = +let kotlin_search_root_visible_ (current : Model.chat_model) = current.search_open && active_search_node_routes current = [] let authentication_screen_visible_ (current : Model.chat_model) = diff --git a/shared/src/logseq_chat/view_base.mli b/shared/src/logseq_chat/view_base.mli index d5aabaaa..9853b9fb 100644 --- a/shared/src/logseq_chat/view_base.mli +++ b/shared/src/logseq_chat/view_base.mli @@ -353,8 +353,8 @@ val active_route_only : 'a list -> 'a list val active_app_node_routes : Model.chat_model -> Model.node_projection list val active_search_node_routes : Model.chat_model -> Model.node_projection list -val flutter_app_root_visible_ : Model.chat_model -> bool -val flutter_search_root_visible_ : Model.chat_model -> bool +val kotlin_app_root_visible_ : Model.chat_model -> bool +val kotlin_search_root_visible_ : Model.chat_model -> bool val authentication_screen_visible_ : Model.chat_model -> bool val graph_picker_screen_visible_ : Model.chat_model -> bool val authentication_signing_in_ : Model.chat_model -> bool diff --git a/shared/src/logseq_chat/view_composer.ml b/shared/src/logseq_chat/view_composer.ml index 40a787ad..076dd201 100644 --- a/shared/src/logseq_chat/view_composer.ml +++ b/shared/src/logseq_chat/view_composer.ml @@ -168,7 +168,7 @@ let composer_send_button context ?send_icon send_disabled on_send : t = let android_icon = Option.value send_icon ~default:(`send : icon) in let apple_icon = Option.value send_icon ~default:(`arrow_up : icon) in if Lui_ui.platform context = Lui_protocol.AndroidOS then - if Lui_ui.host context = Lui_protocol.FlutterHost then + if Lui_ui.host context = Lui_protocol.KotlinHost then button ~icon:android_icon ~variant:`primary ~size:`icon ~width:48 ~height:48 ~label:"Send" ~accessibility_identifier:"button.send" ?disabled_signal:send_disabled ~on_press:on_send [] @@ -188,7 +188,7 @@ let composer_surface ?key ?accessibility_identifier ?attachments ?send_icon ?send_disabled_signal ?on_input ?on_submit ?on_send ?on_press () : t = fun context parent -> - let flutter = Lui_ui.host context = Lui_protocol.FlutterHost in + let kotlin = Lui_ui.host context = Lui_protocol.KotlinHost in let attachment_strip = match attachments with | None -> [] @@ -222,9 +222,9 @@ let composer_surface ?key ?accessibility_identifier ?attachments let capsule = column ?key ?accessibility_identifier ~grow:1.0 ~min_height:58 ~main:`end_ ~gap:0 - ~padding_horizontal:(if flutter then 12 else 16) - ~padding_vertical:(if flutter then 12 else 8) - ~background:(if flutter then "surface-container-high" else "glass") + ~padding_horizontal:(if kotlin then 12 else 16) + ~padding_vertical:(if kotlin then 12 else 8) + ~background:(if kotlin then "surface-container-high" else "glass") ~corner_radius:24 ([ box ~height:6 ~accessibility_identifier:"spacer.composer.top" [] ] @ attachment_strip @@ -266,10 +266,10 @@ let composer_view (context : Lui_ui.ui_context) model_source send : t = ~mount:(fun asset_source -> composer_asset_view context asset_source send)) ~attachments_visible_signal: - (reactive View_base.composer_assets_present_ model_source) + (Signal.map View_base.composer_assets_present_ model_source) ~actions: [ - (if Lui_ui.host context = FlutterHost then + (if Lui_ui.host context = KotlinHost then stack [ composer_attachment_button context send; @@ -295,7 +295,7 @@ let composer_view (context : Lui_ui.ui_context) model_source send : t = (if Lui_ui.platform context = AndroidOS then `app "send" else `app "arrow-up") ~send_disabled_signal: - (reactive View_base.composer_send_disabled_ model_source) + (Signal.map View_base.composer_send_disabled_ model_source) ~on_input:(on_input send (fun text -> Model.ChangeComposerDraft text)) ~on_submit:(press send Model.SendComposer) @@ -306,10 +306,10 @@ let composer_view (context : Lui_ui.ui_context) model_source send : t = ~test:(Signal.map View_base.composer_collapsed_ model_source) (Lui_element_combine.composer_collapsed ~label: - (if Lui_ui.host context = FlutterHost then "Capture a thought" + (if Lui_ui.host context = KotlinHost then "Capture a thought" else "Capture") ?icon: - (if Lui_ui.host context = FlutterHost then Some (`app "add") + (if Lui_ui.host context = KotlinHost then Some (`app "add") else None) ~accessibility_identifier:"button.composer.expand" ~on_press:(press send Model.ExpandComposer) ()); diff --git a/shared/src/logseq_chat/view_flashcards.ml b/shared/src/logseq_chat/view_flashcards.ml index 71c73b74..3957a312 100644 --- a/shared/src/logseq_chat/view_flashcards.ml +++ b/shared/src/logseq_chat/view_flashcards.ml @@ -32,7 +32,7 @@ let flashcard_review_content model_source send : t = spacer ~grow:1.0 []; text ~style_class:"footnote" ~foreground:"muted-foreground" ~value_signal: - (reactive View_base.flashcard_remaining_label + (Signal.map View_base.flashcard_remaining_label model_source) []; ]; @@ -43,7 +43,7 @@ let flashcard_review_content model_source send : t = [ text ~style_class:"title2 semibold" ~value_signal: - (reactive View_base.flashcard_question model_source) + (Signal.map View_base.flashcard_question model_source) ~accessibility_identifier:"flashcard.question" []; if_ ~test: diff --git a/shared/src/logseq_chat/view_graphs.ml b/shared/src/logseq_chat/view_graphs.ml index 1d2d7fa2..07ef6130 100644 --- a/shared/src/logseq_chat/view_graphs.ml +++ b/shared/src/logseq_chat/view_graphs.ml @@ -9,7 +9,7 @@ let graph_row (context : Lui_ui.ui_context) model_source graph_source ~accessibility_identifier:(View_base.graph_identifier graph) ~padding:16 ~corner_radius:16 ~background: - (if Lui_ui.host context = FlutterHost then "surface-container-low" + (if Lui_ui.host context = KotlinHost then "surface-container-low" else "surface") ~disabled_signal: (Signal.map2 View_base.graph_row_disabled_ model_source @@ -62,13 +62,13 @@ let graph_list_row (context : Lui_ui.ui_context) model_source graph_source (Signal.map (fun (current_graph : Model.graph) -> if - Lui_ui.host context <> FlutterHost + Lui_ui.host context <> KotlinHost && (not local) && current_graph.is_encrypted then "app:graph-locked" else View_base.graph_icon_name local current_graph) graph_source) (list_item - ~min_height:(if Lui_ui.host context = FlutterHost then 56 else 44) + ~min_height:(if Lui_ui.host context = KotlinHost then 56 else 44) ~accessibility_identifier:(View_base.graph_identifier graph) ~disabled_signal: (if local then @@ -100,7 +100,7 @@ let graph_list_row (context : Lui_ui.ui_context) model_source graph_source ~test: (Signal.map (fun (current_graph : Model.graph) -> - Lui_ui.host context = FlutterHost + Lui_ui.host context = KotlinHost && current_graph.is_encrypted) graph_source) (text ~style_class:"caption" @@ -127,7 +127,7 @@ let graph_list_row (context : Lui_ui.ui_context) model_source graph_source ]) let graph_create_sheet (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then sheet ~text:"Add sync graph" ~height:480 ~accessibility_identifier:"sheet.graph-create" ~on_dismiss:(press send Model.DismissCreateGraph) @@ -276,7 +276,7 @@ let graph_create_sheet (context : Lui_ui.ui_context) model_source send : t = let graph_delete_dialog (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then dialog ~text:"Delete local graph" ~height:320 ~accessibility_identifier:"dialog.graph-delete" ~on_dismiss:(press send Model.CancelDeleteGraph) @@ -349,15 +349,15 @@ let graph_delete_dialog (context : Lui_ui.ui_context) model_source send : t let graph_picker_overflow_menu send : t = fun context parent -> - let flutter_host = Lui_ui.host context = FlutterHost in + let kotlin_host = Lui_ui.host context = KotlinHost in let entries = [ menu_item ~text:"Settings" - ?icon:(if flutter_host then Some (`app "settings") else None) + ?icon:(if kotlin_host then Some (`app "settings") else None) ~on_press:(press send Model.OpenSettings) [] ] in - if flutter_host then + if kotlin_host then menu ~icon:(`app "more-vert") ~label:"More" ~accessibility_identifier:"button.overflow-menu" entries context parent else @@ -388,7 +388,7 @@ let graph_picker_error_banner model_source : t = let graph_password_sheet (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then sheet ~text:"Unlock encrypted graphs" ~height:360 ~accessibility_identifier:"sheet.graph-unlock" ~on_dismiss:(press send Model.CancelGraphUnlock) @@ -483,7 +483,7 @@ let graph_password_sheet (context : Lui_ui.ui_context) model_source send ] let graphs_screen (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then list ~accessibility_identifier:"screen.graphs" ~gap:0 [ row ~gap:12 ~padding:16 @@ -578,7 +578,7 @@ let graphs_screen (context : Lui_ui.ui_context) model_source send : t = graph_list_row context model_source graph_source false send); ] -let flutter_graph_picker_loading_state () : t = +let kotlin_graph_picker_loading_state () : t = column ~grow:1.0 ~main:`center ~cross:`center ~gap:12 ~accessibility_identifier:"loading.graph-picker" [ @@ -587,7 +587,7 @@ let flutter_graph_picker_loading_state () : t = ~value:"Loading sync graphs\xE2\x80\xA6" []; ] -let flutter_graph_picker_empty_state send : t = +let kotlin_graph_picker_empty_state send : t = column ~grow:1.0 ~main:`center ~cross:`stretch [ column ~cross:`center ~gap:16 @@ -613,7 +613,7 @@ let flutter_graph_picker_empty_state send : t = ]; ] -let flutter_graph_picker_catalog (context : Lui_ui.ui_context) model_source +let kotlin_graph_picker_catalog (context : Lui_ui.ui_context) model_source send : t = column ~grow:1.0 ~gap:16 [ @@ -636,7 +636,7 @@ let flutter_graph_picker_catalog (context : Lui_ui.ui_context) model_source let graph_picker_screen (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~accessibility_identifier:"screen.graph-picker" ~main:`start ~cross:`stretch ~grow:1.0 ~gap:16 ~padding:24 [ @@ -656,14 +656,14 @@ let graph_picker_screen (context : Lui_ui.ui_context) model_source send : t (graph_picker_error_banner model_source); if_ ~test:(Signal.map View_base.empty_graphs_loading_ model_source) - (flutter_graph_picker_loading_state ()); + (kotlin_graph_picker_loading_state ()); if_ ~test: (Signal.map View_base.empty_graphs_refreshable_ model_source) - (flutter_graph_picker_empty_state send); + (kotlin_graph_picker_empty_state send); if_ ~test:(Signal.map View_base.graphs_present_ model_source) - (flutter_graph_picker_catalog context model_source send); + (kotlin_graph_picker_catalog context model_source send); ] else column ~accessibility_identifier:"screen.graph-picker" ~main:`start diff --git a/shared/src/logseq_chat/view_graphs.mli b/shared/src/logseq_chat/view_graphs.mli index cfbf1885..10f16e06 100644 --- a/shared/src/logseq_chat/view_graphs.mli +++ b/shared/src/logseq_chat/view_graphs.mli @@ -28,10 +28,10 @@ val graphs_screen : Lui_ui.ui_context -> Model.chat_model Signal.signal -> (Model.chat_action -> bool) -> Lui_elements.t -val flutter_graph_picker_loading_state : unit -> Lui_elements.t -val flutter_graph_picker_empty_state : +val kotlin_graph_picker_loading_state : unit -> Lui_elements.t +val kotlin_graph_picker_empty_state : (Model.chat_action -> bool) -> Lui_elements.t -val flutter_graph_picker_catalog : +val kotlin_graph_picker_catalog : Lui_ui.ui_context -> Model.chat_model Signal.signal -> (Model.chat_action -> bool) -> Lui_elements.t diff --git a/shared/src/logseq_chat/view_outliner.ml b/shared/src/logseq_chat/view_outliner.ml index f44cd5e7..80b8724b 100644 --- a/shared/src/logseq_chat/view_outliner.ml +++ b/shared/src/logseq_chat/view_outliner.ml @@ -7,11 +7,11 @@ let outliner_editor_view block_id_source title_source caret_source send : t let node = Lui_ui.extension context "outliner-editor" in attach context parent node; Lui_ui.extension_property_signal context node "block-id" - (reactive View_base.string_wire_value block_id_source); + (Signal.map View_base.string_wire_value block_id_source); Lui_ui.extension_property_signal context node "title" - (reactive View_base.string_wire_value title_source); + (Signal.map View_base.string_wire_value title_source); Lui_ui.extension_property_signal context node "caret-utf16-offset" - (reactive View_base.int_wire_value caret_source); + (Signal.map View_base.int_wire_value caret_source); Lui_ui.on_event context node (fun input_event -> ignore (View_base.handle_outliner_editor_event input_event @@ -25,21 +25,21 @@ let outliner_rich_block_view model_source title_source markup_source let node = Lui_ui.extension context "outliner-block-content" in attach context parent node; Lui_ui.extension_property_signal context node "block-id" - (reactive View_base.outliner_row_id_wire_value row_source); + (Signal.map View_base.outliner_row_id_wire_value row_source); Lui_ui.extension_property_signal context node "title" - (reactive View_base.string_wire_value title_source); + (Signal.map View_base.string_wire_value title_source); Lui_ui.extension_property_signal context node "markup-json" - (reactive View_base.string_wire_value markup_source); + (Signal.map View_base.string_wire_value markup_source); Lui_ui.extension_property_signal context node "youtube-target-url" - (reactive View_base.string_wire_value youtube_target_source); + (Signal.map View_base.string_wire_value youtube_target_source); Lui_ui.extension_property_signal context node "is-asset" - (reactive View_base.bool_wire_value is_asset_source); + (Signal.map View_base.bool_wire_value is_asset_source); Lui_ui.extension_property_signal context node "is-completed" - (reactive View_base.outliner_row_completed_wire_value row_source); + (Signal.map View_base.outliner_row_completed_wire_value row_source); Lui_ui.extension_property_signal context node "asset-type" - (reactive View_base.string_wire_value asset_type_source); + (Signal.map View_base.string_wire_value asset_type_source); Lui_ui.extension_property_signal context node "local-path" - (reactive View_base.string_wire_value local_path_source); + (Signal.map View_base.string_wire_value local_path_source); Lui_ui.on_event context node (fun input_event -> ignore (View_base.handle_outliner_block_content_event input_event @@ -49,9 +49,9 @@ let outliner_rich_block_view model_source title_source markup_source let outliner_collapse_button (context : Lui_ui.ui_context) row_source send : t = let current_row = Signal.sample row_source in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then View_base.with_label_signal - (reactive View_base.outliner_row_collapse_label row_source) + (Signal.map View_base.outliner_row_collapse_label row_source) (View_base.with_string_prop_signal InlineIconName (Signal.map (fun (current_row : Model.outline_row) -> @@ -69,7 +69,7 @@ let outliner_collapse_button (context : Lui_ui.ui_context) row_source send : [])) else View_base.with_label_signal - (reactive View_base.outliner_row_collapse_label row_source) + (Signal.map View_base.outliner_row_collapse_label row_source) (View_base.with_string_prop_signal InlineIconName (Signal.map (fun (current_row : Model.outline_row) -> @@ -140,7 +140,7 @@ let related_row_breadcrumbs row_source send : t = let outliner_tag tag_source send : t = let tag = Signal.sample tag_source in View_base.with_label_signal - (reactive View_base.outliner_tag_title tag_source) + (Signal.map View_base.outliner_tag_title tag_source) (button ~text_signal:(reactive View_base.outliner_tag_title tag_source) ~style_class:"caption" ~variant:`ghost ~foreground:"accent" @@ -157,7 +157,7 @@ let outliner_zoom_control (context : Lui_ui.ui_context) model_source let current_row = Signal.sample row_source in if (not search_open) && View_base.outliner_row_journal_ current current_row then - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then row ~width:24 ~height:24 ~main:`center ~cross:`center [ box ~width:7 ~height:7 ~corner_radius:4 ~background:"border" @@ -170,9 +170,9 @@ let outliner_zoom_control (context : Lui_ui.ui_context) model_source icon ~name:(`app "outliner-bullet") ~style_class:"body-line" ~foreground:"border" ~width:24 ~height:24 ~accessibility_identifier_signal: - (reactive View_base.outliner_row_zoom_identifier row_source) + (Signal.map View_base.outliner_row_zoom_identifier row_source) [] - else if Lui_ui.host context = FlutterHost then + else if Lui_ui.host context = KotlinHost then stack ~width:24 ~height:24 [ row ~width:24 ~height:24 ~main:`center ~cross:`center @@ -184,10 +184,10 @@ let outliner_zoom_control (context : Lui_ui.ui_context) model_source []; ]; View_base.with_label_signal - (reactive View_base.outliner_row_zoom_label row_source) + (Signal.map View_base.outliner_row_zoom_label row_source) (button ~variant:`ghost ~width:24 ~height:24 ~accessibility_identifier_signal: - (reactive View_base.outliner_row_zoom_identifier + (Signal.map View_base.outliner_row_zoom_identifier row_source) ~on_press:(fun _ -> ignore @@ -202,12 +202,12 @@ let outliner_zoom_control (context : Lui_ui.ui_context) model_source ] else View_base.with_label_signal - (reactive View_base.outliner_row_zoom_label row_source) + (Signal.map View_base.outliner_row_zoom_label row_source) (button ~icon:(`app "outliner-bullet") ~size:`icon ~style_class:"body-line" ~variant:`ghost ~foreground:"border" ~width:24 ~height:24 ~accessibility_identifier_signal: - (reactive View_base.outliner_row_zoom_identifier row_source) + (Signal.map View_base.outliner_row_zoom_identifier row_source) ~on_press:(fun _ -> ignore (send @@ -238,7 +238,7 @@ let outliner_status_control (context : Lui_ui.ui_context) model_source (View_base.outliner_row_uuid current_row) status_source send); ] in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack ~width:24 ~height:24 [ row ~width:24 ~height:24 ~main:`center ~cross:`center @@ -278,16 +278,16 @@ let outliner_status_control (context : Lui_ui.ui_context) model_source ]; ]; View_base.with_label_signal - (reactive View_base.outliner_row_status_title row_source) + (Signal.map View_base.outliner_row_status_title row_source) (button ~variant:`ghost ~size:`icon ~width:24 ~height:24 ~accessibility_identifier:"button.block-task-status" [ status_menu ]); ] else View_base.with_label_signal - (reactive View_base.outliner_row_status_title row_source) + (Signal.map View_base.outliner_row_status_title row_source) (View_base.with_string_prop_signal InlineIconName - (reactive View_base.outliner_task_status_icon row_source) + (Signal.map View_base.outliner_task_status_icon row_source) (button ~variant:`ghost ~style_class:"body-line" ~foreground:"secondary" ~size:`icon ~width:22 ~height:24 ~accessibility_identifier:"button.block-task-status" @@ -297,32 +297,32 @@ let outliner_row_main_content (context : Lui_ui.ui_context) model_source retained_row_source row_source send : t = let current_row = Signal.sample row_source in let block_id_source = - reactive View_base.outliner_row_uuid row_source + Signal.map View_base.outliner_row_uuid row_source in - let title_source = reactive View_base.outliner_row_title row_source in + let title_source = Signal.map View_base.outliner_row_title row_source in let markup_source = - reactive (fun (r : Model.outline_row) -> r.markup_json) row_source + Signal.map (fun (r : Model.outline_row) -> r.markup_json) row_source in let youtube_target_source = - reactive View_base.outliner_row_youtube_target row_source + Signal.map View_base.outliner_row_youtube_target row_source in let is_asset_source = - reactive (fun (r : Model.outline_row) -> r.is_asset) row_source + Signal.map (fun (r : Model.outline_row) -> r.is_asset) row_source in let asset_type_source = - reactive View_base.outliner_row_asset_type row_source + Signal.map View_base.outliner_row_asset_type row_source in let local_path_source = - reactive View_base.outliner_row_local_path row_source + Signal.map View_base.outliner_row_local_path row_source in let editing_title_source = - reactive View_rows.retained_row_editing_title retained_row_source + Signal.map View_rows.retained_row_editing_title retained_row_source in let editing_caret_source = - reactive View_rows.retained_row_editing_caret retained_row_source + Signal.map View_rows.retained_row_editing_caret retained_row_source in let editing_source = - reactive View_rows.retained_row_editing_ retained_row_source + Signal.map View_rows.retained_row_editing_ retained_row_source in let not_editing_source = Signal.map2 @@ -331,16 +331,16 @@ let outliner_row_main_content (context : Lui_ui.ui_context) model_source model_source row_source in let has_children_source = - reactive View_base.outliner_row_has_children row_source + Signal.map View_base.outliner_row_has_children row_source in let has_status_source = - reactive View_base.outliner_row_has_status_ row_source + Signal.map View_base.outliner_row_has_status_ row_source in let has_tags_source = - reactive View_base.outliner_row_has_tags_ row_source + Signal.map View_base.outliner_row_has_tags_ row_source in let sync_failed_source = - reactive View_base.outliner_row_sync_failed_ row_source + Signal.map View_base.outliner_row_sync_failed_ row_source in row ~gap:7 ~cross:`start ~grow:1.0 [ @@ -382,7 +382,7 @@ let outliner_row_content context model_source retained_row_source row ~gap:0 ~cross:`start ~padding_vertical:5 [ outliner_indent_view - (reactive View_base.outliner_row_indent row_source); + (Signal.map View_base.outliner_row_indent row_source); outliner_zoom_control context model_source row_source send search_open; box ~width:2 []; @@ -407,13 +407,13 @@ let outliner_row (context : Lui_ui.ui_context) model_source View_base.with_bool_prop_signal Selected selected_source (box ~accessibility_identifier_signal: - (reactive View_base.outliner_row_identifier row_source) + (Signal.map View_base.outliner_row_identifier row_source) ~padding:0 ~corner_radius:10 [ row ~gap:0 ~cross:`start ~padding_vertical:5 [ outliner_indent_view - (reactive View_base.outliner_row_indent row_source); + (Signal.map View_base.outliner_row_indent row_source); outliner_zoom_control context model_source row_source send search_open; box ~width:2 []; @@ -426,7 +426,7 @@ let outliner_row (context : Lui_ui.ui_context) model_source model_source row_source) (list_item ~accessibility_identifier_signal: - (reactive View_base.outliner_row_action_identifier + (Signal.map View_base.outliner_row_action_identifier row_source) ~padding:0 ~grow:1.0 ~on_press:(fun _ -> @@ -465,7 +465,7 @@ let outliner_row (context : Lui_ui.ui_context) model_source model_source row_source) (box ~accessibility_identifier_signal: - (reactive View_base.outliner_row_identifier row_source) + (Signal.map View_base.outliner_row_identifier row_source) ~padding:0 ~corner_radius:10 [ outliner_row_content context model_source @@ -516,7 +516,7 @@ let outliner_entry context model_source retained_row_source row_source let outliner_first_journal_section (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~accessibility_identifier: (View_rows.first_journal_section_identifier @@ -528,7 +528,7 @@ let outliner_first_journal_section (context : Lui_ui.ui_context) ~key:View_rows.retained_row_identifier ~cmp:compare ~mount:(fun retained_row_source -> outliner_entry context model_source retained_row_source - (reactive View_rows.retained_row_value retained_row_source) + (Signal.map View_rows.retained_row_value retained_row_source) send); ] else @@ -544,7 +544,7 @@ let outliner_first_journal_section (context : Lui_ui.ui_context) ~key:View_rows.retained_row_identifier ~cmp:compare ~mount:(fun retained_row_source -> outliner_entry context model_source retained_row_source - (reactive View_rows.retained_row_value retained_row_source) + (Signal.map View_rows.retained_row_value retained_row_source) send); ] @@ -556,7 +556,7 @@ let toolbar_button icon label identifier action send : t = [] let outliner_selection_toolbar (context : Lui_ui.ui_context) send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then box ~height:56 ~padding_horizontal:8 ~padding_vertical:4 ~background:"surface-container-high" ~corner_radius:20 ~accessibility_identifier:"surface.outliner.selection-toolbar" @@ -622,12 +622,12 @@ let outliner_autocomplete_row (context : Lui_ui.ui_context) candidate_source send : t = let candidate = Signal.sample candidate_source in View_base.with_label_signal - (reactive View_base.outliner_autocomplete_label candidate_source) + (Signal.map View_base.outliner_autocomplete_label candidate_source) (button ~text_signal: - (reactive View_base.outliner_autocomplete_label candidate_source) + (Signal.map View_base.outliner_autocomplete_label candidate_source) ~variant:`ghost - ~grow:(if Lui_ui.host context = FlutterHost then 0.0 else 1.0) + ~grow:(if Lui_ui.host context = KotlinHost then 0.0 else 1.0) ~height:44 ~padding_horizontal:10 ~background:"autocomplete-row-background" ~foreground:"foreground" ~corner_radius:8 ~text_alignment:`start @@ -648,8 +648,8 @@ let outliner_autocomplete_bar (context : Lui_ui.ui_context) model_source [ column ~gap:2 ~padding:8 ~cross: - (if Lui_ui.host context = FlutterHost then `stretch else `center) - ~grow:(if Lui_ui.host context = FlutterHost then 0.0 else 1.0) + (if Lui_ui.host context = KotlinHost then `stretch else `center) + ~grow:(if Lui_ui.host context = KotlinHost then 0.0 else 1.0) [ keyed ~source: @@ -665,7 +665,7 @@ let outliner_autocomplete_bar (context : Lui_ui.ui_context) model_source let outliner_editor_toolbar (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then box ~height:56 ~padding_horizontal:8 ~padding_vertical:4 ~accessibility_identifier:"surface.outliner.editor-toolbar" [ @@ -674,7 +674,7 @@ let outliner_editor_toolbar (context : Lui_ui.ui_context) model_source send ~style_class:"scroll-leading" ~toolbar_gap:4 [ View_base.with_label_signal - (reactive View_base.outliner_editor_task_label model_source) + (Signal.map View_base.outliner_editor_task_label model_source) (button ~icon:(`app "toolbar-task") ~variant:`ghost ~size:`icon ~width:48 ~height:48 ~foreground:"muted-foreground" @@ -727,7 +727,7 @@ let outliner_editor_toolbar (context : Lui_ui.ui_context) model_source send ~toolbar_gap:4 ~placement:"bottom" [ View_base.with_label_signal - (reactive View_base.outliner_editor_task_label model_source) + (Signal.map View_base.outliner_editor_task_label model_source) (apple_icon_button (`app "toolbar-task") "" "button.outliner.editor.task" "task"); apple_icon_button (`app "toolbar-outdent") "Outdent" @@ -763,10 +763,10 @@ let outliner_editor_toolbar (context : Lui_ui.ui_context) model_source send let node_related_row context model_source row_source send : t = let structured_breadcrumb_source = - reactive View_base.outliner_row_structured_breadcrumb_ row_source + Signal.map View_base.outliner_row_structured_breadcrumb_ row_source in let fallback_breadcrumb_source = - reactive View_base.outliner_row_fallback_breadcrumb_ row_source + Signal.map View_base.outliner_row_fallback_breadcrumb_ row_source in let retained_row_source = Signal.map2 @@ -785,7 +785,7 @@ let node_related_row context model_source row_source send : t = box ~height:10 []; text ~value_signal: - (reactive View_base.outliner_row_breadcrumb row_source) + (Signal.map View_base.outliner_row_breadcrumb row_source) ~style_class:"caption" ~foreground:"muted-foreground" []; ]); outliner_row context model_source retained_row_source row_source @@ -872,7 +872,7 @@ let add_first_block_button model_source send : t = let node_screen (context : Lui_ui.ui_context) model_source send : t = column ~accessibility_identifier:"screen.node" - ~grow:(if Lui_ui.host context = FlutterHost then 1.0 else 0.0) + ~grow:(if Lui_ui.host context = KotlinHost then 1.0 else 0.0) [ scroll ~grow:1.0 ~accessibility_identifier:"scroll.outliner" [ @@ -895,7 +895,7 @@ let node_screen (context : Lui_ui.ui_context) model_source send : t = [ heading ~level:3 ~value_signal: - (reactive View_base.active_node_title + (Signal.map View_base.active_node_title model_source) ~accessibility_identifier:"title.node" []; ]; @@ -915,7 +915,7 @@ let node_screen (context : Lui_ui.ui_context) model_source send : t = ~mount:(fun retained_row_source -> outliner_row context model_source retained_row_source - (reactive View_rows.retained_row_value + (Signal.map View_rows.retained_row_value retained_row_source) send); ]); diff --git a/shared/src/logseq_chat/view_screens.ml b/shared/src/logseq_chat/view_screens.ml index 95783bbb..dfeccaec 100644 --- a/shared/src/logseq_chat/view_screens.ml +++ b/shared/src/logseq_chat/view_screens.ml @@ -147,7 +147,7 @@ let main_bottom_chrome (context : Lui_ui.ui_context) model_source send : t box ~height:21 []; ] in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack [ if_ @@ -320,7 +320,7 @@ let root_outliner_view (context : Lui_ui.ui_context) model_source visible_source let retained_journal_pane (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack ~grow:1.0 [ if_ @@ -354,7 +354,7 @@ let retained_journal_pane (context : Lui_ui.ui_context) model_source send let journal_tree_panes (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack ~grow:1.0 [ retained_journal_pane context model_source send; @@ -389,7 +389,7 @@ let journal_tree_panes (context : Lui_ui.ui_context) model_source send : t let global_effect_error_feedback (context : Lui_ui.ui_context) model_source : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then alert ~variant:`destructive ~padding:14 ~corner_radius:16 ~border_width:0 ~accessibility_identifier:"layout.error.banner" @@ -427,7 +427,7 @@ let global_effect_error_feedback (context : Lui_ui.ui_context) let graph_loading_feedback (context : Lui_ui.ui_context) model_source : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~grow:1.0 ~main:`center ~cross:`center ~gap:12 ~background:"background" ~accessibility_identifier:"journals.loading" @@ -480,7 +480,7 @@ let main_header_leading (context : Lui_ui.ui_context) model_source send : ~test: (Signal.map (fun (current : Model.chat_model) -> - Lui_ui.host context = FlutterHost + Lui_ui.host context = KotlinHost && View_base.node_screen_visible_ current) model_source) (button ~icon:(`app "navigation-back") ~variant:`ghost ~size:`icon @@ -504,7 +504,7 @@ let main_header_leading (context : Lui_ui.ui_context) model_source send : ] let main_header_title (context : Lui_ui.ui_context) model_source : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then heading ~value_signal:(Signal.map View_base.main_title model_source) ~level:3 ~accessibility_identifier:"title.main" [] @@ -523,7 +523,7 @@ let main_header_sync (context : Lui_ui.ui_context) model_source send : t = (Signal.map View_base.sync_indicator_label model_source) (button ~icon: - (if Lui_ui.host context = FlutterHost then + (if Lui_ui.host context = KotlinHost then `app "sync-status" else `app "status-dot") ~variant:`ghost ~size:`icon @@ -545,15 +545,15 @@ let active_overflow_menu model_source send : t = let settings_source = Signal.map View_base.connection_settings_visible_ model_source in - let flutter_host = Lui_ui.host context = FlutterHost in - (* iOS menu items carry no icons; the Flutter popup did. *) - let item_icon name = if flutter_host then Some (`app name) else None in + let kotlin_host = Lui_ui.host context = KotlinHost in + (* iOS menu items carry no icons; the Kotlin popup did. *) + let item_icon name = if kotlin_host then Some (`app name) else None in let entries = [ if_ ~test:page_actions_source - (if flutter_host then + (if kotlin_host then menu_item ~text_signal: - (reactive View_base.active_page_favorite_label model_source) + (Signal.map View_base.active_page_favorite_label model_source) ~icon_signal: (Signal.map (fun current -> @@ -568,7 +568,7 @@ let active_overflow_menu model_source send : t = else menu_item ~text_signal: - (reactive View_base.active_page_favorite_label model_source) + (Signal.map View_base.active_page_favorite_label model_source) ~on_press:(press send Model.ToggleActivePageFavorite) []); if_ ~test:page_actions_source @@ -586,7 +586,7 @@ let active_overflow_menu model_source send : t = []); ] in - if flutter_host then + if kotlin_host then menu ~icon:(`app "more-vert") ~label:"More" ~accessibility_identifier:"button.overflow-menu" entries context parent else @@ -611,7 +611,7 @@ let native_node_screen (context : Lui_ui.ui_context) model_source let route_model_source = Signal.map2 View_base.node_route_model model_source route_source in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then box ~grow:1.0 ~background:"background" [ View_outliner.node_screen context route_model_source send ] else View_outliner.node_screen context route_model_source send @@ -629,21 +629,21 @@ let native_search_view (_context : Lui_ui.ui_context) model_source send : in let query_source = Signal.map View_base.model_search_query model_source in Lui_ui.extension_property_signal context node "presented" - (reactive View_base.bool_wire_value presented_source); + (Signal.map View_base.bool_wire_value presented_source); Lui_ui.extension_property_signal context node "depth" - (reactive View_base.int_wire_value depth_source); + (Signal.map View_base.int_wire_value depth_source); Lui_ui.extension_property_signal context node "query" - (reactive View_base.string_wire_value query_source); + (Signal.map View_base.string_wire_value query_source); Lui_ui.extension_property context node "title" (StringValue "Search"); Lui_ui.on_event context node (fun input_event -> ignore (View_base.handle_native_search_event input_event send)); - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then let _ = stack ~grow:1.0 [ if_ ~test: - (Signal.map View_base.flutter_app_root_visible_ model_source) + (Signal.map View_base.kotlin_app_root_visible_ model_source) (chat_main_view context model_source send); keyed ~source: @@ -653,7 +653,7 @@ let native_search_view (_context : Lui_ui.ui_context) model_source send : native_node_screen context model_source route_source send); if_ ~test: - (Signal.map View_base.flutter_search_root_visible_ + (Signal.map View_base.kotlin_search_root_visible_ model_source) (column ~grow:1.0 ~background:"background" [ search_screen model_source send ]); @@ -701,19 +701,19 @@ let native_navigation_view (_context : Lui_ui.ui_context) model_source send in let title_source = Signal.map View_base.main_title model_source in Lui_ui.extension_property_signal context node "depth" - (reactive View_base.int_wire_value depth_source); + (Signal.map View_base.int_wire_value depth_source); Lui_ui.extension_property_signal context node "bottom-occupies-layout-space" - (reactive View_base.bool_wire_value bottom_occupies_source); + (Signal.map View_base.bool_wire_value bottom_occupies_source); Lui_ui.extension_property_signal context node "composer-dismissal-enabled" - (reactive View_base.bool_wire_value composer_dismissal_source); + (Signal.map View_base.bool_wire_value composer_dismissal_source); Lui_ui.extension_property_signal context node "title" - (reactive View_base.string_wire_value title_source); + (Signal.map View_base.string_wire_value title_source); Lui_ui.on_event context node (fun input_event -> ignore (View_base.handle_native_navigation_event input_event send)); - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then let _ = column ~grow:1.0 [ @@ -821,7 +821,7 @@ let authentication_content model_source send : t = let authentication_screen (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~accessibility_identifier:"screen.authentication" ~grow:1.0 ~main:`center ~cross:`stretch ~padding:32 [ authentication_content model_source send ] @@ -914,7 +914,7 @@ let application_main_content (context : Lui_ui.ui_context) model_source in let stack_grow = Lui_ui.platform context = AndroidOS in if stack_grow then - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack ~grow:1.0 content_children else stack ~grow:1.0 ~container_relative_frame:`vertical @@ -925,7 +925,7 @@ let chat_view (context : Lui_ui.ui_context) model_source send : t = let drawer_main = application_main_content context model_source send in let drawer_panel = View_sidebar.sidebar_view context model_source send in if Lui_ui.platform context = AndroidOS then - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack ~grow:1.0 [ if_ diff --git a/shared/src/logseq_chat/view_settings.ml b/shared/src/logseq_chat/view_settings.ml index cacac0b6..b7416257 100644 --- a/shared/src/logseq_chat/view_settings.ml +++ b/shared/src/logseq_chat/view_settings.ml @@ -30,7 +30,7 @@ let settings_language_choice_menu_item choice_source send : t = let settings_language_control (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack ~accessibility_identifier:"layout.settings.language-control" [ @@ -91,7 +91,7 @@ let settings_language_control (context : Lui_ui.ui_context) model_source let settings_appearance_control (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then let appearance_item id title : t = menu_item ~text:title ~selected_signal: @@ -171,7 +171,7 @@ let settings_appearance_control (context : Lui_ui.ui_context) model_source let settings_community_link_row (context : Lui_ui.ui_context) model_source link_source send : t = let link = Signal.sample link_source in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~gap:12 [ list_item @@ -290,7 +290,7 @@ let settings_tab_row (context : Lui_ui.ui_context) model_source tab title View_base.tab_selection_icon_name current tab) model_source in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then row ~cross:`center ~padding:8 ~background:"surface-container-low" ~corner_radius:12 ~accessibility_identifier:("row.settings.tab." ^ tab) @@ -363,7 +363,7 @@ let settings_tab_row (context : Lui_ui.ui_context) model_source tab title let settings_tabs_screen (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~grow:1.0 ~cross:`stretch ~gap:10 ~padding:16 ~accessibility_identifier:"screen.settings.tabs" [ @@ -454,7 +454,7 @@ let runtime_log_toolbar (context : Lui_ui.ui_context) model_source send : t ~on_press:(press send action) []) in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~gap:8 ~cross:`stretch [ toolbar ~orientation:`horizontal ~toolbar_gap:8 @@ -542,7 +542,7 @@ let settings_toggle_auto_correction send input_event = let settings_general_card (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~gap:12 ~cross:`stretch ~padding:16 ~background:"surface-container-low" ~corner_radius:14 ~accessibility_identifier:"layout.settings.general-card" @@ -615,7 +615,7 @@ let settings_general_card (context : Lui_ui.ui_context) model_source send : let settings_editor_card (context : Lui_ui.ui_context) spell_check_source auto_correction_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then column ~gap:0 ~cross:`stretch ~padding:16 ~background:"surface-container-low" ~corner_radius:14 [ @@ -642,7 +642,7 @@ let settings_editor_card (context : Lui_ui.ui_context) spell_check_source ] let settings_export_row (context : Lui_ui.ui_context) send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then list_item ~icon:(`app "download") ~padding:0 ~accessibility_identifier:"button.export-graph-database" ~on_press:(press send Model.ExportGraphDatabase) @@ -654,7 +654,7 @@ let settings_export_row (context : Lui_ui.ui_context) send : t = ~text:"Export Graph SQLite DB" [] let settings_runtime_log_row (context : Lui_ui.ui_context) send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then list_item ~icon:(`app "terminal") ~padding:0 ~accessibility_identifier:"button.runtime-log" ~on_press:(press send Model.OpenRuntimeLog) @@ -665,7 +665,7 @@ let settings_runtime_log_row (context : Lui_ui.ui_context) send : t = ~text:"Check log" [] let settings_sign_out_row (context : Lui_ui.ui_context) send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then list_item ~icon:(`app "sign-out") ~padding:0 ~accessibility_identifier:"button.sign-out" ~on_press:(press send Model.RequestSignOut) @@ -676,7 +676,7 @@ let settings_sign_out_row (context : Lui_ui.ui_context) send : t = ~text:"Sign Out" [] let sign_out_dialog (context : Lui_ui.ui_context) send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then dialog ~text:"Sign out?" ~height:280 ~accessibility_identifier:"dialog.sign-out" ~on_dismiss:(press send Model.CancelSignOut) @@ -729,10 +729,10 @@ let sign_out_dialog (context : Lui_ui.ui_context) send : t = ~text:"Sign Out" []; ] -let settings_flutter_screen (context : Lui_ui.ui_context) model_source +let settings_kotlin_screen (context : Lui_ui.ui_context) model_source send : t = let card_bg = - if Lui_ui.host context = FlutterHost then "surface-container-low" + if Lui_ui.host context = KotlinHost then "surface-container-low" else "surface" in column ~gap:18 ~padding:20 ~background:"background" @@ -958,13 +958,13 @@ let settings_form_screen (context : Lui_ui.ui_context) model_source send ] let settings_screen (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then - settings_flutter_screen context model_source send + if Lui_ui.host context = KotlinHost then + settings_kotlin_screen context model_source send else settings_form_screen context model_source send let settings_tabs_sheet (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then sheet ~text:"Tabs" ~style_class:"navigation-content" ~accessibility_identifier:"sheet.settings" ~on_dismiss:(press send Model.BackSettings) @@ -1002,7 +1002,7 @@ let settings_tabs_sheet (context : Lui_ui.ui_context) model_source send : ] let runtime_log_actions (context : Lui_ui.ui_context) send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then row ~gap:12 ~padding_horizontal:16 ~padding_vertical:12 ~background:"surface-container-low" ~accessibility_identifier:"toolbar.settings.actions" @@ -1032,7 +1032,7 @@ let runtime_log_actions (context : Lui_ui.ui_context) send : t = ] let runtime_log_sheet (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then sheet ~text:"Log" ~style_class:"navigation-content" ~accessibility_identifier:"sheet.settings" ~on_dismiss:(press send Model.DismissRuntimeLog) @@ -1055,7 +1055,7 @@ let runtime_log_sheet (context : Lui_ui.ui_context) model_source send : t = let settings_main_sheet (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then sheet ~text:"Settings" ~height:640 ~accessibility_identifier:"sheet.settings" ~on_dismiss:(press send Model.DismissSettings) @@ -1113,7 +1113,7 @@ let settings_main_sheet (context : Lui_ui.ui_context) model_source send : ] let settings_sheet (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then stack [ if_ @@ -1129,7 +1129,7 @@ let settings_sheet (context : Lui_ui.ui_context) model_source send : t = else settings_main_sheet context model_source send let page_delete_dialog (context : Lui_ui.ui_context) send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then dialog ~text:"Delete page?" ~height:280 ~accessibility_identifier:"dialog.page-delete" ~on_dismiss:(press send Model.CancelDeleteActivePage) @@ -1187,7 +1187,7 @@ let sync_status_sheet (context : Lui_ui.ui_context) model_source send : t = ]; ] in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then sheet ~text:"Sync status" ~height:420 ~accessibility_identifier:"sheet.sync-status" ~on_dismiss:(press send Model.CloseSyncDetails) diff --git a/shared/src/logseq_chat/view_sidebar.ml b/shared/src/logseq_chat/view_sidebar.ml index 2be144d7..e8ef14a0 100644 --- a/shared/src/logseq_chat/view_sidebar.ml +++ b/shared/src/logseq_chat/view_sidebar.ml @@ -4,9 +4,9 @@ open Lui_elements let sidebar_page_row (context : Lui_ui.ui_context) model_source page_source send : t = let page = sample page_source in - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then View_base.with_label_signal - (reactive View_base.sidebar_page_title page_source) + (Signal.map View_base.sidebar_page_title page_source) (list_item ~text:(reactive View_base.sidebar_page_title page_source) ~icon:(`app "document") @@ -17,7 +17,7 @@ let sidebar_page_row (context : Lui_ui.ui_context) model_source page_source send []) else View_base.with_label_signal - (reactive View_base.sidebar_page_title page_source) + (Signal.map View_base.sidebar_page_title page_source) (list_item ~text:(reactive View_base.sidebar_page_title page_source) ~role:`navigation ~icon:(`app "document") @@ -77,7 +77,7 @@ let sidebar_graph_switch_content model_source : t = ] let sidebar_graph_switch (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then View_base.with_label "Switch graph" (list_item ~accessibility_identifier:"button.graph-switch" @@ -94,7 +94,7 @@ let sidebar_graph_switch (context : Lui_ui.ui_context) model_source send : t = []) let sidebar_journals_row (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then View_base.with_label "Journals" (list_item ~icon:(`app "calendar") ~selected:(reactive View_base.journals_sidebar_selected_ model_source) @@ -110,7 +110,7 @@ let sidebar_journals_row (context : Lui_ui.ui_context) model_source send : t = ~text:"Journals" []) let sidebar_flashcards_row (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then View_base.with_label "Flashcards" (list_item ~icon:(`app "flashcards") ~selected:(reactive View_base.flashcards_sidebar_selected_ model_source) @@ -126,7 +126,7 @@ let sidebar_flashcards_row (context : Lui_ui.ui_context) model_source send : t = ~text:"Flashcards" []) let sidebar_graphs_row (context : Lui_ui.ui_context) model_source send : t = - if Lui_ui.host context = FlutterHost then + if Lui_ui.host context = KotlinHost then View_base.with_label "Graphs" (list_item ~icon:(`app "folder") ~selected:(reactive View_base.graphs_sidebar_selected_ model_source) @@ -146,7 +146,7 @@ let sidebar_view (context : Lui_ui.ui_context) model_source send : t = ~padding:12 [ box - ~height:(if Lui_ui.host context = FlutterHost then 8 else 48) + ~height:(if Lui_ui.host context = KotlinHost then 8 else 48) []; stack [ diff --git a/shared/test/logseq_chat/app_test.ml b/shared/test/logseq_chat/app_test.ml index a5f2f0c0..26ba2f57 100644 --- a/shared/test/logseq_chat/app_test.ml +++ b/shared/test/logseq_chat/app_test.ml @@ -449,9 +449,9 @@ let ios_backend () = Native_bridge.backend (Lui_protocol.profile Lui_protocol.IOS Lui_protocol.SwiftUIHost) -let flutter_backend () = +let kotlin_backend () = Native_bridge.backend - (Lui_protocol.profile Lui_protocol.AndroidOS Lui_protocol.FlutterHost) + (Lui_protocol.profile Lui_protocol.AndroidOS Lui_protocol.KotlinHost) let start application = ignore (Lui_app.start application) let send application action = ignore (Lui_app.send application action) @@ -1255,8 +1255,8 @@ let settings_render_the_main_branch_navigation_contract () = check ~msg:"runtime diagnostics retain their actions" (descendant_with_identifier renderer root "button.log-copy" <> -1) -let flutter_settings_sheet_uses_one_bounded_scroll_layout () = - let application = App.create (flutter_backend ()) in +let kotlin_settings_sheet_uses_one_bounded_scroll_layout () = + let application = App.create (kotlin_backend ()) in start application; send application (Model.ApplyCoreSnapshot @@ -1275,13 +1275,13 @@ let flutter_settings_sheet_uses_one_bounded_scroll_layout () = let layout = descendant_with_identifier renderer sheet "layout.settings.sheet" in - check_eq ~msg:"Flutter bounds the Material bottom sheet" + check_eq ~msg:"Kotlin bounds the Material bottom sheet" (property_int renderer sheet Lui_protocol.HeightValue) 640; - check_eq ~msg:"Flutter gives the backend one composed sheet child" + check_eq ~msg:"Kotlin gives the backend one composed sheet child" (List.length (children renderer sheet)) 1; - check ~msg:"Flutter owns one vertical settings layout" (layout <> -1); + check ~msg:"Kotlin owns one vertical settings layout" (layout <> -1); if layout <> -1 then begin check_eq ~msg:"long Settings content scrolls inside the sheet" (descendant_count_with_node_kind renderer layout Lui_protocol.Scroll) @@ -1291,8 +1291,8 @@ let flutter_settings_sheet_uses_one_bounded_scroll_layout () = <> -1) end -let flutter_settings_tabs_open_through_the_rendered_press_handler () = - let application = App.create (flutter_backend ()) in +let kotlin_settings_tabs_open_through_the_rendered_press_handler () = + let application = App.create (kotlin_backend ()) in start application; send application (Model.ApplySettingsSnapshot @@ -1304,7 +1304,7 @@ let flutter_settings_tabs_open_through_the_rendered_press_handler () = let tabs_link = descendant_with_identifier renderer root "link.settings.tabs" in - check ~msg:"Flutter renders the Settings tabs navigation row" + check ~msg:"Kotlin renders the Settings tabs navigation row" (tabs_link <> -1); dispatch application (Lui_protocol.Press tabs_link); flush application; @@ -1337,10 +1337,10 @@ let flutter_settings_tabs_open_through_the_rendered_press_handler () = check_eq ~msg:"the Tabs screen replaces the Settings main screen" (descendant_with_identifier renderer updated_root "screen.settings") (-1); - check_eq ~msg:"Flutter gives the Tabs sheet one bounded child" + check_eq ~msg:"Kotlin gives the Tabs sheet one bounded child" (List.length (children renderer sheet)) 1; - check ~msg:"Flutter owns one vertical Tabs layout" (layout <> -1); + check ~msg:"Kotlin owns one vertical Tabs layout" (layout <> -1); if layout <> -1 then begin check_eq ~msg:"Tabs uses one vertical Material layout" (node renderer layout) @@ -1364,8 +1364,8 @@ let flutter_settings_tabs_open_through_the_rendered_press_handler () = (property_float renderer flashcards_toggle Lui_protocol.GrowValue) (-1.0) -let flutter_runtime_log_actions_fit_phone_width () = - let application = App.create (flutter_backend ()) in +let kotlin_runtime_log_actions_fit_phone_width () = + let application = App.create (kotlin_backend ()) in start application; send application (Model.ApplyCoreSnapshot @@ -1396,10 +1396,10 @@ let flutter_runtime_log_actions_fit_phone_width () = let done_button = descendant_with_identifier renderer root "button.connection.apply" in - check_eq ~msg:"Flutter gives the Runtime log sheet one bounded child" + check_eq ~msg:"Kotlin gives the Runtime log sheet one bounded child" (List.length (children renderer sheet)) 1; - check ~msg:"Flutter owns one vertical Runtime log layout" (layout <> -1); + check ~msg:"Kotlin owns one vertical Runtime log layout" (layout <> -1); if layout <> -1 then begin check_eq ~msg:"Runtime log owns one vertical Material layout" (node renderer layout) @@ -1426,16 +1426,16 @@ let flutter_runtime_log_actions_fit_phone_width () = check_eq ~msg:"Done shares the phone width" (property_float renderer done_button Lui_protocol.GrowValue) 1.0; - check ~msg:"Flutter groups the first two log actions into a bounded row" + check ~msg:"Kotlin groups the first two log actions into a bounded row" (descendant_with_identifier renderer root "toolbar.log-filters.primary" <> -1); - check ~msg:"Flutter groups the remaining log actions into a bounded row" + check ~msg:"Kotlin groups the remaining log actions into a bounded row" (descendant_with_identifier renderer root "toolbar.log-filters.secondary" <> -1) -let flutter_settings_use_full_width_material_controls () = - let application = App.create (flutter_backend ()) in +let kotlin_settings_use_full_width_material_controls () = + let application = App.create (kotlin_backend ()) in start application; send application (Model.ApplyCoreSnapshot @@ -1504,10 +1504,10 @@ let flutter_settings_use_full_width_material_controls () = let sign_out = descendant_with_identifier renderer root "button.sign-out" in - check_eq ~msg:"Flutter uses one bounded Material select control" + check_eq ~msg:"Kotlin uses one bounded Material select control" (node renderer picker) (Some Lui_protocol.Select); - check_eq ~msg:"Flutter exposes the Material Theme select directly" + check_eq ~msg:"Kotlin exposes the Material Theme select directly" (node renderer appearance_picker) (Some Lui_protocol.Select); check_eq ~msg:"the Theme select is not pinned to a phone-specific width" @@ -1586,7 +1586,7 @@ let flutter_settings_use_full_width_material_controls () = check_eq ~msg:"Cancel and Apply share the available phone width" (property_float renderer apply Lui_protocol.GrowValue) 1.0; - check_eq ~msg:"Flutter does not lay every language out in one row" + check_eq ~msg:"Kotlin does not lay every language out in one row" (descendant_count_with_node_kind renderer picker Lui_protocol.RadioGroup) 0; send application Model.OpenSettingsLanguageMenu; @@ -1956,17 +1956,17 @@ let authentication_screen_uses_the_product_name_on_every_host () = (descendant_count_with_property_string renderer root Lui_protocol.TextValue "Logseq") 0) - [ ios_backend (); flutter_backend () ] + [ ios_backend (); kotlin_backend () ] let outliner_editor_extension_contract_is_pinned () = check_eq ~msg:"the native editor registry must match the LG wire schema" (Lui_extension.fingerprint (View.outliner_editor_schema ())) - "lui-extension-v1|15:outliner-editor|profiles:android/flutter,ios/swiftui|standard-children:0|children:|properties:18:caret-utf16-offset:int:required:none,5:title:string:required:none,8:block-id:string:required:none|events:11:text-change[18:caret-utf16-offset:int:required,5:title:string:required],12:caret-change[18:caret-utf16-offset:int:required],6:return[18:caret-utf16-offset:int:required,5:title:string:required],9:backspace[16:selection-length:int:required,5:title:string:required]" + "lui-extension-v1|15:outliner-editor|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:18:caret-utf16-offset:int:required:none,5:title:string:required:none,8:block-id:string:required:none|events:11:text-change[18:caret-utf16-offset:int:required,5:title:string:required],12:caret-change[18:caret-utf16-offset:int:required],6:return[18:caret-utf16-offset:int:required,5:title:string:required],9:backspace[16:selection-length:int:required,5:title:string:required]" let outliner_block_content_extension_contract_is_pinned () = check_eq ~msg:"the rich block renderer must match the LG wire schema" (Lui_extension.fingerprint (View.outliner_block_content_schema ())) - "lui-extension-v1|22:outliner-block-content|profiles:android/flutter,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]" + "lui-extension-v1|22:outliner-block-content|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]" let native_navigation_stack_extension_contract_is_pinned () = let schema = @@ -1982,7 +1982,7 @@ let native_navigation_stack_extension_contract_is_pinned () = ~msg:"native navigation must share one pinned LG and Swift wire contract" fingerprint (Some - "lui-extension-v1|23:native-navigation-stack|profiles:android/flutter,ios/swiftui|standard-children:1|children:|properties:26:composer-dismissal-enabled:bool:required:none,28:bottom-occupies-layout-space:bool:required:none,5:depth:int:required:none,5:title:string:required:none|events:16:dismiss-composer[],4:back[5:count:int:required]") + "lui-extension-v1|23:native-navigation-stack|profiles:android/kotlin,ios/swiftui|standard-children:1|children:|properties:26:composer-dismissal-enabled:bool:required:none,28:bottom-occupies-layout-space:bool:required:none,5:depth:int:required:none,5:title:string:required:none|events:16:dismiss-composer[],4:back[5:count:int:required]") let native_search_presentation_extension_contract_is_pinned () = let schema = @@ -1998,7 +1998,7 @@ let native_search_presentation_extension_contract_is_pinned () = ~msg:"search must use a distinct native full-screen navigation contract" fingerprint (Some - "lui-extension-v1|26:native-search-presentation|profiles:android/flutter,ios/swiftui|standard-children:1|children:|properties:5:depth:int:required:none,5:query:string:required:none,5:title:string:required:none,9:presented:bool:required:none|events:13:query-changed[5:query:string:required],4:back[5:count:int:required],7:dismiss[]") + "lui-extension-v1|26:native-search-presentation|profiles:android/kotlin,ios/swiftui|standard-children:1|children:|properties:5:depth:int:required:none,5:query:string:required:none,5:title:string:required:none,9:presented:bool:required:none|events:13:query-changed[5:query:string:required],4:back[5:count:int:required],7:dismiss[]") let liquid_glass_remains_an_ios_local_tweak () = let registry = View.extension_registry () in @@ -2198,8 +2198,8 @@ let graph_picker_matches_main_layout_actions_errors_and_overflow_menu () = Lui_protocol.TextValue) "Connection refused" -let flutter_graph_picker_uses_a_material_empty_state () = - let application = App.create (flutter_backend ()) in +let kotlin_graph_picker_uses_a_material_empty_state () = + let application = App.create (kotlin_backend ()) in start application; flush application; let renderer = Lui_app.runtime application in @@ -2276,8 +2276,8 @@ let persisted_graph_loading_hides_the_launch_picker () = check ~msg:"cached journals remain visible during a background reload" (View_base.journal_root_visible_ cached) -let flutter_loading_and_errors_use_material_feedback_surfaces () = - let application = App.create (flutter_backend ()) in +let kotlin_loading_and_errors_use_material_feedback_surfaces () = + let application = App.create (kotlin_backend ()) in let local = graph "local" "Local graph" false true in start application; send application (Model.ApplyGraphLoading true); @@ -3848,8 +3848,8 @@ let graphs_render_the_existing_catalog_and_modal_contract () = (Some false) end -let flutter_graphs_use_a_compact_material_action_group () = - let application = App.create (flutter_backend ()) in +let kotlin_graphs_use_a_compact_material_action_group () = + let application = App.create (kotlin_backend ()) in let local = graph "local" "Local graph" false true in let remote = graph "remote" "Remote graph" false true in let projection = @@ -3923,8 +3923,8 @@ let flutter_graphs_use_a_compact_material_action_group () = (node renderer local_row) (Some Lui_protocol.ListItem) -let flutter_add_graph_sheet_uses_one_material_form_layout () = - let application = App.create (flutter_backend ()) in +let kotlin_add_graph_sheet_uses_one_material_form_layout () = + let application = App.create (kotlin_backend ()) in start application; send application Model.OpenCreateGraph; flush application; @@ -3936,13 +3936,13 @@ let flutter_add_graph_sheet_uses_one_material_form_layout () = let layout = descendant_with_identifier renderer sheet "layout.graph-create.sheet" in - check_eq ~msg:"Flutter bounds the Add graph Material sheet" + check_eq ~msg:"Kotlin bounds the Add graph Material sheet" (property_int renderer sheet Lui_protocol.HeightValue) 480; - check_eq ~msg:"Flutter gives the backend one composed Add graph child" + check_eq ~msg:"Kotlin gives the backend one composed Add graph child" (List.length (children renderer sheet)) 1; - check ~msg:"Flutter owns one vertical Add graph layout" (layout <> -1); + check ~msg:"Kotlin owns one vertical Add graph layout" (layout <> -1); if layout <> -1 then begin let form = descendant_with_identifier renderer layout "form.graph-create" @@ -3962,8 +3962,8 @@ let flutter_add_graph_sheet_uses_one_material_form_layout () = <> -1) end -let flutter_add_graph_sheet_shows_creation_errors_inline () = - let application = App.create (flutter_backend ()) in +let kotlin_add_graph_sheet_shows_creation_errors_inline () = + let application = App.create (kotlin_backend ()) in start application; send application Model.OpenCreateGraph; send application (Model.ChangeNewGraphName "Broken graph"); @@ -4093,8 +4093,8 @@ let graph_deletion_confirmation_names_the_local_graph () = (property_string renderer error Lui_protocol.TextValue) "Could not delete graph" -let flutter_graph_deletion_uses_a_material_destructive_dialog () = - let application = App.create (flutter_backend ()) in +let kotlin_graph_deletion_uses_a_material_destructive_dialog () = + let application = App.create (kotlin_backend ()) in let local = graph "local" "Local graph" false true in start application; send application @@ -4775,8 +4775,8 @@ let composer_renders_autofocus_and_native_outside_dismissal () = check ~msg:"the native outside tap collapses the composer" (not (App.model application).composer_expanded) -let flutter_composer_uses_a_tonal_material_dock () = - let application = App.create (flutter_backend ()) in +let kotlin_composer_uses_a_tonal_material_dock () = + let application = App.create (kotlin_backend ()) in start application; send application (Model.ApplyCoreSnapshot @@ -4833,8 +4833,8 @@ let flutter_composer_uses_a_tonal_material_dock () = (property_string renderer send_button Lui_protocol.SizeValue) "icon" -let flutter_sidebar_uses_compact_material_drawer_metrics () = - let application = App.create (flutter_backend ()) in +let kotlin_sidebar_uses_compact_material_drawer_metrics () = + let application = App.create (kotlin_backend ()) in start application; send application (Model.ApplyCoreSnapshot @@ -5040,27 +5040,27 @@ let native_bridge_renders_restored_authentication_in_the_first_patch () = check ~msg:"signed-out initialization excludes main navigation controls" (not (contains "button.search" patch)) -let native_bridge_selects_the_flutter_host_profile () = - check ~msg:"Flutter Android uses the retained Flutter backend profile" - (Native_bridge.host_kind 3 = Lui_protocol.FlutterHost); +let native_bridge_selects_the_kotlin_host_profile () = + check ~msg:"Kotlin Android uses the retained Kotlin backend profile" + (Native_bridge.host_kind 4 = Lui_protocol.KotlinHost); check ~msg:"Apple hosts keep the SwiftUI profile" (Native_bridge.host_kind 2 = Lui_protocol.SwiftUIHost); - let patch = Native_bridge.initialize 3 3 1 in - check ~msg:"Flutter renders the shared authentication screen" + let patch = Native_bridge.initialize 3 4 1 in + check ~msg:"Kotlin renders the shared authentication screen" (contains "screen.authentication" patch); - check ~msg:"Flutter does not receive SwiftUI-only viewport properties" + check ~msg:"Kotlin does not receive SwiftUI-only viewport properties" (not (contains "container-relative-frame" patch)); - let patch = Native_bridge.initialize 3 3 3 in - check ~msg:"signed-in Flutter renders the Material graph picker controls" + let patch = Native_bridge.initialize 3 4 3 in + check ~msg:"signed-in Kotlin renders the Material graph picker controls" (contains "menu-trigger" patch); check - ~msg:"signed-in Flutter excludes every SwiftUI-only viewport property" + ~msg:"signed-in Kotlin excludes every SwiftUI-only viewport property" (not (contains "container-relative-frame" patch)); - check ~msg:"signed-in Flutter excludes unsupported list-item icon placement" + check ~msg:"signed-in Kotlin excludes unsupported list-item icon placement" (not (contains "icon-placement" patch)); - check ~msg:"signed-in Flutter excludes unsupported list-item heading roles" + check ~msg:"signed-in Kotlin excludes unsupported list-item heading roles" (not (contains "navigation-heading" patch)); - check ~msg:"signed-in Flutter excludes unsupported list-item navigation roles" + check ~msg:"signed-in Kotlin excludes unsupported list-item navigation roles" (not (contains "\"navigation\"" patch)); let patch = Native_bridge.flush_action @@ -5083,8 +5083,8 @@ let native_bridge_selects_the_flutter_host_profile () = (not (contains "\"navigation\"" patch)); ignore (Native_bridge.dispose ()) -let flutter_search_presentation_owns_an_opaque_background () = - ignore (Native_bridge.initialize 3 3 3); +let kotlin_search_presentation_owns_an_opaque_background () = + ignore (Native_bridge.initialize 3 4 3); ignore (Native_bridge.flush_action (Model.ApplyCoreSnapshot @@ -5095,9 +5095,9 @@ let flutter_search_presentation_owns_an_opaque_background () = graphs = [ graph "graph-a" "Work" false true ]; })); let patch = Native_bridge.flush_action Model.OpenSearch in - check ~msg:"opening Flutter search mounts the search screen" + check ~msg:"opening Kotlin search mounts the search screen" (contains "screen.search" patch); - check ~msg:"Flutter search covers the retained journal surface" + check ~msg:"Kotlin search covers the retained journal surface" (contains "\"property\":\"background\",\"value\":\"background\"" patch); ignore (Native_bridge.dispose ()); ignore (Native_bridge.initialize 2 2 3); @@ -5111,15 +5111,15 @@ let flutter_search_presentation_owns_an_opaque_background () = graphs = [ graph "graph-a" "Work" false true ]; })); let patch = Native_bridge.flush_action Model.OpenSearch in - check ~msg:"the Flutter-only backdrop does not alter SwiftUI search" + check ~msg:"the Kotlin-only backdrop does not alter SwiftUI search" (not (contains "\"property\":\"background\",\"value\":\"background\"" patch)); ignore (Native_bridge.dispose ()) -let flutter_node_route_owns_an_opaque_background () = +let kotlin_node_route_owns_an_opaque_background () = let route = node_projection "node-a" "page-a" "Project" [] [] in - ignore (Native_bridge.initialize 3 3 3); + ignore (Native_bridge.initialize 3 4 3); ignore (Native_bridge.flush_action (Model.RequestAppNode "node-a")); let patch = Native_bridge.flush_action @@ -5131,9 +5131,9 @@ let flutter_node_route_owns_an_opaque_background () = node_routes = [ route ]; }) in - check ~msg:"opening a Flutter node mounts the node screen" + check ~msg:"opening a Kotlin node mounts the node screen" (contains "screen.node" patch); - check ~msg:"Flutter node routes cover the retained journal surface" + check ~msg:"Kotlin node routes cover the retained journal surface" (contains "\"property\":\"background\",\"value\":\"background\"" patch); ignore (Native_bridge.dispose ()); ignore (Native_bridge.initialize 2 2 3); @@ -5149,14 +5149,14 @@ let flutter_node_route_owns_an_opaque_background () = }) in check - ~msg:"the Flutter-only node backdrop does not alter SwiftUI navigation" + ~msg:"the Kotlin-only node backdrop does not alter SwiftUI navigation" (not (contains "\"property\":\"background\",\"value\":\"background\"" patch)); ignore (Native_bridge.dispose ()) let native_bridge_drains_and_resolves_typed_effects_once () = - ignore (Native_bridge.initialize 2 1 0); + ignore (Native_bridge.initialize 2 2 0); let application = Native_bridge.app () in send application Model.ExpandComposer; send application @@ -5624,7 +5624,7 @@ let ios_node_navigation_is_owned_by_the_native_stack () = in let chrome = native_bottom_chrome renderer application in check_eq - ~msg:"SwiftUI node navigation does not receive Flutter flex sizing" + ~msg:"SwiftUI node navigation does not receive Kotlin flex sizing" (property_float renderer screen Lui_protocol.GrowValue) 0.0; check_eq ~msg:"the retained node does not duplicate the system back button" @@ -5647,8 +5647,8 @@ let ios_node_navigation_is_owned_by_the_native_stack () = check_eq ~msg:"the native iOS back action pops the LG route" (App.model application).app_navigation_path [] -let flutter_navigation_and_search_own_one_composed_standard_child () = - let application = App.create (flutter_backend ()) in +let kotlin_navigation_and_search_own_one_composed_standard_child () = + let application = App.create (kotlin_backend ()) in start application; send application (Model.ApplyCoreSnapshot @@ -5663,9 +5663,9 @@ let flutter_navigation_and_search_own_one_composed_standard_child () = let search = extension_node application "native-search-presentation" in let navigation_children = children renderer navigation in let search_children = children renderer search in - check_eq ~msg:"Flutter navigation receives one composed child" + check_eq ~msg:"Kotlin navigation receives one composed child" (List.length navigation_children) 1; - check_eq ~msg:"Flutter search receives one composed child" + check_eq ~msg:"Kotlin search receives one composed child" (List.length search_children) 1; let navigation_content = List.nth navigation_children 0 in let search_content = List.nth search_children 0 in @@ -5676,20 +5676,20 @@ let flutter_navigation_and_search_own_one_composed_standard_child () = descendant_with_identifier renderer navigation_content "row.bottom.capture" in - check_eq ~msg:"the composed Flutter child uses Material title typography" + check_eq ~msg:"the composed Kotlin child uses Material title typography" (node renderer navigation_title) (Some Lui_protocol.Heading); - check_eq ~msg:"the Flutter navigation title maps to Material titleLarge" + check_eq ~msg:"the Kotlin navigation title maps to Material titleLarge" (property_int renderer navigation_title Lui_protocol.HeadingLevel) 3; - check ~msg:"the composed Flutter child owns the bottom controls" + check ~msg:"the composed Kotlin child owns the bottom controls" (descendant_with_identifier renderer navigation_content "button.search" <> -1); - check ~msg:"the closed Flutter search child retains journal content" + check ~msg:"the closed Kotlin search child retains journal content" (descendant_with_identifier renderer search_content "list.outliner" <> -1); check_eq - ~msg:"Flutter capture keeps intrinsic height inside the bottom dock" + ~msg:"Kotlin capture keeps intrinsic height inside the bottom dock" (property_float renderer capture_row Lui_protocol.GrowValue) (-1.0); send application Model.ExpandComposer; @@ -5699,12 +5699,12 @@ let flutter_navigation_and_search_own_one_composed_standard_child () = "row.composer.placement" in check_eq - ~msg:"Flutter expanded Capture uses intrinsic height inside the bottom overlay" + ~msg:"Kotlin expanded Capture uses intrinsic height inside the bottom overlay" (property_float renderer expanded_row Lui_protocol.GrowValue) (-1.0) -let flutter_navigation_renders_each_active_node_row_once () = - let application = App.create (flutter_backend ()) in +let kotlin_navigation_renders_each_active_node_row_once () = + let application = App.create (kotlin_backend ()) in let row = journal_outline_row "route-child" "route-page" "Route child" "" 0 0 in @@ -5735,17 +5735,17 @@ let flutter_navigation_renders_each_active_node_row_once () = descendant_with_identifier renderer navigation "button.navigation.back" in check - ~msg:"Flutter node destinations expose a Material top-app-bar back action" + ~msg:"Kotlin node destinations expose a Material top-app-bar back action" (back_button <> -1); check_eq ~msg:"the Android back action remains available to TalkBack" (property_string renderer back_button Lui_protocol.AccessibilityLabel) "Back"; - check_eq ~msg:"Flutter node content receives a bounded flex height" + check_eq ~msg:"Kotlin node content receives a bounded flex height" (property_float renderer screen Lui_protocol.GrowValue) 1.0; check_eq ~msg: - "Flutter keeps the journal as the stack root instead of duplicating \ + "Kotlin keeps the journal as the stack root instead of duplicating \ the active node route" (descendant_count_with_identifier renderer navigation "outliner.block.route-child") @@ -5755,8 +5755,8 @@ let flutter_navigation_renders_each_active_node_row_once () = check_eq ~msg:"the visible Android back action pops exactly one route" (App.model application).app_navigation_path [] -let flutter_selected_pages_unmount_the_hidden_journal_pane () = - let application = App.create (flutter_backend ()) in +let kotlin_selected_pages_unmount_the_hidden_journal_pane () = + let application = App.create (kotlin_backend ()) in let page = { Model.uuid = "page-a"; title = "Project" } in let row = journal_outline_row "selected-row" "page-a" "Selected row" "" 0 0 @@ -5786,18 +5786,18 @@ let flutter_selected_pages_unmount_the_hidden_journal_pane () = flush application; let renderer = Lui_app.runtime application in let navigation = extension_node application "native-navigation-stack" in - check ~msg:"Flutter renders the selected page" + check ~msg:"Kotlin renders the selected page" (descendant_with_identifier renderer navigation "pane.selected-page" <> -1); check_eq ~msg: - "Flutter removes the hidden journal so it cannot paint or receive \ + "Kotlin removes the hidden journal so it cannot paint or receive \ input behind the selected page" (descendant_with_identifier renderer navigation "pane.journals") (-1) -let flutter_selected_page_navigation_renders_the_opened_node () = - let application = App.create (flutter_backend ()) in +let kotlin_selected_page_navigation_renders_the_opened_node () = + let application = App.create (kotlin_backend ()) in let page = { Model.uuid = "page-a"; title = "Project" } in let selected_row = journal_outline_row "selected-row" "page-a" "Selected row" "" 0 0 @@ -5842,12 +5842,12 @@ let flutter_selected_page_navigation_renders_the_opened_node () = flush application; let renderer = Lui_app.runtime application in let navigation = extension_node application "native-navigation-stack" in - check_eq ~msg:"Flutter renders the opened route above an existing selected page" + check_eq ~msg:"Kotlin renders the opened route above an existing selected page" (descendant_count_with_identifier renderer navigation "outliner.block.route-row") 1; check_eq - ~msg:"Flutter unmounts the selected-page root while a node route is active" + ~msg:"Kotlin unmounts the selected-page root while a node route is active" (descendant_with_identifier renderer navigation "pane.selected-page") (-1) @@ -7099,23 +7099,23 @@ let outliner_row_press_publishes_a_typed_core_effect () = check_eq ~msg:"outliner effects share the monotonic effect sequence" editing.next_effect_id 2 -let flutter_rich_rows_own_their_primary_tap () = +let kotlin_rich_rows_own_their_primary_tap () = let row = outline_row "block-a" "Linked block" in check_eq - ~msg:"Flutter rich content avoids a competing whole-row primary tap" + ~msg:"Kotlin rich content avoids a competing whole-row primary tap" (View_base.outliner_row_list_item_press_enabled_ - Lui_protocol.FlutterHost row) + Lui_protocol.KotlinHost row) false; check_eq - ~msg:"Flutter asset rows retain their whole-row presentation action" + ~msg:"Kotlin asset rows retain their whole-row presentation action" (View_base.outliner_row_list_item_press_enabled_ - Lui_protocol.FlutterHost + Lui_protocol.KotlinHost { row with Model.is_asset = true }) true; check_eq - ~msg:"Flutter page rows retain their whole-row navigation action" + ~msg:"Kotlin page rows retain their whole-row navigation action" (View_base.outliner_row_list_item_press_enabled_ - Lui_protocol.FlutterHost + Lui_protocol.KotlinHost { row with Model.opens_as_page = true }) true; check_eq @@ -7173,7 +7173,7 @@ let active_page_actions_use_typed_core_and_platform_effects () = ] let page_delete_dialog_exposes_stable_material_actions () = - let application = App.create (flutter_backend ()) in + let application = App.create (kotlin_backend ()) in let page = { Model.uuid = "page-a"; title = "Project" } in let sidebar = { (empty_sidebar_projection ()) with Model.selected_page = Some page } @@ -7277,8 +7277,8 @@ let outliner_task_status_selection_uses_the_core_event_boundary () = chosen.pending_effects [ Model.SetOutlinerTaskStatusEffect (1, "block-a", done_status) ] -let flutter_outliner_markers_match_ios_visual_metrics () = - let application = App.create (flutter_backend ()) in +let kotlin_outliner_markers_match_ios_visual_metrics () = + let application = App.create (kotlin_backend ()) in let todo = Model.task_status "todo" "logseq.property/status.todo" "Todo" "Todo" in @@ -7318,9 +7318,9 @@ let flutter_outliner_markers_match_ios_visual_metrics () = "button.block-task-status" in check - ~msg:"Flutter renders a dedicated visual bullet inside its hit target" + ~msg:"Kotlin renders a dedicated visual bullet inside its hit target" (bullet <> -1); - check_eq ~msg:"Flutter uses iOS main's seven-point bullet diameter" + check_eq ~msg:"Kotlin uses iOS main's seven-point bullet diameter" (property_int renderer bullet Lui_protocol.WidthValue) 7; check_eq ~msg:"the outliner bullet remains circular" @@ -7333,15 +7333,15 @@ let flutter_outliner_markers_match_ios_visual_metrics () = (property_int renderer zoom_button Lui_protocol.HeightValue) 24; check - ~msg:"Flutter renders the task glyph independently from its menu target" + ~msg:"Kotlin renders the task glyph independently from its menu target" (status_icon <> -1); - check_eq ~msg:"Flutter uses iOS main's 22-point task status glyph" + check_eq ~msg:"Kotlin uses iOS main's 22-point task status glyph" (property_int renderer status_icon Lui_protocol.WidthValue) 22; check_eq ~msg:"the task status glyph keeps iOS main's square frame" (property_int renderer status_icon Lui_protocol.HeightValue) 22; - check_eq ~msg:"Flutter matches iOS by using the row foreground for Todo" + check_eq ~msg:"Kotlin matches iOS by using the row foreground for Todo" (property_string renderer status_icon Lui_protocol.ForegroundValue) "foreground"; check_eq @@ -7370,7 +7370,7 @@ let flutter_outliner_markers_match_ios_visual_metrics () = in check_eq ~msg: - "a retained Flutter row replaces the task glyph when status changes" + "a retained Kotlin row replaces the task glyph when status changes" (property_string renderer updated_status_icon Lui_protocol.IconName) "app:task-done"; check_eq @@ -7572,8 +7572,8 @@ let outliner_long_press_selection_and_toolbar_use_typed_effects () = (App.model application).pending_effects [ Model.OutlinerToolbarEffect (1, "copy") ] -let flutter_outliner_selection_toolbar_uses_compact_material_actions () = - let application = App.create (flutter_backend ()) in +let kotlin_outliner_selection_toolbar_uses_compact_material_actions () = + let application = App.create (kotlin_backend ()) in let row = outline_row "parent" "Parent" in start application; send application @@ -7593,10 +7593,10 @@ let flutter_outliner_selection_toolbar_uses_compact_material_actions () = descendant_with_identifier renderer root "toolbar.outliner.selection" in let buttons = children renderer toolbar in - check_eq ~msg:"Flutter keeps the editor-compatible row surface" + check_eq ~msg:"Kotlin keeps the editor-compatible row surface" (node renderer selected_row) (Some Lui_protocol.Box); - check ~msg:"Flutter projects selection onto the row surface" + check ~msg:"Kotlin projects selection onto the row surface" (property_bool renderer selected_row Lui_protocol.Selected); check_eq ~msg:"Android uses compact Material action spacing" (property_int renderer toolbar Lui_protocol.Gap) @@ -7642,8 +7642,8 @@ let flutter_outliner_selection_toolbar_uses_compact_material_actions () = Lui_protocol.ForegroundValue) "muted-foreground" -let flutter_outliner_editor_toolbar_uses_a_material_bottom_surface () = - let application = App.create (flutter_backend ()) in +let kotlin_outliner_editor_toolbar_uses_a_material_bottom_surface () = + let application = App.create (kotlin_backend ()) in let row = outline_row "block-a" "Draft" in let editing = { @@ -7889,8 +7889,8 @@ let outliner_editor_toolbar_and_autocomplete_use_core_owned_state () = check_eq ~msg:"a task mutation hides stale synced state immediately" (App.model application).sync_state Model.SyncingState -let flutter_outliner_autocomplete_stacks_above_the_editor_toolbar () = - let application = App.create (flutter_backend ()) in +let kotlin_outliner_autocomplete_stacks_above_the_editor_toolbar () = + let application = App.create (kotlin_backend ()) in let row = outline_row "block-a" "Draft #" in let editing = { @@ -7938,7 +7938,7 @@ let flutter_outliner_autocomplete_stacks_above_the_editor_toolbar () = in check_eq ~msg: - "Flutter lays autocomplete above the editor toolbar instead of \ + "Kotlin lays autocomplete above the editor toolbar instead of \ overlaying it" (node renderer editor_container) (Some Lui_protocol.Column); @@ -7954,12 +7954,12 @@ let flutter_outliner_autocomplete_stacks_above_the_editor_toolbar () = "toolbar.outliner.autocomplete" <> -1); check_eq - ~msg:"Flutter fills autocomplete width with cross-axis stretching" + ~msg:"Kotlin fills autocomplete width with cross-axis stretching" (property_string renderer autocomplete_column Lui_protocol.CrossAlignment) "stretch"; check_eq - ~msg:"Flutter does not flex rows along an unbounded scroll axis" + ~msg:"Kotlin does not flex rows along an unbounded scroll axis" (property_float renderer candidate_button Lui_protocol.GrowValue) 0.0 @@ -8134,13 +8134,13 @@ let outliner_row_splices_update_the_existing_keyed_projection () = collapsed.outliner_rows [ collapsed_parent; sibling ] let native_bridge_returns_initial_and_disposal_patch_batches () = - let initial_patch = Native_bridge.initialize 2 1 0 in + let initial_patch = Native_bridge.initialize 2 2 0 in check ~msg:"initialization emits a patch" (initial_patch <> ""); check ~msg:"the bridge exposes a root node" (Native_bridge.root_node () > 0); check ~msg:"disposal emits a patch" (Native_bridge.dispose () <> "") let native_bridge_preserves_native_search_query_values () = - ignore (Native_bridge.initialize 2 1 0); + ignore (Native_bridge.initialize 2 2 0); let application = Native_bridge.app () in send application (Model.SelectGraph "Work"); send application Model.OpenSearch; @@ -8154,7 +8154,7 @@ let native_bridge_preserves_native_search_query_values () = ignore (Native_bridge.dispose ()) let native_bridge_preserves_native_navigation_back_counts () = - ignore (Native_bridge.initialize 2 1 0); + ignore (Native_bridge.initialize 2 2 0); let application = Native_bridge.app () in send application (Model.SelectGraph "Work"); send application (Model.RequestAppNode "page-a"); @@ -8557,10 +8557,10 @@ let cases = case "graph-database-export-has-a-stable-native-effect-payload" graph_database_export_has_a_stable_native_effect_payload; case "cancel-outliner-editing-has-a-stable-native-effect-payload" cancel_outliner_editing_has_a_stable_native_effect_payload; case "settings-render-the-main-branch-navigation-contract" settings_render_the_main_branch_navigation_contract; - case "flutter-settings-sheet-uses-one-bounded-scroll-layout" flutter_settings_sheet_uses_one_bounded_scroll_layout; - case "flutter-settings-tabs-open-through-the-rendered-press-handler" flutter_settings_tabs_open_through_the_rendered_press_handler; - case "flutter-runtime-log-actions-fit-phone-width" flutter_runtime_log_actions_fit_phone_width; - case "flutter-settings-use-full-width-material-controls" flutter_settings_use_full_width_material_controls; + case "kotlin-settings-sheet-uses-one-bounded-scroll-layout" kotlin_settings_sheet_uses_one_bounded_scroll_layout; + case "kotlin-settings-tabs-open-through-the-rendered-press-handler" kotlin_settings_tabs_open_through_the_rendered_press_handler; + case "kotlin-runtime-log-actions-fit-phone-width" kotlin_runtime_log_actions_fit_phone_width; + case "kotlin-settings-use-full-width-material-controls" kotlin_settings_use_full_width_material_controls; case "settings-tabs-match-main-visibility-and-movement-boundaries" settings_tabs_match_main_visibility_and_movement_boundaries; case "android-disclosure-and-selection-icons-use-material-semantics" android_disclosure_and_selection_icons_use_material_semantics; case "settings-tabs-render-saved-order-and-separate-available-tabs" settings_tabs_render_saved_order_and_separate_available_tabs; @@ -8578,9 +8578,9 @@ let cases = case "initial-shell-renders-the-graph-picker-without-a-selected-graph" initial_shell_renders_the_graph_picker_without_a_selected_graph; case "graph-picker-not-ready-status-matches-main-copy" graph_picker_not_ready_status_matches_main_copy; case "graph-picker-matches-main-layout-actions-errors-and-overflow-menu" graph_picker_matches_main_layout_actions_errors_and_overflow_menu; - case "flutter-graph-picker-uses-a-material-empty-state" flutter_graph_picker_uses_a_material_empty_state; + case "kotlin-graph-picker-uses-a-material-empty-state" kotlin_graph_picker_uses_a_material_empty_state; case "persisted-graph-loading-hides-the-launch-picker" persisted_graph_loading_hides_the_launch_picker; - case "flutter-loading-and-errors-use-material-feedback-surfaces" flutter_loading_and_errors_use_material_feedback_surfaces; + case "kotlin-loading-and-errors-use-material-feedback-surfaces" kotlin_loading_and_errors_use_material_feedback_surfaces; case "graph-and-sync-actions-update-retained-status-in-place" graph_and_sync_actions_update_retained_status_in_place; case "sync-details-render-projected-cursor-and-trigger-the-existing-pump" sync_details_render_projected_cursor_and_trigger_the_existing_pump; case "sync-details-show-the-last-sync-failure" sync_details_show_the_last_sync_failure; @@ -8605,13 +8605,13 @@ let cases = case "graph-catalog-and-lifecycle-state-are-owned-by-lg" graph_catalog_and_lifecycle_state_are_owned_by_lg; case "graph-picker-surfaces-open-graph-effect-failures" graph_picker_surfaces_open_graph_effect_failures; case "graphs-render-the-existing-catalog-and-modal-contract" graphs_render_the_existing_catalog_and_modal_contract; - case "flutter-graphs-use-a-compact-material-action-group" flutter_graphs_use_a_compact_material_action_group; - case "flutter-add-graph-sheet-uses-one-material-form-layout" flutter_add_graph_sheet_uses_one_material_form_layout; - case "flutter-add-graph-sheet-shows-creation-errors-inline" flutter_add_graph_sheet_shows_creation_errors_inline; + case "kotlin-graphs-use-a-compact-material-action-group" kotlin_graphs_use_a_compact_material_action_group; + case "kotlin-add-graph-sheet-uses-one-material-form-layout" kotlin_add_graph_sheet_uses_one_material_form_layout; + case "kotlin-add-graph-sheet-shows-creation-errors-inline" kotlin_add_graph_sheet_shows_creation_errors_inline; case "graph-lifecycle-effects-disable-duplicate-actions" graph_lifecycle_effects_disable_duplicate_actions; case "empty-graph-picker-replaces-refresh-with-progress-while-loading" empty_graph_picker_replaces_refresh_with_progress_while_loading; case "graph-deletion-confirmation-names-the-local-graph" graph_deletion_confirmation_names_the_local_graph; - case "flutter-graph-deletion-uses-a-material-destructive-dialog" flutter_graph_deletion_uses_a_material_destructive_dialog; + case "kotlin-graph-deletion-uses-a-material-destructive-dialog" kotlin_graph_deletion_uses_a_material_destructive_dialog; case "search-lifecycle-keeps-query-owned-by-the-lg-model" search_lifecycle_keeps_query_owned_by_the_lg_model; case "bottom-chrome-presentation-is-mutually-exclusive" bottom_chrome_presentation_is_mutually_exclusive; case "composer-matches-the-main-branch-expand-draft-and-send-contract" composer_matches_the_main_branch_expand_draft_and_send_contract; @@ -8625,8 +8625,8 @@ let cases = case "composer-draft-restore-focus-and-dismissal-are-owned-by-lg" composer_draft_restore_focus_and_dismissal_are_owned_by_lg; case "composer-refocus-always-emits-an-autofocus-edge" composer_refocus_always_emits_an_autofocus_edge; case "composer-renders-autofocus-and-native-outside-dismissal" composer_renders_autofocus_and_native_outside_dismissal; - case "flutter-composer-uses-a-tonal-material-dock" flutter_composer_uses_a_tonal_material_dock; - case "flutter-sidebar-uses-compact-material-drawer-metrics" flutter_sidebar_uses_compact_material_drawer_metrics; + case "kotlin-composer-uses-a-tonal-material-dock" kotlin_composer_uses_a_tonal_material_dock; + case "kotlin-sidebar-uses-compact-material-drawer-metrics" kotlin_sidebar_uses_compact_material_drawer_metrics; case "composer-attachment-selection-is-owned-by-lg" composer_attachment_selection_is_owned_by_lg; case "composer-attachment-menu-preserves-main-actions" composer_attachment_menu_preserves_main_actions; case "composer-task-status-selection-and-send-are-owned-by-lg" composer_task_status_selection_and_send_are_owned_by_lg; @@ -8634,9 +8634,9 @@ let cases = case "task-capture-has-a-stable-native-effect-payload" task_capture_has_a_stable_native_effect_payload; case "task-status-choices-preserve-main-built-in-fallbacks" task_status_choices_preserve_main_built_in_fallbacks; case "native-bridge-renders-restored-authentication-in-the-first-patch" native_bridge_renders_restored_authentication_in_the_first_patch; - case "native-bridge-selects-the-flutter-host-profile" native_bridge_selects_the_flutter_host_profile; - case "flutter-search-presentation-owns-an-opaque-background" flutter_search_presentation_owns_an_opaque_background; - case "flutter-node-route-owns-an-opaque-background" flutter_node_route_owns_an_opaque_background; + case "native-bridge-selects-the-kotlin-host-profile" native_bridge_selects_the_kotlin_host_profile; + case "kotlin-search-presentation-owns-an-opaque-background" kotlin_search_presentation_owns_an_opaque_background; + case "kotlin-node-route-owns-an-opaque-background" kotlin_node_route_owns_an_opaque_background; case "native-bridge-drains-and-resolves-typed-effects-once" native_bridge_drains_and_resolves_typed_effects_once; case "successful-effect-resolution-preserves-the-core-response-for-projection" successful_effect_resolution_preserves_the_core_response_for_projection; case "app-navigation-matches-the-main-branch-path-contract" app_navigation_matches_the_main_branch_path_contract; @@ -8650,10 +8650,10 @@ let cases = case "failed-navigation-effects-restore-the-optimistic-path" failed_navigation_effects_restore_the_optimistic_path; case "active-node-route-renders-a-core-backed-navigation-screen" active_node_route_renders_a_core_backed_navigation_screen; case "ios-node-navigation-is-owned-by-the-native-stack" ios_node_navigation_is_owned_by_the_native_stack; - case "flutter-navigation-and-search-own-one-composed-standard-child" flutter_navigation_and_search_own_one_composed_standard_child; - case "flutter-navigation-renders-each-active-node-row-once" flutter_navigation_renders_each_active_node_row_once; - case "flutter-selected-pages-unmount-the-hidden-journal-pane" flutter_selected_pages_unmount_the_hidden_journal_pane; - case "flutter-selected-page-navigation-renders-the-opened-node" flutter_selected_page_navigation_renders_the_opened_node; + case "kotlin-navigation-and-search-own-one-composed-standard-child" kotlin_navigation_and_search_own_one_composed_standard_child; + case "kotlin-navigation-renders-each-active-node-row-once" kotlin_navigation_renders_each_active_node_row_once; + case "kotlin-selected-pages-unmount-the-hidden-journal-pane" kotlin_selected_pages_unmount_the_hidden_journal_pane; + case "kotlin-selected-page-navigation-renders-the-opened-node" kotlin_selected_page_navigation_renders_the_opened_node; case "native-navigation-retains-the-journal-and-every-node-route" native_navigation_retains_the_journal_and_every_node_route; case "journal-navigation-exposes-an-immediate-preview-route" journal_navigation_exposes_an_immediate_preview_route; case "block-navigation-exposes-an-immediate-outliner-preview-route" block_navigation_exposes_an_immediate_outliner_preview_route; @@ -8678,19 +8678,19 @@ let cases = case "projected-markup-renders-through-the-native-rich-block-extension" projected_markup_renders_through_the_native_rich_block_extension; case "projected-assets-render-and-open-through-the-native-extension" projected_assets_render_and_open_through_the_native_extension; case "outliner-row-press-publishes-a-typed-core-effect" outliner_row_press_publishes_a_typed_core_effect; - case "flutter-rich-rows-own-their-primary-tap" flutter_rich_rows_own_their_primary_tap; + case "kotlin-rich-rows-own-their-primary-tap" kotlin_rich_rows_own_their_primary_tap; case "active-page-actions-use-typed-core-and-platform-effects" active_page_actions_use_typed_core_and_platform_effects; case "page-delete-dialog-exposes-stable-material-actions" page_delete_dialog_exposes_stable_material_actions; case "connection-menu-matches-active-page-actions" connection_menu_matches_active_page_actions; case "outliner-structure-controls-use-native-navigation-and-core-effects" outliner_structure_controls_use_native_navigation_and_core_effects; case "outliner-task-status-selection-uses-the-core-event-boundary" outliner_task_status_selection_uses_the_core_event_boundary; - case "flutter-outliner-markers-match-ios-visual-metrics" flutter_outliner_markers_match_ios_visual_metrics; + case "kotlin-outliner-markers-match-ios-visual-metrics" kotlin_outliner_markers_match_ios_visual_metrics; case "outliner-rows-render-status-tags-and-sync-failures" outliner_rows_render_status_tags_and_sync_failures; case "outliner-long-press-selection-and-toolbar-use-typed-effects" outliner_long_press_selection_and_toolbar_use_typed_effects; - case "flutter-outliner-selection-toolbar-uses-compact-material-actions" flutter_outliner_selection_toolbar_uses_compact_material_actions; - case "flutter-outliner-editor-toolbar-uses-a-material-bottom-surface" flutter_outliner_editor_toolbar_uses_a_material_bottom_surface; + case "kotlin-outliner-selection-toolbar-uses-compact-material-actions" kotlin_outliner_selection_toolbar_uses_compact_material_actions; + case "kotlin-outliner-editor-toolbar-uses-a-material-bottom-surface" kotlin_outliner_editor_toolbar_uses_a_material_bottom_surface; case "outliner-editor-toolbar-and-autocomplete-use-core-owned-state" outliner_editor_toolbar_and_autocomplete_use_core_owned_state; - case "flutter-outliner-autocomplete-stacks-above-the-editor-toolbar" flutter_outliner_autocomplete_stacks_above_the_editor_toolbar; + case "kotlin-outliner-autocomplete-stacks-above-the-editor-toolbar" kotlin_outliner_autocomplete_stacks_above_the_editor_toolbar; case "outliner-rows-preserve-depth-zoom-and-collapse-controls" outliner_rows_preserve_depth_zoom_and_collapse_controls; case "journal-child-blocks-keep-zoom-navigation" journal_child_blocks_keep_zoom_navigation; case "outliner-row-splices-update-the-existing-keyed-projection" outliner_row_splices_update_the_existing_keyed_projection; diff --git a/shared/test/logseq_chat/lui_projection_test.ml b/shared/test/logseq_chat/lui_projection_test.ml index 3a4a67ac..59c833a7 100644 --- a/shared/test/logseq_chat/lui_projection_test.ml +++ b/shared/test/logseq_chat/lui_projection_test.ml @@ -69,7 +69,7 @@ let node_route_projection_retains_journal_and_editor_state () = [ "journal" ] let first_authoritative_snapshot_publishes_retained_patch () = - ignore (Native_bridge.initialize 2 1 0); + ignore (Native_bridge.initialize 2 2 0); Fun.protect ~finally:(fun () -> ignore (Native_bridge.dispose ())) (fun () -> @@ -97,7 +97,7 @@ let native_session_roundtrip_preserves_draft_assets_and_navigation () = let saved = Native_bridge.encode_ui_session (Model.ui_session original) in - ignore (Native_bridge.initialize 2 1 3); + ignore (Native_bridge.initialize 2 2 3); Fun.protect ~finally:(fun () -> ignore (Native_bridge.dispose ())) (fun () -> @@ -112,7 +112,7 @@ let native_session_roundtrip_preserves_draft_assets_and_navigation () = let quick_actions_clear_selection_before_opening_native_recorder () = List.iter (fun kind -> - ignore (Native_bridge.initialize 2 1 3); + ignore (Native_bridge.initialize 2 2 3); Fun.protect ~finally:(fun () -> ignore (Native_bridge.dispose ())) (fun () -> From 3face94850983fef9a0d61cf6ea7450846d7da54 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 13:51:53 +0000 Subject: [PATCH 2/3] Bump lui pin to the merged logseq/lui#135 SHA (afeb4cc) --- logseq_chat.opam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logseq_chat.opam b/logseq_chat.opam index cf4041a3..eb04dd88 100644 --- a/logseq_chat.opam +++ b/logseq_chat.opam @@ -31,7 +31,7 @@ pin-depends: [ ["melange-transit-core.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] ["melange-transit-native.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] ["melange-transit-melange.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] - ["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#264aa9f67da0d0d3e413f9dfe970785e7286f23c"] + ["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#afeb4cc82c346deddb304248fd74d102b5907863"] ["drive.dev" "git+https://github.com/logseq/drive.git#7f407b07f18e68596cb0dcb3ba1781cb0572a2bb"] ["ocaml-signal.0.1.0" "git+https://github.com/logseq/ocaml-signal.git#868c1459f865b4ba3b227eb440dba40f1c812899"] ["mldoc.dev" "git+https://github.com/logseq/mldoc.git#553dea6ed8694352527a189747f787365469c9cb"] From 43a515388bf2846b1ec4e37c57fc76b621c868e7 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 6 Oct 2026 15:19:19 +0000 Subject: [PATCH 3/3] fix(android): declare extension prop/event schemas in registrations The minimal LuiExtensionRegistry.register overload defaults to an empty properties list, so every set-extension-prop op was rejected (e.g. native-navigation-stack.depth), leaving the retained tree wedged after the first navigation patch. Declare the schemas matching each fingerprint. --- .../chat/ui/extensions/ChatExtensions.kt | 129 ++++++++++++++++-- 1 file changed, 121 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt b/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt index 5efc898b..ee8eb465 100644 --- a/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt +++ b/android/app/src/main/kotlin/com/logseq/chat/ui/extensions/ChatExtensions.kt @@ -26,7 +26,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import dev.lui.LuiExtensionContext +import dev.lui.LuiExtensionEventSchema +import dev.lui.LuiExtensionProperty import dev.lui.LuiExtensionRegistry +import dev.lui.LuiExtensionValueKind // Port of the former logseq_chat_extensions.dart — the chat-specific // extension hosts. Fingerprints are generated by the OCaml core and must @@ -68,17 +71,127 @@ private const val SEARCH_FINGERPRINT = fun logseqChatExtensionRegistry( resolveAssetPath: (String) -> String = { it }, ): LuiExtensionRegistry = LuiExtensionRegistry().apply { - register("outliner-editor", EDITOR_FINGERPRINT) { context -> - OutlinerEditor(context) + register( + "outliner-editor", + EDITOR_FINGERPRINT, + properties = listOf( + LuiExtensionProperty("caret-utf16-offset", LuiExtensionValueKind.INTEGER), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING), + LuiExtensionProperty("block-id", LuiExtensionValueKind.STRING), + ), + events = listOf( + LuiExtensionEventSchema( + "text-change", + listOf( + LuiExtensionProperty("caret-utf16-offset", LuiExtensionValueKind.INTEGER, required = true), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING, required = true), + ), + ), + LuiExtensionEventSchema( + "caret-change", + listOf( + LuiExtensionProperty("caret-utf16-offset", LuiExtensionValueKind.INTEGER, required = true), + ), + ), + LuiExtensionEventSchema( + "return", + listOf( + LuiExtensionProperty("caret-utf16-offset", LuiExtensionValueKind.INTEGER, required = true), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING, required = true), + ), + ), + LuiExtensionEventSchema( + "backspace", + listOf( + LuiExtensionProperty("selection-length", LuiExtensionValueKind.INTEGER, required = true), + LuiExtensionProperty("caret-utf16-offset", LuiExtensionValueKind.INTEGER, required = true), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING, required = true), + ), + ), + ), + ) { + OutlinerEditor(this) } - register("outliner-block-content", RICH_FINGERPRINT) { context -> - OutlinerBlockContent(context, resolveAssetPath) + register( + "outliner-block-content", + RICH_FINGERPRINT, + properties = listOf( + LuiExtensionProperty("asset-type", LuiExtensionValueKind.STRING), + LuiExtensionProperty("local-path", LuiExtensionValueKind.STRING), + LuiExtensionProperty("markup-json", LuiExtensionValueKind.STRING), + LuiExtensionProperty("is-completed", LuiExtensionValueKind.BOOLEAN), + LuiExtensionProperty("youtube-target-url", LuiExtensionValueKind.STRING), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING), + LuiExtensionProperty("block-id", LuiExtensionValueKind.STRING), + LuiExtensionProperty("is-asset", LuiExtensionValueKind.BOOLEAN), + ), + events = listOf( + LuiExtensionEventSchema( + "drag-start", + listOf(LuiExtensionProperty("uuid", LuiExtensionValueKind.STRING, required = true)), + ), + LuiExtensionEventSchema( + "drop", + listOf( + LuiExtensionProperty("uuid", LuiExtensionValueKind.STRING, required = true), + LuiExtensionProperty("placement", LuiExtensionValueKind.STRING, required = true), + ), + ), + LuiExtensionEventSchema( + "edit", + listOf(LuiExtensionProperty("uuid", LuiExtensionValueKind.STRING, required = true)), + ), + LuiExtensionEventSchema( + "open-node", + listOf(LuiExtensionProperty("uuid", LuiExtensionValueKind.STRING, required = true)), + ), + ), + ) { + OutlinerBlockContent(this, resolveAssetPath) } - register("native-navigation-stack", NAVIGATION_FINGERPRINT) { context -> - NavigationBoundary(context) + register( + "native-navigation-stack", + NAVIGATION_FINGERPRINT, + acceptsStandardChildren = true, + properties = listOf( + LuiExtensionProperty("composer-dismissal-enabled", LuiExtensionValueKind.BOOLEAN), + LuiExtensionProperty("bottom-occupies-layout-space", LuiExtensionValueKind.BOOLEAN), + LuiExtensionProperty("depth", LuiExtensionValueKind.INTEGER), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING), + ), + events = listOf( + LuiExtensionEventSchema("dismiss-composer"), + LuiExtensionEventSchema( + "back", + listOf(LuiExtensionProperty("count", LuiExtensionValueKind.INTEGER, required = true)), + ), + ), + ) { + NavigationBoundary(this) } - register("native-search-presentation", SEARCH_FINGERPRINT) { context -> - SearchPresentation(context) + register( + "native-search-presentation", + SEARCH_FINGERPRINT, + acceptsStandardChildren = true, + properties = listOf( + LuiExtensionProperty("depth", LuiExtensionValueKind.INTEGER), + LuiExtensionProperty("query", LuiExtensionValueKind.STRING), + LuiExtensionProperty("title", LuiExtensionValueKind.STRING), + LuiExtensionProperty("presented", LuiExtensionValueKind.BOOLEAN), + ), + events = listOf( + LuiExtensionEventSchema( + "query-changed", + listOf(LuiExtensionProperty("query", LuiExtensionValueKind.STRING, required = true)), + ), + LuiExtensionEventSchema( + "back", + listOf(LuiExtensionProperty("count", LuiExtensionValueKind.INTEGER, required = true)), + ), + LuiExtensionEventSchema("dismiss"), + ), + ) { + SearchPresentation(this) } }