craft-native 0.0.141 → 0.0.143

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.
@@ -13,6 +13,7 @@ import android.graphics.BitmapFactory
13
13
  import android.graphics.Canvas
14
14
  import android.graphics.Color
15
15
  import android.graphics.DashPathEffect
16
+ import android.graphics.Matrix
16
17
  import android.graphics.Paint
17
18
  import android.graphics.Path
18
19
  import android.graphics.Typeface
@@ -217,16 +218,36 @@ private class CraftNativeSvgView(context: Context) : View(context) {
217
218
  setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec))
218
219
  }
219
220
 
221
+ /** viewBox units to the view's pixels, set by each draw. */
222
+ private val viewTransform = Matrix()
223
+ private val inverseTransform = Matrix()
224
+
220
225
  override fun onDraw(canvas: Canvas) {
221
226
  super.onDraw(canvas)
222
227
  val box = viewBox() ?: return
223
228
  if (box.first <= 0 || box.second <= 0) return
224
- val scale = min(width.toFloat() / box.first, height.toFloat() / box.second)
225
- if (!scale.isFinite() || scale <= 0f) return
229
+ // Centred at one scale (SVG's default, `xMidYMid meet`), or each axis
230
+ // stretched to fill (`preserveAspectRatio="none"`).
231
+ val stretch = drawing.optJSONObject("props")?.optString("preserveAspectRatio")?.trim() == "none"
232
+ var sx = width.toFloat() / box.first
233
+ var sy = height.toFloat() / box.second
234
+ var dx = 0f
235
+ var dy = 0f
236
+ if (!stretch) {
237
+ val scale = min(sx, sy)
238
+ sx = scale
239
+ sy = scale
240
+ dx = (width - box.first * scale) / 2f
241
+ dy = (height - box.second * scale) / 2f
242
+ }
243
+ if (!sx.isFinite() || !sy.isFinite() || sx <= 0f || sy <= 0f) return
244
+ viewTransform.reset()
245
+ viewTransform.postTranslate(-box.third, -box.fourth)
246
+ viewTransform.postScale(sx, sy)
247
+ viewTransform.postTranslate(dx, dy)
248
+ viewTransform.invert(inverseTransform)
226
249
  canvas.save()
227
- canvas.translate((width - box.first * scale) / 2f, (height - box.second * scale) / 2f)
228
- canvas.scale(scale, scale)
229
- canvas.translate(-box.third, -box.fourth)
250
+ canvas.concat(viewTransform)
230
251
  drawNodes(canvas, drawing.optJSONArray("children") ?: JSONArray(), SvgPaint().merge(drawing))
231
252
  canvas.restore()
232
253
  }
@@ -310,7 +331,17 @@ private class CraftNativeSvgView(context: Context) : View(context) {
310
331
  }
311
332
  }
312
333
  }
313
- canvas.drawPath(path, stroke)
334
+ // `vector-effect="non-scaling-stroke"`: the stroke is in the view's
335
+ // pixels, so a stretched chart's line keeps its width.
336
+ if (props.optString("vectorEffect") == "non-scaling-stroke") {
337
+ val line = Path(path).apply { transform(viewTransform) }
338
+ canvas.save()
339
+ canvas.concat(inverseTransform)
340
+ canvas.drawPath(line, stroke)
341
+ canvas.restore()
342
+ } else {
343
+ canvas.drawPath(path, stroke)
344
+ }
314
345
  }
315
346
  }
316
347
 
@@ -443,7 +474,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
443
474
  }.toFloatArray()
444
475
  val sharedLineIds = CraftNative.flexLines(availableMain.toFloat(), gap.toFloat(), wrap, basis, margins)
445
476
  val lineIds = sharedLineIds?.takeIf { ids ->
446
- ids.size >= pending.size && ids.firstOrNull() == 0 && ids.zipWithNext().all { (left, right) -> right >= left }
477
+ ids.size >= pending.size && ids.firstOrNull() == 0 &&
478
+ ids.indices.drop(1).all { index -> ids[index] >= ids[index - 1] }
447
479
  }
448
480
  var line = Line()
