@lucent-lang/lucent 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/bin/lucent.cjs +2 -0
  2. package/dist/{bench-eEez4jhQ.js → bench-BLZugR80.js} +1 -1
  3. package/dist/{build-CpUjeHFz.js → build-C1Uct_mH.js} +1 -1
  4. package/dist/{build-check-BA1ZEp9s.js → build-check-hlYTmPRd.js} +1 -1
  5. package/dist/{check-BZx3FgI6.js → check-BksBP--n.js} +1 -1
  6. package/dist/cli.js +15 -11
  7. package/dist/{codes-D-6LxUqD.js → codes-B6AgFgAq.js} +24 -2
  8. package/dist/compiler.js +3 -3
  9. package/dist/{dashboard-CC-HXhcd.js → dashboard-CzOJghrS.js} +1 -1
  10. package/dist/{dev-DI5yL8zx.js → dev-CtaNF3sP.js} +3 -3
  11. package/dist/{explain-Byit07F-.js → explain-ImANcx5a.js} +1 -1
  12. package/dist/{pipeline-BM_VM2X_.js → pipeline-DXFTV3Mg.js} +21 -7
  13. package/dist/{project-BKNoTEEp.js → project-Bp9I-fLP.js} +254 -5
  14. package/dist/{sdk-coverage-iiv3JqMp.js → sdk-coverage-BpbKBG_O.js} +33 -4
  15. package/dist/{sdk-diff-ByJ1Gw-F.js → sdk-diff-BueKDJQB.js} +3 -3
  16. package/dist/{sdk-lock-BQn7IyRs.js → sdk-lock-CKLnZ0Wl.js} +2 -2
  17. package/dist/{sdk-prefetch-Bv2pNega.js → sdk-prefetch-rk6MuMe6.js} +2 -2
  18. package/dist/{sdk-search-CPGJm_Ne.js → sdk-search-U_2M81lq.js} +2 -2
  19. package/dist/{sdk-show-BUkLqRnA.js → sdk-show-DG6H40fp.js} +2 -2
  20. package/dist/{sdk-usage-0ZFMAYg1.js → sdk-usage-Cc7u1i0L.js} +1 -1
  21. package/dist/{src-ChnmWLwn.js → src-DMbXwPlK.js} +2209 -546
  22. package/lib/sdk/android.d.ts +12 -0
  23. package/lib/sdk/compose.d.ts +17 -4
  24. package/lib/sdk/ui.d.ts +21 -0
  25. package/package.json +1 -1
  26. package/runtime/cpp/lucent/platform/android.cpp +152 -16
  27. package/runtime/cpp/lucent/platform/android.h +79 -2
  28. package/runtime/cpp/lucent/platform/ios.h +7 -0
  29. package/runtime/cpp/lucent/platform/ios_ui.h +20 -0
  30. package/runtime/cpp/rn/LucentComponentView.mm +12 -0
  31. package/runtime/cpp/rn/LucentViewSizing.h +9 -1
@@ -1,10 +1,22 @@
1
1
  // lucent:android — Android helpers for platform code.
2
2
  import type { Activity, Instrumentation_ActivityResult } from "lucent:android/android.app";
3
3
  import type { Context, Intent } from "lucent:android/android.content";
4
+ import type { Throwable } from "lucent:android/java.lang";
4
5
 
5
6
  /** The application Context. */
6
7
  export declare function appContext(): Context;
7
8
 
9
+ /**
10
+ * The Error Lucent makes of `throwable` when a call throws it: its message
11
+ * the exception's (or its class name, without one) and its `code` the class
12
+ * name (`java.lang.IllegalStateException`). An exception carrying a Lucent
13
+ * error (one a Lucent suspend function ended a Kotlin call with) gives
14
+ * that error.
15
+ * For an adapter whose callback API reports failure with a Throwable:
16
+ * `reject(errorOf(e))` rejects as the call would have thrown.
17
+ */
18
+ export declare function errorOf(throwable: Throwable): Error;
19
+
8
20
  /** Whether the device runs at least API level `api` (`Build.VERSION.SDK_INT >= api`). */
