craft-native 0.0.394 → 0.0.395

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.
@@ -96,6 +96,12 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
96
96
  }
97
97
 
98
98
  private var renderItem: CraftNativeRenderItem? = null
99
+ /**
100
+ * The `fontFamily` the rows inherit from the list's ancestors. Rows are
101
+ * rebound only when their own node changes, so the screen redraws them
102
+ * when this changes (`reconcileFlatList` in MainActivity).
103
+ */
104
+ var inheritedFontFamily: String? = null
99
105
  private var recycleItem: ((String) -> Unit)? = null
100
106
  private var removeItem: ((String) -> Unit)? = null
101
107
  private var endReached: (() -> Unit)? = null
@@ -2389,6 +2389,31 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
2389
2389
  }
2390
2390
  }
2391
2391
 
2392
+ /**
2393
+ * The `fontFamily` a node passes to its children: its own, which already
2394
+ * carries any family it inherited (see [craftNativeInheritFontFamily]).
2395
+ */
2396
+ internal fun craftNativeInheritableFontFamily(style: JSONObject): String? =
2397
+ craftNativeOptionalString(style.opt("fontFamily"))?.takeUnless { it == "inherit" }
2398
+
2399
+ /**
2400
+ * CSS-style `fontFamily` inheritance: a node that sets no family (or
2401
+ * `inherit`) takes its nearest ancestor's, so a brand face set on a screen's
2402
+ * root reaches every label, button and input below it. Returns [style]
2403
+ * itself when nothing is inherited or the node sets its own family, else a
2404
+ * shallow copy carrying the inherited one; the protocol document's style is
2405
+ * never written to, so an ancestor's later change still reaches the node.
2406
+ */
2407
+ internal fun craftNativeInheritFontFamily(style: JSONObject, inherited: String?): JSONObject {
2408
+ if (inherited == null) return style
2409
+ val own = craftNativeOptionalString(style.opt("fontFamily"))
2410
+ if (own != null && own != "inherit") return style
2411
+ return JSONObject().also { copy ->
2412
+ style.keys().forEach { key -> copy.put(key, style.opt(key)) }
2413
+ copy.put("fontFamily", inherited)
2414
+ }
2415
+ }
2416
+
2392
2417
  /** CSS family fallback order, using bundled app fonts before platform families. */
2393
2418
  internal fun craftNativeFontFamily(family: String?, bundledFonts: Map<String, Typeface> = emptyMap()): Typeface =