449
481
  pending.forEachIndexed { index, entry ->
@@ -1896,6 +1928,7 @@ class MainActivity : AppCompatActivity() {
1896
1928
  if (!screen.controls.containsKey("id:$target")) return false
1897
1929
  makeView(screen, document, target, screen.controls)
1898
1930
  val updated = screen.controls["id:$target"] ?: return false
1931
+ screen.lastLayoutBounds.remove(updated)
1899
1932
  if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
1900
1933
  }
1901
1934
  if (applied.isNotEmpty()) {
@@ -1908,14 +1941,16 @@ class MainActivity : AppCompatActivity() {
1908
1941
  if (!screen.active) return
1909
1942
  val observer = screen.view.viewTreeObserver
1910
1943
  if (observer.isAlive) {
1911
- val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
1912
- override fun onGlobalLayout() {
1913
- if (observer.isAlive) observer.removeOnGlobalLayoutListener(this)
1944
+ val listener = object : ViewTreeObserver.OnPreDrawListener {
1945
+ override fun onPreDraw(): Boolean {
1946
+ if (observer.isAlive) observer.removeOnPreDrawListener(this)
1914
1947
  if (screen.active) emitLayoutEvents(screen)
1948
+ return true
1915
1949
  }
1916
1950
  }
1917
- observer.addOnGlobalLayoutListener(listener)
1951
+ observer.addOnPreDrawListener(listener)
1918
1952
  }
1953
+ screen.view.forceLayout()
1919
1954
  screen.view.requestLayout()
1920
1955
  // A tree that is already settled may not produce a global-layout
1921
1956
  // callback; keep the event path live for that case as well.
@@ -1929,15 +1964,23 @@ class MainActivity : AppCompatActivity() {
1929
1964
  */
1930
1965
  private fun updateParentLayoutStyle(view: View, style: JSONObject): Boolean {
1931
1966
  var parent = view.parent
1967
+ var found = false
1932
1968
  while (parent != null) {
1933
- if (parent is CraftNativeFlexLayout) {
1934
- parent.setChildStyle(view, style, resources.displayMetrics.density)
1935
- parent.requestLayout()
1936
- return true
1969
+ when (parent) {
1970
+ is CraftNativeFlexLayout -> {
1971
+ if (!found) parent.setChildStyle(view, style, resources.displayMetrics.density)
1972
+ parent.forceLayout()
1973
+ parent.requestLayout()
1974
+ found = true
1975
+ }
1976
+ is CraftNativeScrollView -> {
1977
+ parent.forceLayout()
1978
+ parent.requestLayout()
1979
+ }
1937
1980
  }
1938
1981
  parent = parent.parent
1939
1982
  }
1940
- return false
1983
+ return found
1941
1984
  }
1942
1985
 
1943
1986
  private fun renderCommitted(screen: Screen, document: JSONObject) {
package/dist/cli.js CHANGED
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3303
3303
  `);
3304
3304
  }
3305
3305
  // package.json
3306
- var version = "0.0.141";
3306
+ var version = "0.0.143";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.141";
313
+ var version = "0.0.143";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.141";
61
+ var version = "0.0.143";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -38,7 +38,7 @@ enum CraftNativeActions {
38
38
  private static let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
39
39
 
40
40
  static func capabilities(config: CraftConfig) -> [String] {
41
- var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch"]
41
+ var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "dialog"]
42
42
  if config.enableBiometric { values.append("biometric") }
43
43
  if config.enableSecureStorage { values.append("secureStorage") }
44
44
  if config.enableLocalDatabase { values.append("database") }
@@ -146,6 +146,23 @@ enum CraftNativeActions {
146
146
  }
147
147
  databaseQueue.async { complete(requestToken, database(method: method, args: args), completion) }
148
148
  return
149
+ case ("Dialog", _):
150
+ // The system's own alert, confirmation and action sheet, the same
151
+ // ones a page gets (CraftNativeUI.swift), over the native screen.
152
+ let body = args.first as? [String: Any] ?? [:]
153
+ DispatchQueue.main.async {
154
+ switch method {
155
+ case "alert":
156
+ CraftDialogs.alert(body, from: nil) { complete(requestToken, .success(NSNull()), completion) }
157
+ case "confirm":
158
+ CraftDialogs.confirm(body, from: nil) { complete(requestToken, .success($0), completion) }
159
+ case "actionSheet":
160
+ CraftDialogs.actionSheet(body, from: nil) { complete(requestToken, .success($0 ?? NSNull()), completion) }
161
+ default:
162
+ complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Dialog method"), completion)
163
+ }
164
+ }
165
+ return
149
166
  case ("Health", _):
150
167
  // Apple Health, the same as a page's `craft.health` (CraftHealth.swift).
151
168
  guard config.enableHealthKit else {
@@ -525,7 +525,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
525
525
 
526
526
  private static func identity(for node: [String: Any]) -> String? {
527
527
  let props = node["props"] as? [String: Any] ?? [:]
528
- return [node["id"], node["key"], props["key"], props["testID"]]
528
+ return [node["key"], props["key"], props["testID"], node["id"]]
529
529
  .compactMap { $0 as? String }
530
530
  .first { !$0.isEmpty }
531
531
  }
@@ -1789,14 +1789,36 @@ private final class CraftNativeSvgView: UIView {
1789
1789
  }
1790
1790
  }
1791
1791
 
1792
+ /// viewBox units to the view's points, set by each draw.
1793
+ private var viewTransform = CGAffineTransform.identity
1794
+
1795
+ /// viewBox to view: centred at one scale (SVG's default, `xMidYMid meet`),
1796
+ /// or each axis stretched to fill (`preserveAspectRatio="none"`), as a
1797
+ /// chart a page sizes to its width and a fixed height asks for.
1798
+ static func transform(box: CGRect, bounds: CGSize, preserveAspectRatio: String?) -> CGAffineTransform? {
1799
+ var sx = bounds.width / box.width
1800
+ var sy = bounds.height / box.height
1801
+ var dx: CGFloat = 0
1802
+ var dy: CGFloat = 0
1803
+ if preserveAspectRatio?.trimmingCharacters(in: .whitespaces) != "none" {
1804
+ let scale = min(sx, sy)
1805
+ sx = scale
1806
+ sy = scale
1807
+ dx = (bounds.width - box.width * scale) / 2
1808
+ dy = (bounds.height - box.height * scale) / 2
1809
+ }
1810
+ guard sx.isFinite, sy.isFinite, sx > 0, sy > 0 else { return nil }
1811
+ return CGAffineTransform(translationX: dx, y: dy).scaledBy(x: sx, y: sy).translatedBy(x: -box.minX, y: -box.minY)
1812
+ }
1813
+
1792
1814
  override func draw(_ rect: CGRect) {
1793
1815
  guard let context = UIGraphicsGetCurrentContext() else { return }
1816
+ viewTransform = .identity
1794
1817
  if let box = viewBox {
1795
- let scale = min(bounds.width / box.width, bounds.height / box.height)
1796
- guard scale.isFinite, scale > 0 else { return }
1797
- context.translateBy(x: (bounds.width - box.width * scale) / 2, y: (bounds.height - box.height * scale) / 2)
1798
- context.scaleBy(x: scale, y: scale)
1799
- context.translateBy(x: -box.minX, y: -box.minY)
1818
+ let props = drawing["props"] as? [String: Any] ?? [:]
1819
+ guard let transform = CraftNativeSvgView.transform(box: box, bounds: bounds.size, preserveAspectRatio: props["preserveAspectRatio"] as? String) else { return }
1820
+ viewTransform = transform
1821
+ context.concatenate(transform)
1800
1822
  }
1801
1823
  draw(children: drawing["children"] as? [Any] ?? [], paint: Paint().merged(drawing), in: context)
1802
1824
  }
@@ -1820,12 +1842,25 @@ private final class CraftNativeSvgView: UIView {
1820
1842
  path.fill()
1821
1843
  }
1822
1844
  if let stroke = own.resolve(own.stroke), own.strokeWidth > 0 {
1845
+ // `vector-effect="non-scaling-stroke"`: the stroke is in the
1846
+ // view's points, so a stretched chart's line keeps its width
1847
+ // and a round cap stays a round dot.
1848
+ let nonScaling = (shape["props"] as? [String: Any])?["vectorEffect"] as? String == "non-scaling-stroke"
1849
+ let line: UIBezierPath
1850
+ if nonScaling, let copy = path.copy() as? UIBezierPath {
1851
+ copy.apply(viewTransform)
1852
+ context.concatenate(viewTransform.inverted())
1853
+ line = copy
1854
+ }
1855
+ else {
1856
+ line = path
1857
+ }
1823
1858
  stroke.setStroke()
1824
- path.lineWidth = own.strokeWidth
1825
- path.lineCapStyle = own.linecap == "round" ? .round : own.linecap == "square" ? .square : .butt
1826
- path.lineJoinStyle = own.linejoin == "round" ? .round : own.linejoin == "bevel" ? .bevel : .miter
1827
- if let dashes = own.dashes, !dashes.isEmpty { path.setLineDash(dashes, count: dashes.count, phase: 0) }
1828
- path.stroke()
1859
+ line.lineWidth = own.strokeWidth
1860
+ line.lineCapStyle = own.linecap == "round" ? .round : own.linecap == "square" ? .square : .butt
1861
+ line.lineJoinStyle = own.linejoin == "round" ? .round : own.linejoin == "bevel" ? .bevel : .miter
1862
+ if let dashes = own.dashes, !dashes.isEmpty { line.setLineDash(dashes, count: dashes.count, phase: 0) }
1863
+ line.stroke()
1829
1864
  }
1830
1865
  context.restoreGState()
1831
1866
  }
@@ -3493,7 +3528,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3493
3528
  private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? {
3494
3529
  // The current @stacksjs/stx native compiler emits key in props; accept the IR
3495
3530
  // field too so keyed children keep working when the compiler adopts it.
3496
- [node["id"], node["key"], props["key"], props["testID"]]
3531
+ [node["key"], props["key"], props["testID"], node["id"]]
3497
3532
  .compactMap { $0 as? String }
3498
3533
  .first { !$0.isEmpty }
3499
3534
  }
@@ -4293,7 +4328,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4293
4328
  view.accessibilityValue = nil
4294
4329
  }
4295
4330
  let role = props["accessibilityRole"] as? String
4296
- view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Image", "TextInput", "Picker", "Switch", "Slider"].contains(type))
4331
+ view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Image", "Video", "TextInput", "Picker", "Switch", "Slider"].contains(type))
4297
4332
  var traits: UIAccessibilityTraits = []
4298
4333
  switch role ?? type.lowercased() {
4299
4334
  case "button": traits.insert(.button)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.141",
3
+ "version": "0.0.143",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",