9
21
  export declare function available(platform: "android", api: number): boolean;
10
22
 
@@ -32,8 +32,8 @@
32
32
  *
33
33
  * @experimental
34
34
  */
35
- import type { ViewGroup } from "lucent:android/android.view";
36
- import type { Bound } from "lucent:ui";
35
+ import type { View, ViewGroup } from "lucent:android/android.view";
36
+ import type { Bound, NativeViewTag } from "lucent:ui";
37
37
 
38
38
  /** Kotlin's Float: a number, converted where the body passes it. */
39
39
  export type Float = number & { readonly "lucent:compose.Float"?: never };
@@ -76,15 +76,28 @@ export type ScopedChildren<S> = Shown | ((scope: S) => Shown);
76
76
 
77
77
  /**
78
78
  * How TypeScript types an Android file's JSX: an element is a composable,
79
- * a function of its props whose children are its `children` prop.
79
+ * a function of its props whose children are its `children` prop, or a
80
+ * view class (T48), typed with its declarations' `~jsx` attributes, as
81
+ * native-jsx-dts.ts writes the generated toolkits' namespaces.
80
82
  */
81
83
  export declare namespace JSX {
82
- type Element = Composed;
84
+ type Element = Composed & View;
85
+
86
+ type ElementType = ((props: never) => Composed) | NativeViewTag<View>;
83
87
 
84
88
  interface ElementChildrenAttribute {
85
89
  children: {};
86
90
  }
87
91
 
92
+ interface ElementAttributesProperty {
93
+ "~jsx": {};
94
+ }
95
+
96
+ interface IntrinsicClassAttributes<T> {
97
+ /** Makes the view, where its class has no frame or Context constructor to make it with. */
98
+ create?: () => T;
99
+ }
100
+
88
101
  interface IntrinsicElements {}
89
102
  }
90
103
 