2394
2419
  family?.split(',')?.asSequence()?.map { it.trim().trim('"', '\'') }?.mapNotNull { name ->
@@ -4210,6 +4235,12 @@ class MainActivity : AppCompatActivity() {
4210
4235
  // the original tree path without manufacturing a detached view.
4211
4236
  val nodeViews = mutableMapOf<String, View>()
4212
4237
  val nodePaths = mutableMapOf<String, String>()
4238
+ /**
4239
+ * The `fontFamily` a protocol node inherited from its ancestors when
4240
+ * drawn, so a targeted update of its subtree keeps it. Only nodes that
4241
+ * inherit one are recorded: a screen with no family keeps it empty.
4242
+ */
4243
+ val inheritedFontFamilies = mutableMapOf<String, String>()
4213
4244
  val protocolIds = mutableMapOf<View, String>()
4214
4245
  val handlers = mutableMapOf<View, String>()
4215
4246
  val linkTargets = mutableMapOf<View, String>()
@@ -5497,7 +5528,7 @@ class MainActivity : AppCompatActivity() {
5497
5528
  val document = screen.mutations.node(target) ?: return false
5498
5529
  val path = screen.nodePaths[target] ?: return false
5499
5530
  if (screen.nodeViews[target] == null) return false
5500
- makeView(screen, document, path, screen.controls)
5531
+ makeView(screen, document, path, screen.controls, inheritedFontFamily = screen.inheritedFontFamilies[target])
5501
5532
  val updated = screen.nodeViews[target] ?: return false
5502
5533
  screen.lastLayoutBounds.remove(updated)
5503
5534
  if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
@@ -5584,6 +5615,7 @@ class MainActivity : AppCompatActivity() {
5584
5615
  val next = mutableMapOf<String, View>()
5585
5616
  screen.nodeViews.clear()
5586
5617
  screen.nodePaths.clear()
5618
+ screen.inheritedFontFamilies.clear()
5587
5619
  val tree = makeView(screen, document, "root", next)
5588
5620
  if (screen.view.getChildAt(0) !== tree) {
5589
5621
  (tree.parent as? ViewGroup)?.removeView(tree)
@@ -5673,6 +5705,7 @@ class MainActivity : AppCompatActivity() {
5673
5705
  screen.controls.clear()
5674
5706
  screen.nodeViews.clear()
5675
5707
  screen.nodePaths.clear()
5708
+ screen.inheritedFontFamilies.clear()
5676
5709
  screen.protocolIds.clear()
5677
5710
  screen.handlers.clear()
5678
5711
  screen.linkTargets.clear()
@@ -5681,16 +5714,22 @@ class MainActivity : AppCompatActivity() {
5681
5714
  screen.flatListOwnerByView.clear()
5682
5715
  }
5683
5716
 
5717
+ /**
5718
+ * [inheritedFontFamily]: the nearest ancestor's `fontFamily`, which the
5719
+ * node takes when it sets none ([craftNativeInheritFontFamily]) and
5720
+ * passes on to its children and list rows.
5721
+ */
5684
5722
  private fun makeView(
5685
5723
  screen: Screen,
5686
5724
  node: JSONObject,
5687
5725
  path: String,
5688
5726
  next: MutableMap<String, View>,
5689
5727
  previousControls: Map<String, View> = screen.controls,
5728
+ inheritedFontFamily: String? = null,
5690
5729
  ): View {
5691
5730
  val type = canonicalType(node.optString("type", "View"))
5692
5731
  val props = node.optJSONObject("props") ?: JSONObject()
5693
- val style = node.optJSONObject("style") ?: JSONObject()
5732
+ val style = craftNativeInheritFontFamily(node.optJSONObject("style") ?: JSONObject(), inheritedFontFamily)
5694
5733
  val events = node.optJSONObject("events") ?: JSONObject()
5695
5734
  val children = node.optJSONArray("children") ?: JSONArray()
5696
5735
  val protocolId = node.optString("id").takeIf { it.isNotBlank() }
@@ -6477,7 +6516,7 @@ class MainActivity : AppCompatActivity() {
6477
6516
  (getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
6478
6517
  ?.hideSoftInputFromWindow(windowToken, 0)
6479
6518
  }
6480
- reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events)
6519
+ reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events, craftNativeInheritableFontFamily(style))
6481
6520
  }
6482
6521
  "View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
6483
6522
  configureContainer(this, style)
@@ -6622,6 +6661,11 @@ class MainActivity : AppCompatActivity() {
6622
6661
  }
6623
6662
  }
6624
6663
  protocolId?.let {
6664
+ if (inheritedFontFamily != null) {
6665
+ screen.inheritedFontFamilies[it] = inheritedFontFamily
6666
+ } else if (screen.inheritedFontFamilies.isNotEmpty()) {
6667
+ screen.inheritedFontFamilies.remove(it)
6668
+ }
6625
6669
  screen.nodeViews[it] = control
6626
6670
  screen.nodePaths[it] = path
6627
6671
  }
@@ -6742,10 +6786,12 @@ class MainActivity : AppCompatActivity() {
6742
6786
  }
6743
6787
  val desired = mutableListOf<View>()
6744
6788
  val desiredOrder = mutableMapOf<View, Int>()
6789
+ // `style` is the parent's effective style, its inherited family included.
6790
+ val fontFamily = craftNativeInheritableFontFamily(style)
6745
6791
  for (index in 0 until children.length()) {
6746
6792
  val child = children.opt(index)
6747
6793
  val childView = when (child) {
6748
- is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls)
6794
+ is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls, fontFamily)
6749
6795
  is String -> TextView(this).apply {
6750
6796
  includeFontPadding = false
6751
6797
  configureText(this, child, craftNativeTypography(style))
@@ -6790,6 +6836,7 @@ class MainActivity : AppCompatActivity() {
6790
6836
  owner: String,
6791
6837
  props: JSONObject,
6792
6838
  events: JSONObject,
6839
+ fontFamily: String?,
6793
6840
  ) {
6794
6841
  screen.flatListOwners.entries.removeAll { it.value == owner }
6795
6842
  screen.flatListOwnerByView[list] = owner
@@ -6819,6 +6866,10 @@ class MainActivity : AppCompatActivity() {
6819
6866
  val threshold = craftNativeNonNegativeDouble(props, "threshold", 0.1)
6820
6867
  val authoredThreshold = craftNativeNonNegativeDouble(props, "onEndReachedThreshold", threshold)
6821
6868
  val horizontal = craftNativeBoolean(props.opt("horizontal"))
6869
+ // A row redraws when its own node changes; an ancestor's new family
6870
+ // leaves every node as it was, so rows drawn before are redrawn below.
6871
+ val familyChanged = list.inheritedFontFamily != fontFamily
6872
+ val rowsDrawnBefore = familyChanged && screen.flatListRows[list]?.isNotEmpty() == true
6822
6873
  list.apply(
6823
6874
  nodes = nodes,
6824
6875
  horizontal = horizontal,
@@ -6829,7 +6880,9 @@ class MainActivity : AppCompatActivity() {
6829
6880
  val rows = screen.flatListRows.getOrPut(list) { mutableMapOf() }
6830
6881
  val previous = rows[itemIdentity].orEmpty()
6831
6882
  val next = mutableMapOf<String, View>()
6832
- val view = makeView(screen, node, "$path.$itemIdentity", next, previous)
6883
+ // Rows are drawn later, as they scroll in: each takes the
6884
+ // list's family from this render's closure.
6885
+ val view = makeView(screen, node, "$path.$itemIdentity", next, previous, fontFamily)
6833
6886
  releaseViewState(screen, previous.values.filter { it !in next.values })
6834
6887
  rows[itemIdentity] = next
6835
6888
  view
@@ -6858,6 +6911,10 @@ class MainActivity : AppCompatActivity() {
6858
6911
  craftNativeBoolean(props.opt("alwaysBounceVertical"), !horizontal),
6859
6912
  craftNativeBoolean(props.opt("alwaysBounceHorizontal"), horizontal),
6860
6913
  )
6914
+ if (familyChanged) {
6915
+ list.inheritedFontFamily = fontFamily
6916
+ if (rowsDrawnBefore) list.refreshThemeDefaults()
6917
+ }
6861
6918
  }
6862
6919
 
6863
6920
  private fun registerFlatListOwnership(screen: Screen, node: JSONObject, owner: String) {
@@ -8358,6 +8415,7 @@ class MainActivity : AppCompatActivity() {
8358
8415
  screen.controls.clear()
8359
8416
  screen.nodeViews.clear()
8360
8417
  screen.nodePaths.clear()
8418
+ screen.inheritedFontFamilies.clear()
8361
8419
  screen.protocolIds.clear()
8362
8420
  screen.handlers.clear()
8363
8421
  screen.pressInHandlers.clear()
package/dist/cli.js CHANGED
@@ -3047,7 +3047,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3047
3047
  `);
3048
3048
  }
3049
3049
  // package.json
3050
- var version = "0.0.394";
3050
+ var version = "0.0.395";
3051
3051
 
3052
3052
  // bin/cli.ts
3053
3053
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -375,7 +375,7 @@ function craftBinaryNotFoundMessage(triedPath) {
375
375
  `);
376
376
  }
377
377
  // package.json
378
- var version = "0.0.394";
378
+ var version = "0.0.395";
379
379
 
380
380
  // src/sdk-version.ts
381
381
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.394";
120
+ var version = "0.0.395";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -188,6 +188,10 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
188
188
  private var recycleItem: ((String) -> Void)?
189
189
  private var removeItem: ((String) -> Void)?
190
190
  private var endReached: (() -> Void)?
191
+ /// The `fontFamily` the list's rows inherit from its ancestors. Rows are
192
+ /// reconfigured when only their node changes, so the screen redraws them
193
+ /// itself when this changes (`CraftNativeScreenController.reconcileFlatList`).
194
+ var inheritedFontFamily: String?
191
195
  var onScrollEvent: ((UIScrollView) -> Void)?
192
196
  /// Called as the visible rows move, for a screen that lays out around them.
193
197
  var onLayoutChanged: (() -> Void)?
@@ -5895,7 +5895,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5895
5895
  let type: String
5896
5896
  let view: UIView
5897
5897
  var protocolId: String?
5898
+ /// The node's effective style: its own, plus the `fontFamily` it
5899
+ /// inherited when it sets none (see `reconcile`).
5898
5900
  var style: [String: Any] = [:]
5901
+ /// The nearest ancestor's `fontFamily` when this node was drawn, so a
5902
+ /// targeted update of this subtree inherits it without its ancestors.
5903
+ var inheritedFontFamily: String?
5899
5904
  var children: [RenderedNode] = []
5900
5905
  var widthConstraint: NSLayoutConstraint?
5901
5906
  var heightConstraint: NSLayoutConstraint?
@@ -8380,7 +8385,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
8380
8385
  // its parent's to swap in: a targeted update only redraws in
8381
8386
  // place, and the new view was made and never attached.
8382
8387
  guard canReuseView(document, previous: previous) else { return false }
8383
- let next = reconcile(document, identity: previous.identity, path: target, previous: previous)
8388
+ let next = reconcile(document, identity: previous.identity, path: target, previous: previous, inheritedFontFamily: previous.inheritedFontFamily)
8384
8389
  // A style patch is applied in place, layout included: the parent
8385
8390
  // reads the node's new layout style on its next pass.
8386
8391
  CraftNativeFlexLayout.invalidateAncestors(of: next.view)
@@ -8521,10 +8526,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
8521
8526
 
8522
8527
  /// `adopting`: `previous` is a recycled list row's root, whose views may
8523
8528
  /// show this row instead (`identity` is the new row's).
8524
- private func reconcile(_ node: [String: Any], identity: String, path: String, previous: RenderedNode?, adopting: Bool = false) -> RenderedNode {
8529
+ ///
8530
+ /// `inheritedFontFamily`: the nearest ancestor's `fontFamily`. As in CSS,
8531
+ /// a node that sets none (or `inherit`) takes it, and its children inherit
8532
+ /// in turn, so a brand face set on a screen's root reaches every label,
8533
+ /// button and input below it. It is written into the node's effective
8534
+ /// style, which every font computed from that style (Dynamic Type
8535
+ /// re-renders included) reads. With no ancestor family this costs one nil
8536
+ /// check and copies nothing.
8537
+ private func reconcile(_ node: [String: Any], identity: String, path: String, previous: RenderedNode?, adopting: Bool = false, inheritedFontFamily: String? = nil) -> RenderedNode {
8525
8538
  let type = canonicalType(node["type"] as? String ?? "View")
8526
8539
  let props = node["props"] as? [String: Any] ?? [:]
8527
- let style = node["style"] as? [String: Any] ?? [:]
8540
+ var style = node["style"] as? [String: Any] ?? [:]
8541
+ if let inheritedFontFamily {
8542
+ let own = style["fontFamily"] as? String
8543
+ if own == nil || own == "inherit" { style["fontFamily"] = inheritedFontFamily }
8544
+ }
8528
8545
  let events = node["events"] as? [String: String] ?? [:]
8529
8546
  let children = node["children"] as? [Any] ?? []
8530
8547
  let protocolId = node["id"] as? String
@@ -8545,6 +8562,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
8545
8562
  let result = current.view
8546
8563
  current.protocolId = protocolId
8547
8564
  current.style = style
8565
+ current.inheritedFontFamily = inheritedFontFamily
8548
8566
  let linkTarget = ["href", "to"].lazy.compactMap { props[$0] as? String }
8549
8567
  .first(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })?
8550
8568
  .trimmingCharacters(in: .whitespacesAndNewlines)
@@ -9091,7 +9109,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9091
9109
  parent: current,
9092
9110
  path: path,
9093
9111
  props: props,
9094
- events: events
9112
+ events: events,
9113
+ fontFamily: style["fontFamily"] as? String
9095
9114
  )
9096
9115
  default:
9097
9116
  let stack = result as! CraftNativeFlowView
@@ -9319,6 +9338,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9319
9338
  let counts = Dictionary(keys.map { ($0, 1) }, uniquingKeysWith: +)
9320
9339
  let old = Dictionary(uniqueKeysWithValues: parent.children.map { ($0.identity, $0) })
9321
9340
  var next: [RenderedNode] = []
9341
+ // `style` is the parent's effective style, its inherited family included.
9342
+ let fontFamily = style["fontFamily"] as? String
9322
9343
 
9323
9344
  for (index, node) in nodes.enumerated() {
9324
9345
  let key = explicitKey(node, props: node["props"] as? [String: Any] ?? [:])
@@ -9331,7 +9352,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9331
9352
  let identity = parent.identity.hasPrefix("list:") || parent.identity.hasPrefix("key:")
9332
9353
  ? "\(parent.identity)/\(localIdentity)"
9333
9354
  : localIdentity
9334
- let child = reconcile(node, identity: identity, path: "\(path).\(index)", previous: old[identity])
9355
+ let child = reconcile(node, identity: identity, path: "\(path).\(index)", previous: old[identity], inheritedFontFamily: fontFamily)
9335
9356
  next.append(child)
9336
9357
  }
9337
9358
  if (style["flexDirection"] as? String)?.hasSuffix("-reverse") == true { next.reverse() }
@@ -9376,7 +9397,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9376
9397
  parent: RenderedNode,
9377
9398
  path: String,
9378
9399
  props: [String: Any],
9379
- events: [String: String]
9400
+ events: [String: String],
9401
+ fontFamily: String?
9380
9402
  ) {
9381
9403
  let listKey = ObjectIdentifier(list)
9382
9404
  let owner = parent.protocolId ?? path
@@ -9420,6 +9442,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9420
9442
  0,
9421
9443
  Double(number(props["onEndReachedThreshold"]) ?? number(props["threshold"]) ?? 0.1)
9422
9444
  )
9445
+ let familyChanged = list.inheritedFontFamily != fontFamily
9446
+ let rowsDrawnBefore = familyChanged && flatListRows[listKey]?.isEmpty == false
9423
9447
  list.apply(
9424
9448
  nodes: children,
9425
9449
  horizontal: props["horizontal"] as? Bool == true,
@@ -9444,7 +9468,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9444
9468
  identity: "list:\(identity)",
9445
9469
  path: "\(path).\(identity)",
9446
9470
  previous: previous,
9447
- adopting: adopting
9471
+ adopting: adopting,
9472
+ // Rows are drawn later, as they scroll in: each takes the
9473
+ // list's family from this render's closure.
9474
+ inheritedFontFamily: fontFamily
9448
9475
  )
9449
9476
  // A recycled row's handlers went when it was recycled.
9450
9477
  if let previous, previous.view !== next.view, !adopting {
@@ -9489,6 +9516,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9489
9516
  list.setScrollTarget(props["scrollTarget"])
9490
9517
  list.alwaysBounceVertical = props["alwaysBounceVertical"] as? Bool ?? !horizontal
9491
9518
  list.alwaysBounceHorizontal = props["alwaysBounceHorizontal"] as? Bool ?? horizontal
9519
+ // A row redraws when its own node changes; an ancestor's new family
9520
+ // leaves every node as it was, so rows drawn before are redrawn here.
9521
+ if familyChanged {
9522
+ list.inheritedFontFamily = fontFamily
9523
+ if rowsDrawnBefore { list.refreshThemeDefaults() }
9524
+ }
9492
9525
  parent.children = []
9493
9526
  }
9494
9527
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.394",
3
+ "version": "0.0.395",
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>",