package/lib/sdk/ui.d.ts CHANGED
@@ -97,6 +97,27 @@ export interface Children {
97
97
  */
98
98
  export declare function slot<T extends object>(): T;
99
99
 
100
+ /** A native view class, as a JSX tag: any class whose instances are `V`. */
101
+ export type NativeViewTag<V> = abstract new (...args: never[]) => V;
102
+
103
+ /**
104
+ * The JSX attributes of a native view class: what each class in its
105
+ * hierarchy gives under its own `~jsx:<module>.<class>` key (derived by
106
+ * rule from its declarations), together. Each key's type takes the class
107
+ * itself (`this`), so a control event's handler gets the tag's class.
108
+ */
109
+ export type NativeAttributes<T> = [
110
+ {
111
+ [K in keyof T]-?: K extends `~jsx:${string}` ? (x: NonNullable<T[K]>) => void : never;
112
+ }[keyof T],
113
+ ] extends [never]
114
+ ? {}
115
+ : {
116
+ [K in keyof T]-?: K extends `~jsx:${string}` ? (x: NonNullable<T[K]>) => void : never;
117
+ }[keyof T] extends (x: infer I) => void
118
+ ? I
119
+ : never;
120
+
100
121
  /**
101
122
  * Has the view measured again, once the main thread's current work ends:
102
123
  * for a change to its native content its host would not hear of. The host
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucent-lang/lucent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Write native React Native modules in TypeScript: the Lucent compiler, CLI, Metro integration, Expo config plugin and editor plugin",
5
5
  "homepage": "https://lucent-lang.dev",
6
6
  "bugs": "https://github.com/Fausto95/lucent/issues",
@@ -1,12 +1,16 @@
1
1
  // Lucent runtime — Android: JNI environment, classes, errors, the main
2
- // Looper. Built only into Android apps (fbjni comes with React Native).
3
- #ifdef __ANDROID__
2
+ // Looper. Built into Android apps (fbjni comes with React Native); its JNI
3
+ // alone also into the desktop JNI host the tests run on a JVM
4
+ // (LUCENT_JNI_HOST), which defines env(), appContext() and available().
5
+ #if defined(__ANDROID__) || defined(LUCENT_JNI_HOST)
4
6
 
5
7
  #include "android.h"
6
8
 
9
+ #ifdef __ANDROID__
7
10
  #include <fbjni/NativeRunnable.h>
8
11
  #include <fbjni/fbjni.h>
9
12
  #include <sys/system_properties.h>
13
+ #endif
10
14
 
11
15
  #include <atomic>
12
16
  #include <cstdint>
@@ -19,7 +23,9 @@
19
23
 
20
24
  namespace lucent::jni {
21
25
 
26
+ #ifdef __ANDROID__
22
27
  JNIEnv* env() { return facebook::jni::Environment::ensureCurrentThreadIsAttached(); }
28
+ #endif
23
29
 
24
30
  namespace {
25
31
 
@@ -103,12 +109,14 @@ std::vector<Thrown>& thrown() {
103
109
  return *v;
104
110
  }
105
111
 
106
- /// The Lucent error `t` carries, removed from the list; none for other exceptions.
107
- std::optional<Error> takeThrown(JNIEnv* e, jobject t) {
112
+ /// The Lucent error `t` carries, removed from the list when `take` (Lucent
113
+ /// has caught the exception); none for other exceptions.
114
+ std::optional<Error> findThrown(JNIEnv* e, jobject t, bool take) {
108
115
  std::lock_guard<std::mutex> g(thrownMutex);
109
116
  auto& v = thrown();
110
117
  for (auto it = v.begin(); it != v.end(); ++it) {
111
118
  if (!e->IsSameObject(it->exception, t)) continue;
119
+ if (!take) return it->error;
112
120
 
113
121
  Error error = std::move(it->error);
114
122
  e->DeleteWeakGlobalRef(it->exception);
@@ -120,13 +128,13 @@ std::optional<Error> takeThrown(JNIEnv* e, jobject t) {
120
128
 
121
129
  } // namespace
122
130
 
123
- void throwToJava(JNIEnv* e, Error error) {
131
+ jthrowable throwableOf(JNIEnv* e, Error error) {
124
132
  static jclass cls = findClass("java/lang/RuntimeException");
125
133
  static jmethodID init = method(cls, "<init>", "(Ljava/lang/String;)V");
126
134
  jstring message = toJString(e, error->message);
127
135
  auto exception = static_cast<jthrowable>(e->NewObject(cls, init, message));
128
136
  e->DeleteLocalRef(message);
129
- if (!exception) return; // A pending OutOfMemoryError ends the call instead.
137
+ if (!exception) return nullptr; // A pending OutOfMemoryError ends the call instead.
130
138
 
131
139
  {
132
140
  std::lock_guard<std::mutex> g(thrownMutex);
@@ -139,37 +147,159 @@ void throwToJava(JNIEnv* e, Error error) {
139
147
  });
140
148
  v.push_back({e->NewWeakGlobalRef(exception), std::move(error)});
141
149
  }
150
+ return exception;
151
+ }
152
+
153
+ void throwToJava(JNIEnv* e, Error error) {
154
+ jthrowable exception = throwableOf(e, std::move(error));
155
+ if (!exception) return;
156
+
142
157
  e->Throw(exception);
143
158
  e->DeleteLocalRef(exception);
144
159
  }
145
160
 
161
+ namespace {
162
+
163
+ /// A primitive of descriptor `d`, from its boxed object.
164
+ jvalue unboxed(JNIEnv* e, char d, jobject boxed) {
165
+ jvalue v{};
166
+ switch (d) {
167
+ case 'Z': v.z = unboxBoolean(e, boxed) ? JNI_TRUE : JNI_FALSE; break;
168
+ case 'J': v.j = unboxLong(e, boxed); break;
169
+ case 'I': v.i = static_cast<jint>(unboxNumber(e, boxed)); break;
170
+ case 'S': v.s = static_cast<jshort>(unboxNumber(e, boxed)); break;
171
+ case 'B': v.b = static_cast<jbyte>(unboxNumber(e, boxed)); break;
172
+ case 'C': v.c = static_cast<jchar>(unboxNumber(e, boxed)); break;
173
+ case 'F': v.f = static_cast<jfloat>(unboxNumber(e, boxed)); break;
174
+ case 'D': v.d = unboxNumber(e, boxed); break;
175
+ default: v.l = boxed;
176
+ }
177
+ return v;
178
+ }
179
+
180
+ } // namespace
181
+
182
+ jobject boxValueClass(JNIEnv* e, const char* cls, const char* underlying, jobject value) {
183
+ if (!value && underlying[0] != 'L' && underlying[0] != '[') return nullptr;
184
+
185
+ jclass c = findClass(cls);
186
+ std::string sig = std::string("(") + underlying + ")L" + cls + ";";
187
+ jmethodID box = staticMethod(c, "box-impl", sig.c_str());
188
+ jvalue v = unboxed(e, underlying[0], value);
189
+ jobject r = e->CallStaticObjectMethodA(c, box, &v);
190
+ check(e);
191
+ return r;
192
+ }
193
+
194
+ jobject unboxValueClass(JNIEnv* e, const char* cls, const char* underlying, jobject boxed) {
195
+ if (!boxed) return nullptr;
196
+
197
+ jmethodID unbox = method(findClass(cls), "unbox-impl", (std::string("()") + underlying).c_str());
198
+
199
+ // Each read checked before the boxing calls Java again.
200
+ auto read = [e](auto v) {
201
+ check(e);
202
+ return v;
203
+ };
204
+ switch (underlying[0]) {
205
+ case 'Z': return boxBoolean(e, read(e->CallBooleanMethod(boxed, unbox)) == JNI_TRUE);
206
+ case 'J': return boxLong(e, read(e->CallLongMethod(boxed, unbox)));
207
+ case 'I': return boxInt(e, read(e->CallIntMethod(boxed, unbox)));
208
+ case 'S': return boxShort(e, read(e->CallShortMethod(boxed, unbox)));
209
+ case 'B': return boxByte(e, read(e->CallByteMethod(boxed, unbox)));
210
+ case 'C': return boxChar(e, read(e->CallCharMethod(boxed, unbox)));
211
+ case 'F': return boxFloat(e, read(e->CallFloatMethod(boxed, unbox)));
212
+ case 'D': return boxDouble(e, read(e->CallDoubleMethod(boxed, unbox)));
213
+ default: return read(e->CallObjectMethod(boxed, unbox));
214
+ }
215
+ }
216
+
217
+ jobject suspendedCall(JNIEnv* e, jobject continuation, const std::function<void(Resume)>& start) {
218
+ static jclass intrinsics = findClass("kotlin/coroutines/intrinsics/IntrinsicsKt");
219
+ static jmethodID intercepted = staticMethod(
220
+ intrinsics, "intercepted", "(Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;");
221
+ static jclass safeClass = findClass("kotlin/coroutines/SafeContinuation");
222
+ static jmethodID safeInit = method(safeClass, "<init>", "(Lkotlin/coroutines/Continuation;)V");
223
+ static jmethodID getOrThrow = method(safeClass, "getOrThrow", "()Ljava/lang/Object;");
224
+ static jmethodID resumeWith = method(safeClass, "resumeWith", "(Ljava/lang/Object;)V");
225
+ static jclass results = findClass("kotlin/ResultKt");
226
+ static jmethodID failure = staticMethod(results, "createFailure", "(Ljava/lang/Throwable;)Ljava/lang/Object;");
227
+
228
+ // As suspendCoroutine does: resumed through its dispatcher, never on the Lucent thread,
229
+ // and a result that arrives before the call returns is its result.
230
+ jobject dispatched = e->CallStaticObjectMethod(intrinsics, intercepted, continuation);
231
+ check(e);
232
+ jobject local = e->NewObject(safeClass, safeInit, dispatched);
233
+ e->DeleteLocalRef(dispatched);
234
+ check(e);
235
+ auto safe = std::make_shared<NativeRef>(wrap(e, e->NewLocalRef(local), "a continuation"));
236
+
237
+ // Kotlin's Result is the value itself, or a failure of the Throwable.
238
+ start([safe](JNIEnv* env, jobject value, const Error* error) {
239
+ jobject result = value;
240
+ if (error) {
241
+ jthrowable t = throwableOf(env, *error);
242
+ result = env->CallStaticObjectMethod(results, failure, t);
243
+ env->DeleteLocalRef(t);
244
+ check(env);
245
+ }
246
+ env->CallVoidMethod(unwrap(*safe), resumeWith, result);
247
+ if (error) env->DeleteLocalRef(result);
248
+ check(env);
249
+ });
250
+
251
+ // COROUTINE_SUSPENDED, or the result already there; a failure already there stays pending.
252
+ jobject r = e->CallObjectMethod(local, getOrThrow);
253
+ e->DeleteLocalRef(local);
254
+ return r;
255
+ }
256
+
257
+ jobject unit(JNIEnv* e) {
258
+ static jclass cls = findClass("kotlin/Unit");
259
+ static jfieldID instance = staticField(cls, "INSTANCE", "Lkotlin/Unit;");
260
+ return e->GetStaticObjectField(cls, instance);
261
+ }
262
+
146
263
  void rethrowPending(JNIEnv* e) {
147
264
  jthrowable t = e->ExceptionOccurred();
148
265
  e->ExceptionClear();
149
266
  throw Exception(errorOf(e, t));
150
267
  }
151
268
 
152
- Error errorOf(JNIEnv* e, jobject t) {
153
- if (auto own = t ? takeThrown(e, t) : std::nullopt) return std::move(*own);
269
+ namespace {
270
+
271
+ /// A Java exception as a Lucent error; a Lucent error it carries is
272
+ /// removed from the list when `take`.
273
+ Error errorOf(JNIEnv* e, jobject t, bool take) {
274
+ if (auto own = t ? findThrown(e, t, take) : std::nullopt) return std::move(*own);
154
275
  if (!t) {
155
276
  // No exception to read: a cancelled task.
156
277
  Error err = makeError(String::fromLatin1("Error"), String::fromLatin1("cancelled"));
157
278
  err->code = String::fromLatin1("java.util.concurrent.CancellationException");
158
279
  return err;
159
280
  }
160
- jclass objectCls = e->FindClass("java/lang/Object");
161
- jobject cls = e->CallObjectMethod(t, e->GetMethodID(objectCls, "getClass", "()Ljava/lang/Class;"));
162
- jclass classCls = e->FindClass("java/lang/Class");
163
- auto name = static_cast<jstring>(e->CallObjectMethod(cls, e->GetMethodID(classCls, "getName", "()Ljava/lang/String;")));
164
- jclass throwableCls = e->FindClass("java/lang/Throwable");
165
- auto message = static_cast<jstring>(e->CallObjectMethod(t, e->GetMethodID(throwableCls, "getMessage", "()Ljava/lang/String;")));
166
- e->ExceptionClear();
281
+ // These throw only when out of memory, which leaves the defaults below:
282
+ // cleared after each call, before the next JNI call (as CheckJNI requires).
283
+ auto read = [e](jobject o, const char* cls, const char* name, const char* sig) {
284
+ jobject r = e->CallObjectMethod(o, e->GetMethodID(e->FindClass(cls), name, sig));
285
+ e->ExceptionClear();
286
+ return r;
287
+ };
288
+ jobject cls = read(t, "java/lang/Object", "getClass", "()Ljava/lang/Class;");
289
+ auto name = static_cast<jstring>(cls ? read(cls, "java/lang/Class", "getName", "()Ljava/lang/String;") : nullptr);
290
+ auto message = static_cast<jstring>(read(t, "java/lang/Throwable", "getMessage", "()Ljava/lang/String;"));
167
291
  String className = name ? fromJString(e, name, "") : String::fromLatin1("java.lang.Throwable");
168
292
  Error err = makeError(String::fromLatin1("Error"), message ? fromJString(e, message, "") : className);
169
293
  err->code = className;
170
294
  return err;
171
295
  }
172
296
 
297
+ } // namespace
298
+
299
+ Error errorOf(JNIEnv* e, jobject t) { return errorOf(e, t, true); }
300
+
301
+ Error errorOf(const NativeRef& throwable) { return errorOf(env(), unwrap(throwable), false); }
302
+
173
303
  void close(const NativeRef& closeable) {
174
304
  JNIEnv* e = env();
175
305
  static jmethodID m = method(findClass("java/lang/AutoCloseable"), "close", "()V");
@@ -566,6 +696,7 @@ jobject boxDouble(JNIEnv* e, jdouble v) { return boxWith(e, "java/lang/Double",
566
696
  jobject boxFloat(JNIEnv* e, jfloat v) { return boxWith(e, "java/lang/Float", "(F)Ljava/lang/Float;", jvalue{.f = v}); }
567
697
  jobject boxBoolean(JNIEnv* e, bool v) { return boxWith(e, "java/lang/Boolean", "(Z)Ljava/lang/Boolean;", jvalue{.z = static_cast<jboolean>(v ? JNI_TRUE : JNI_FALSE)}); }
568
698
 
699
+ #ifdef __ANDROID__
569
700
  NativeRef appContext() {
570
701
  static NativeRef* app = [] {
571
702
  JNIEnv* e = env();
@@ -581,6 +712,7 @@ NativeRef appContext() {
581
712
  }();
582
713
  return *app;
583
714
  }
715
+ #endif
584
716
 
585
717
  namespace {
586
718
  thread_local jobject hostingView = nullptr;
@@ -601,6 +733,7 @@ Opt<NativeRef> hostContext() {
601
733
  return wrapOpt(e, context);
602
734
  }
603
735
 
736
+ #ifdef __ANDROID__
604
737
  bool available(double api) {
605
738
  static int level = [] {
606
739
  char value[PROP_VALUE_MAX] = {0};
@@ -609,9 +742,11 @@ bool available(double api) {
609
742
  }();
610
743
  return level >= api;
611
744
  }
745
+ #endif
612
746
 
613
747
  } // namespace lucent::jni
614
748
 
749
+ #ifdef __ANDROID__
615
750
  namespace lucent {
616
751
 
617
752
  void postToMain(std::function<void()> job) {
@@ -646,5 +781,6 @@ bool onMainThread() {
646
781
  }
647
782
 
648
783
  } // namespace lucent
784
+ #endif
649
785
 
650
- #endif // __ANDROID__
786
+ #endif
@@ -37,6 +37,9 @@ inline void check(JNIEnv* env) {
37
37
  }
38
38
  /// A Java exception (a Throwable) as a Lucent error, coded as rethrowPending does.
39
39
  Error errorOf(JNIEnv* env, jobject throwable);
40
+ /// `errorOf(throwable)` from lucent:android: the same error, but a Lucent
41
+ /// error the exception carries stays with it, for the next call to read.
42
+ Error errorOf(const NativeRef& throwable);
40
43
 
41
44
  /// Closes an AutoCloseable (a Closeable, a Cursor…): how a `using`
42
45
  /// declaration disposes one. Throws what close() throws.
@@ -204,12 +207,80 @@ jobject boxDouble(JNIEnv* env, jdouble v);
204
207
  jobject boxFloat(JNIEnv* env, jfloat v);
205
208
  jobject boxBoolean(JNIEnv* env, bool v);
206
209
 
210
+ /**
211
+ * A Kotlin value class `cls` (`dev/x/Score`) the JVM passes as its
212
+ * underlying value, whose descriptor is `underlying` (`I`): its boxed
213
+ * object of `value` (boxed itself, as a proxy receives it: an Integer),
214
+ * through the class's `box-impl`; and the underlying value of a boxed
215
+ * object, boxed as a proxy returns it, through its `unbox-impl`. Local
216
+ * references.
217
+ */
218
+ jobject boxValueClass(JNIEnv* env, const char* cls, const char* underlying, jobject value);
219
+ jobject unboxValueClass(JNIEnv* env, const char* cls, const char* underlying, jobject boxed);
220
+
207
221
  // --- Kotlin coroutines through generated shims ----------------------------------------
208
222
 
209
- /// Throws `error` to Java as a java.lang.RuntimeException carrying its
210
- /// message, which errorOf reads back as `error` itself.
223
+ /// `error` as a java.lang.RuntimeException carrying its message, which
224
+ /// errorOf reads back as `error` itself. A local reference.
225
+ jthrowable throwableOf(JNIEnv* env, Error error);
226
+
227
+ /// Throws `error` to Java, as throwableOf makes it.
211
228
  void throwToJava(JNIEnv* env, Error error);
212
229
 
230
+ /**
231
+ * How a Kotlin suspend function a Lucent class implements ends: resumes
232
+ * its continuation with a value (a Java object, or null), or with the
233
+ * error (see throwableOf). Any thread may call it, once.
234
+ */
235
+ using Resume = std::function<void(JNIEnv* env, jobject value, const Error* error)>;
236
+
237
+ /**
238
+ * A suspend function a Lucent class implements, called through a proxy
239
+ * with `continuation` (its last argument): `start` is given what resumes
240
+ * it, and Kotlin is told the call suspended (COROUTINE_SUSPENDED), whose
241
+ * result it waits for.
242
+ */
243
+ jobject suspendedCall(JNIEnv* env, jobject continuation, const std::function<void(Resume)>& start);
244
+
245
+ /// Kotlin's Unit, what a suspend function giving nothing resumes with. A local reference.
246
+ jobject unit(JNIEnv* env);
247
+
248
+ /**
249
+ * Resumes a suspend call (`resume`) with what `call` gives: a promise's
250
+ * outcome once it settles, else its value now; what it throws is its
251
+ * error. `box(env, value)` makes the Java object of a value. On the
252
+ * Lucent thread, where the call runs.
253
+ */
254
+ template <class F, class B>
255
+ void resumeWithResult(const Resume& resume, F call, B box) {
256
+ auto settle = [resume, box](const auto& value) {
257
+ JNIEnv* e = env();
258
+ LocalFrame frame(e);
259
+ resume(e, box(e, value), nullptr);
260
+ };
261
+ auto fail = [resume](const Error& error) {
262
+ JNIEnv* e = env();
263
+ LocalFrame frame(e);
264
+ resume(e, nullptr, &error);
265
+ };
266
+
267
+ try {
268
+ auto r = call();
269
+ if constexpr (IsPromise<decltype(r)>::value) {
270
+ r.onSettled([r, settle, fail] {
271
+ if (r.fulfilled())
272
+ settle(r.value());
273
+ else
274
+ fail(r.error());
275
+ });
276
+ } else {
277
+ settle(r);
278
+ }
279
+ } catch (...) {
280
+ fail(currentError(std::current_exception()));
281
+ }
282
+ }
283
+
213
284
  /**
214
285
  * A Lucent function a generated shim runs as a Kotlin suspend function (a
215
286
  * suspend function argument, a fun interface's suspending function), which
@@ -291,6 +362,12 @@ NativeRef appContext();
291
362
  /// outside any setup.
292
363
  Opt<NativeRef> hostContext();
293
364
 
365
+ /// The Context a view is made with: the hosting view's (its Activity's theme), else the app's.
366
+ inline NativeRef viewContext() {
367
+ Opt<NativeRef> host = hostContext();
368
+ return host.has() ? host.get() : appContext();
369
+ }
370
+
294
371
  /**
295
372
  * While one lives, `view` (a local reference) hosts the mount whose setup
296
373
  * runs on this thread, and hostContext() is its context: the Android host
@@ -90,6 +90,13 @@ NSArray* toNSArray(const Array<T>& a, F toObject) {
90
90
  }
91
91
  return out;
92
92
  }
93
+ /// An array of `items` (objects, never nil): a Swift tuple's elements, as its shim takes them.
94
+ template <class... T>
95
+ NSArray* toNSArrayOf(T... items) {
96
+ id all[] = {items...};
97
+ return [NSArray arrayWithObjects:all count:sizeof...(T)];
98
+ }
99
+
93
100
  template <class T, class F>
94
101
  Array<T> fromNSArray(NSArray* a, F fromObject, const char* what) {
95
102
  if (!a) returnedNil(what);
@@ -189,4 +189,24 @@ Fn<void()> onAppEvent(const String& event, Fn<void()> listener, Opt<AbortSignal>
189
189
  /// scene's session's persistent identifier.
190
190
  Fn<void()> onSceneEvent(const String& event, Fn<void(String)> listener, Opt<AbortSignal> signal = {});
191
191
 
192
+ /**
193
+ * Calls `handler` with the control on each of `events` (a UIControlEvents
194
+ * mask): a JSX element's control event (T48), as the UIAction UIKit
195
+ * registers. Gives the action, for removeControlAction: the control keeps
196
+ * it, and through it the handler, until then.
197
+ */
198
+ template <class Handler>
199
+ NativeRef addControlAction(id control, NSUInteger events, Handler handler) {
200
+ UIAction* action = [UIAction actionWithHandler:^(UIAction* a) {
201
+ handler(a.sender);
202
+ }];
203
+ [(UIControl*)control addAction:action forControlEvents:(UIControlEvents)events];
204
+ return wrap(action, "a control's action");
205
+ }
206
+
207
+ /** Takes back an action addControlAction registered. */
208
+ inline void removeControlAction(id control, const NativeRef& action, NSUInteger events) {
209
+ [(UIControl*)control removeAction:(UIAction*)unwrap(action) forControlEvents:(UIControlEvents)events];
210
+ }
211
+
192
212
  } // namespace lucent::objc
@@ -286,6 +286,18 @@ void inMainContext(const char* where, F&& work) {
286
286
  LucentComponentView *view = weakSelf;
287
287
 
288
288
  return view ? [view lucentMeasure:constraints] : std::nullopt;
289
+ },
290
+ // The content changed, a native timer's or an animation's code too, not
291
+ // only a commit's or a command's: it may have moved the slot, which lays
292
+ // out again on UIKit's next pass and reports where it is, past the
293
+ // reports' bound (TA26).
294
+ [weakSelf] {
295
+ LucentComponentView *view = weakSelf;
296
+
297
+ if (!view) return;
298
+
299
+ view->_place.contentChanged();
300
+ [view->_children.slot setNeedsLayout];
289
301
  });
290
302
  _place.start(static_cast<float>(RCTScreenScale()), static_cast<int>(self.tag));
291
303
  [self.lucentChildren startMount];
@@ -199,11 +199,15 @@ class HostSizing {
199
199
 
200
200
  /// A mount starts on the view `tag` names, at `scale` pixels per point,
201
201
  /// its content measured by `measure`: revision 1, measured from scratch.
202
- void start(float scale, int tag, Measure measure) {
202
+ /// `changed`, if any, runs after each change of the content is measured:
203
+ /// what else the host follows the content for (its slot, which the
204
+ /// content may have moved).
205
+ void start(float scale, int tag, Measure measure, std::function<void()> changed = nullptr) {
203
206
  measurer_ = sizing::Measurer(scale);
204
207
  lastPost_.reset();
205
208
  tag_ = tag;
206
209
  measure_ = std::move(measure);
210
+ changed_ = std::move(changed);
207
211
  content_ = ui::Content::create([this] { contentChanged(); });
208
212
  traceSizing("%d start", tag_);
209
213
  }
@@ -214,6 +218,7 @@ class HostSizing {
214
218
 
215
219
  content_.reset();
216
220
  measure_ = nullptr;
221
+ changed_ = nullptr;
217
222
  state_.reset();
218
223
  lastPost_.reset();
219
224
  }
@@ -246,6 +251,8 @@ class HostSizing {
246
251
  measurer_.contentChanged();
247
252
  traceSizing("%d content rev %llu", tag_, static_cast<unsigned long long>(measurer_.revision()));
248
253
  measure();
254
+
255
+ if (changed_) changed_();
249
256
  }
250
257
 
251
258
  /// Measures if the state asks for constraints this content revision has
@@ -338,6 +345,7 @@ class HostSizing {
338
345
  sizing::Measurer measurer_;
339
346
  std::optional<Post> lastPost_;
340
347
  Measure measure_;
348
+ std::function<void()> changed_;
341
349
  std::shared_ptr<ui::Content> content_;
342
350
  int tag_ = 0;
343
351
  };