@rezi-ui/core 0.1.0-alpha.60 → 0.1.0-alpha.63

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 (345) hide show
  1. package/dist/app/createApp/breadcrumbs.d.ts +38 -0
  2. package/dist/app/createApp/breadcrumbs.d.ts.map +1 -0
  3. package/dist/app/createApp/breadcrumbs.js +65 -0
  4. package/dist/app/createApp/breadcrumbs.js.map +1 -0
  5. package/dist/app/createApp/config.d.ts +25 -0
  6. package/dist/app/createApp/config.d.ts.map +1 -0
  7. package/dist/app/createApp/config.js +130 -0
  8. package/dist/app/createApp/config.js.map +1 -0
  9. package/dist/app/createApp/eventLoop.d.ts +95 -0
  10. package/dist/app/createApp/eventLoop.d.ts.map +1 -0
  11. package/dist/app/createApp/eventLoop.js +384 -0
  12. package/dist/app/createApp/eventLoop.js.map +1 -0
  13. package/dist/app/createApp/guards.d.ts +21 -0
  14. package/dist/app/createApp/guards.d.ts.map +1 -0
  15. package/dist/app/createApp/guards.js +54 -0
  16. package/dist/app/createApp/guards.js.map +1 -0
  17. package/dist/app/createApp/keybindings.d.ts +28 -0
  18. package/dist/app/createApp/keybindings.d.ts.map +1 -0
  19. package/dist/app/createApp/keybindings.js +113 -0
  20. package/dist/app/createApp/keybindings.js.map +1 -0
  21. package/dist/app/createApp/renderLoop.d.ts +64 -0
  22. package/dist/app/createApp/renderLoop.d.ts.map +1 -0
  23. package/dist/app/createApp/renderLoop.js +305 -0
  24. package/dist/app/createApp/renderLoop.js.map +1 -0
  25. package/dist/app/createApp.d.ts +3 -39
  26. package/dist/app/createApp.d.ts.map +1 -1
  27. package/dist/app/createApp.js +403 -1205
  28. package/dist/app/createApp.js.map +1 -1
  29. package/dist/app/widgetRenderer/constraintState.d.ts +98 -0
  30. package/dist/app/widgetRenderer/constraintState.d.ts.map +1 -0
  31. package/dist/app/widgetRenderer/constraintState.js +563 -0
  32. package/dist/app/widgetRenderer/constraintState.js.map +1 -0
  33. package/dist/app/widgetRenderer/fileNodeCache.d.ts +2 -0
  34. package/dist/app/widgetRenderer/fileNodeCache.d.ts.map +1 -1
  35. package/dist/app/widgetRenderer/fileNodeCache.js +31 -0
  36. package/dist/app/widgetRenderer/fileNodeCache.js.map +1 -1
  37. package/dist/app/widgetRenderer/filePickerRouting.d.ts +12 -1
  38. package/dist/app/widgetRenderer/filePickerRouting.d.ts.map +1 -1
  39. package/dist/app/widgetRenderer/filePickerRouting.js +63 -14
  40. package/dist/app/widgetRenderer/filePickerRouting.js.map +1 -1
  41. package/dist/app/widgetRenderer/focusState.d.ts +46 -0
  42. package/dist/app/widgetRenderer/focusState.d.ts.map +1 -0
  43. package/dist/app/widgetRenderer/focusState.js +122 -0
  44. package/dist/app/widgetRenderer/focusState.js.map +1 -0
  45. package/dist/app/widgetRenderer/keyboardRouting.d.ts.map +1 -1
  46. package/dist/app/widgetRenderer/keyboardRouting.js.map +1 -1
  47. package/dist/app/widgetRenderer/mouseRouting.d.ts +5 -0
  48. package/dist/app/widgetRenderer/mouseRouting.d.ts.map +1 -1
  49. package/dist/app/widgetRenderer/mouseRouting.js +78 -8
  50. package/dist/app/widgetRenderer/mouseRouting.js.map +1 -1
  51. package/dist/app/widgetRenderer/overlayShortcuts.d.ts.map +1 -1
  52. package/dist/app/widgetRenderer/overlayShortcuts.js +14 -4
  53. package/dist/app/widgetRenderer/overlayShortcuts.js.map +1 -1
  54. package/dist/app/widgetRenderer/overlayState.d.ts +198 -0
  55. package/dist/app/widgetRenderer/overlayState.d.ts.map +1 -0
  56. package/dist/app/widgetRenderer/overlayState.js +590 -0
  57. package/dist/app/widgetRenderer/overlayState.js.map +1 -0
  58. package/dist/app/widgetRenderer/routeEngineEvent.d.ts +189 -0
  59. package/dist/app/widgetRenderer/routeEngineEvent.d.ts.map +1 -0
  60. package/dist/app/widgetRenderer/routeEngineEvent.js +527 -0
  61. package/dist/app/widgetRenderer/routeEngineEvent.js.map +1 -0
  62. package/dist/app/widgetRenderer.d.ts +2 -1
  63. package/dist/app/widgetRenderer.d.ts.map +1 -1
  64. package/dist/app/widgetRenderer.js +334 -1707
  65. package/dist/app/widgetRenderer.js.map +1 -1
  66. package/dist/forms/internal/arrayState.d.ts +35 -0
  67. package/dist/forms/internal/arrayState.d.ts.map +1 -0
  68. package/dist/forms/internal/arrayState.js +238 -0
  69. package/dist/forms/internal/arrayState.js.map +1 -0
  70. package/dist/forms/internal/bindings.d.ts +46 -0
  71. package/dist/forms/internal/bindings.d.ts.map +1 -0
  72. package/dist/forms/internal/bindings.js +161 -0
  73. package/dist/forms/internal/bindings.js.map +1 -0
  74. package/dist/forms/internal/dev.d.ts +4 -0
  75. package/dist/forms/internal/dev.d.ts.map +1 -0
  76. package/dist/forms/internal/dev.js +21 -0
  77. package/dist/forms/internal/dev.js.map +1 -0
  78. package/dist/forms/internal/state.d.ts +52 -0
  79. package/dist/forms/internal/state.d.ts.map +1 -0
  80. package/dist/forms/internal/state.js +240 -0
  81. package/dist/forms/internal/state.js.map +1 -0
  82. package/dist/forms/internal/submit.d.ts +43 -0
  83. package/dist/forms/internal/submit.d.ts.map +1 -0
  84. package/dist/forms/internal/submit.js +165 -0
  85. package/dist/forms/internal/submit.js.map +1 -0
  86. package/dist/forms/internal/wizard.d.ts +53 -0
  87. package/dist/forms/internal/wizard.d.ts.map +1 -0
  88. package/dist/forms/internal/wizard.js +311 -0
  89. package/dist/forms/internal/wizard.js.map +1 -0
  90. package/dist/forms/useForm.d.ts.map +1 -1
  91. package/dist/forms/useForm.js +90 -1117
  92. package/dist/forms/useForm.js.map +1 -1
  93. package/dist/index.d.ts +1 -1
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +1 -1
  96. package/dist/index.js.map +1 -1
  97. package/dist/keybindings/manager.d.ts.map +1 -1
  98. package/dist/keybindings/manager.js.map +1 -1
  99. package/dist/keybindings/parser.d.ts.map +1 -1
  100. package/dist/keybindings/parser.js +10 -5
  101. package/dist/keybindings/parser.js.map +1 -1
  102. package/dist/layout/dropdownGeometry.d.ts +8 -0
  103. package/dist/layout/dropdownGeometry.d.ts.map +1 -1
  104. package/dist/layout/dropdownGeometry.js +40 -0
  105. package/dist/layout/dropdownGeometry.js.map +1 -1
  106. package/dist/layout/engine/layoutEngine.js +1 -1
  107. package/dist/layout/engine/layoutEngine.js.map +1 -1
  108. package/dist/layout/kinds/overlays.d.ts.map +1 -1
  109. package/dist/layout/kinds/stack.d.ts +1 -3
  110. package/dist/layout/kinds/stack.d.ts.map +1 -1
  111. package/dist/layout/kinds/stack.js +11 -1523
  112. package/dist/layout/kinds/stack.js.map +1 -1
  113. package/dist/layout/kinds/stackParts/axis.d.ts +32 -0
  114. package/dist/layout/kinds/stackParts/axis.d.ts.map +1 -0
  115. package/dist/layout/kinds/stackParts/axis.js +61 -0
  116. package/dist/layout/kinds/stackParts/axis.js.map +1 -0
  117. package/dist/layout/kinds/stackParts/constraintPlan.d.ts +18 -0
  118. package/dist/layout/kinds/stackParts/constraintPlan.d.ts.map +1 -0
  119. package/dist/layout/kinds/stackParts/constraintPlan.js +434 -0
  120. package/dist/layout/kinds/stackParts/constraintPlan.js.map +1 -0
  121. package/dist/layout/kinds/stackParts/layout.d.ts +6 -0
  122. package/dist/layout/kinds/stackParts/layout.d.ts.map +1 -0
  123. package/dist/layout/kinds/stackParts/layout.js +376 -0
  124. package/dist/layout/kinds/stackParts/layout.js.map +1 -0
  125. package/dist/layout/kinds/stackParts/measure.d.ts +6 -0
  126. package/dist/layout/kinds/stackParts/measure.d.ts.map +1 -0
  127. package/dist/layout/kinds/stackParts/measure.js +212 -0
  128. package/dist/layout/kinds/stackParts/measure.js.map +1 -0
  129. package/dist/layout/kinds/stackParts/shared.d.ts +31 -0
  130. package/dist/layout/kinds/stackParts/shared.d.ts.map +1 -0
  131. package/dist/layout/kinds/stackParts/shared.js +94 -0
  132. package/dist/layout/kinds/stackParts/shared.js.map +1 -0
  133. package/dist/layout/kinds/stackParts/wrap.d.ts +26 -0
  134. package/dist/layout/kinds/stackParts/wrap.d.ts.map +1 -0
  135. package/dist/layout/kinds/stackParts/wrap.js +374 -0
  136. package/dist/layout/kinds/stackParts/wrap.js.map +1 -0
  137. package/dist/layout/validate/interactive.d.ts +106 -0
  138. package/dist/layout/validate/interactive.d.ts.map +1 -0
  139. package/dist/layout/validate/interactive.js +430 -0
  140. package/dist/layout/validate/interactive.js.map +1 -0
  141. package/dist/layout/validate/layoutConstraints.d.ts +51 -0
  142. package/dist/layout/validate/layoutConstraints.d.ts.map +1 -0
  143. package/dist/layout/validate/layoutConstraints.js +100 -0
  144. package/dist/layout/validate/layoutConstraints.js.map +1 -0
  145. package/dist/layout/validate/primitives.d.ts +31 -0
  146. package/dist/layout/validate/primitives.d.ts.map +1 -0
  147. package/dist/layout/validate/primitives.js +299 -0
  148. package/dist/layout/validate/primitives.js.map +1 -0
  149. package/dist/layout/validate/shared.d.ts +23 -0
  150. package/dist/layout/validate/shared.d.ts.map +1 -0
  151. package/dist/layout/validate/shared.js +32 -0
  152. package/dist/layout/validate/shared.js.map +1 -0
  153. package/dist/layout/validate/spacing.d.ts +21 -0
  154. package/dist/layout/validate/spacing.d.ts.map +1 -0
  155. package/dist/layout/validate/spacing.js +33 -0
  156. package/dist/layout/validate/spacing.js.map +1 -0
  157. package/dist/layout/validateProps.d.ts +5 -159
  158. package/dist/layout/validateProps.d.ts.map +1 -1
  159. package/dist/layout/validateProps.js +1 -832
  160. package/dist/layout/validateProps.js.map +1 -1
  161. package/dist/pipeline.js +1 -1
  162. package/dist/pipeline.js.map +1 -1
  163. package/dist/renderer/renderToDrawlist/renderTree.d.ts +1 -1
  164. package/dist/renderer/renderToDrawlist/renderTree.d.ts.map +1 -1
  165. package/dist/renderer/renderToDrawlist/renderTree.js +3 -3
  166. package/dist/renderer/renderToDrawlist/renderTree.js.map +1 -1
  167. package/dist/renderer/renderToDrawlist/types.d.ts +2 -0
  168. package/dist/renderer/renderToDrawlist/types.d.ts.map +1 -1
  169. package/dist/renderer/renderToDrawlist/widgets/containers.d.ts.map +1 -1
  170. package/dist/renderer/renderToDrawlist/widgets/containers.js +1 -1
  171. package/dist/renderer/renderToDrawlist/widgets/containers.js.map +1 -1
  172. package/dist/renderer/renderToDrawlist/widgets/files.d.ts +2 -1
  173. package/dist/renderer/renderToDrawlist/widgets/files.d.ts.map +1 -1
  174. package/dist/renderer/renderToDrawlist/widgets/files.js +30 -3
  175. package/dist/renderer/renderToDrawlist/widgets/files.js.map +1 -1
  176. package/dist/renderer/renderToDrawlist/widgets/overlays.d.ts +1 -1
  177. package/dist/renderer/renderToDrawlist/widgets/overlays.d.ts.map +1 -1
  178. package/dist/renderer/renderToDrawlist/widgets/overlays.js +26 -6
  179. package/dist/renderer/renderToDrawlist/widgets/overlays.js.map +1 -1
  180. package/dist/renderer/renderToDrawlist/widgets/renderFormWidgets.d.ts.map +1 -1
  181. package/dist/renderer/renderToDrawlist/widgets/renderFormWidgets.js +52 -18
  182. package/dist/renderer/renderToDrawlist/widgets/renderFormWidgets.js.map +1 -1
  183. package/dist/renderer/renderToDrawlist.d.ts +0 -8
  184. package/dist/renderer/renderToDrawlist.d.ts.map +1 -1
  185. package/dist/renderer/renderToDrawlist.js +1 -9
  186. package/dist/renderer/renderToDrawlist.js.map +1 -1
  187. package/dist/runtime/commit/composite.d.ts +11 -0
  188. package/dist/runtime/commit/composite.d.ts.map +1 -0
  189. package/dist/runtime/commit/composite.js +238 -0
  190. package/dist/runtime/commit/composite.js.map +1 -0
  191. package/dist/runtime/commit/container.d.ts +7 -0
  192. package/dist/runtime/commit/container.d.ts.map +1 -0
  193. package/dist/runtime/commit/container.js +350 -0
  194. package/dist/runtime/commit/container.js.map +1 -0
  195. package/dist/runtime/commit/equality.d.ts +20 -0
  196. package/dist/runtime/commit/equality.d.ts.map +1 -0
  197. package/dist/runtime/commit/equality.js +436 -0
  198. package/dist/runtime/commit/equality.js.map +1 -0
  199. package/dist/runtime/commit/errorBoundary.d.ts +7 -0
  200. package/dist/runtime/commit/errorBoundary.d.ts.map +1 -0
  201. package/dist/runtime/commit/errorBoundary.js +53 -0
  202. package/dist/runtime/commit/errorBoundary.js.map +1 -0
  203. package/dist/runtime/commit/shared.d.ts +138 -0
  204. package/dist/runtime/commit/shared.d.ts.map +1 -0
  205. package/dist/runtime/commit/shared.js +11 -0
  206. package/dist/runtime/commit/shared.js.map +1 -0
  207. package/dist/runtime/commit/transitions.d.ts +9 -0
  208. package/dist/runtime/commit/transitions.d.ts.map +1 -0
  209. package/dist/runtime/commit/transitions.js +93 -0
  210. package/dist/runtime/commit/transitions.js.map +1 -0
  211. package/dist/runtime/commit/validation.d.ts +16 -0
  212. package/dist/runtime/commit/validation.d.ts.map +1 -0
  213. package/dist/runtime/commit/validation.js +157 -0
  214. package/dist/runtime/commit/validation.js.map +1 -0
  215. package/dist/runtime/commit.d.ts +7 -117
  216. package/dist/runtime/commit.d.ts.map +1 -1
  217. package/dist/runtime/commit.js +13 -1394
  218. package/dist/runtime/commit.js.map +1 -1
  219. package/dist/runtime/localState.d.ts +4 -0
  220. package/dist/runtime/localState.d.ts.map +1 -1
  221. package/dist/runtime/localState.js.map +1 -1
  222. package/dist/runtime/widgetMeta/collector.d.ts +77 -0
  223. package/dist/runtime/widgetMeta/collector.d.ts.map +1 -0
  224. package/dist/runtime/widgetMeta/collector.js +293 -0
  225. package/dist/runtime/widgetMeta/collector.js.map +1 -0
  226. package/dist/runtime/widgetMeta/focusContainers.d.ts +44 -0
  227. package/dist/runtime/widgetMeta/focusContainers.d.ts.map +1 -0
  228. package/dist/runtime/widgetMeta/focusContainers.js +190 -0
  229. package/dist/runtime/widgetMeta/focusContainers.js.map +1 -0
  230. package/dist/runtime/widgetMeta/focusInfo.d.ts +19 -0
  231. package/dist/runtime/widgetMeta/focusInfo.d.ts.map +1 -0
  232. package/dist/runtime/widgetMeta/focusInfo.js +172 -0
  233. package/dist/runtime/widgetMeta/focusInfo.js.map +1 -0
  234. package/dist/runtime/widgetMeta/helpers.d.ts +47 -0
  235. package/dist/runtime/widgetMeta/helpers.d.ts.map +1 -0
  236. package/dist/runtime/widgetMeta/helpers.js +182 -0
  237. package/dist/runtime/widgetMeta/helpers.js.map +1 -0
  238. package/dist/runtime/widgetMeta.d.ts +12 -175
  239. package/dist/runtime/widgetMeta.d.ts.map +1 -1
  240. package/dist/runtime/widgetMeta.js +6 -847
  241. package/dist/runtime/widgetMeta.js.map +1 -1
  242. package/dist/ui/capabilities.d.ts.map +1 -1
  243. package/dist/ui/designTokens.d.ts.map +1 -1
  244. package/dist/widgets/accordion.d.ts.map +1 -1
  245. package/dist/widgets/accordion.js +8 -13
  246. package/dist/widgets/accordion.js.map +1 -1
  247. package/dist/widgets/factories/advanced.d.ts +20 -0
  248. package/dist/widgets/factories/advanced.d.ts.map +1 -0
  249. package/dist/widgets/factories/advanced.js +75 -0
  250. package/dist/widgets/factories/advanced.js.map +1 -0
  251. package/dist/widgets/factories/basic.d.ts +14 -0
  252. package/dist/widgets/factories/basic.d.ts.map +1 -0
  253. package/dist/widgets/factories/basic.js +44 -0
  254. package/dist/widgets/factories/basic.js.map +1 -0
  255. package/dist/widgets/factories/feedback.d.ts +20 -0
  256. package/dist/widgets/factories/feedback.d.ts.map +1 -0
  257. package/dist/widgets/factories/feedback.js +102 -0
  258. package/dist/widgets/factories/feedback.js.map +1 -0
  259. package/dist/widgets/factories/helpers.d.ts +41 -0
  260. package/dist/widgets/factories/helpers.d.ts.map +1 -0
  261. package/dist/widgets/factories/helpers.js +72 -0
  262. package/dist/widgets/factories/helpers.js.map +1 -0
  263. package/dist/widgets/factories/interactive.d.ts +15 -0
  264. package/dist/widgets/factories/interactive.d.ts.map +1 -0
  265. package/dist/widgets/factories/interactive.js +46 -0
  266. package/dist/widgets/factories/interactive.js.map +1 -0
  267. package/dist/widgets/factories/layoutShell.d.ts +22 -0
  268. package/dist/widgets/factories/layoutShell.d.ts.map +1 -0
  269. package/dist/widgets/factories/layoutShell.js +190 -0
  270. package/dist/widgets/factories/layoutShell.js.map +1 -0
  271. package/dist/widgets/factories/media.d.ts +14 -0
  272. package/dist/widgets/factories/media.d.ts.map +1 -0
  273. package/dist/widgets/factories/media.js +25 -0
  274. package/dist/widgets/factories/media.js.map +1 -0
  275. package/dist/widgets/factories/navigation.d.ts +10 -0
  276. package/dist/widgets/factories/navigation.d.ts.map +1 -0
  277. package/dist/widgets/factories/navigation.js +24 -0
  278. package/dist/widgets/factories/navigation.js.map +1 -0
  279. package/dist/widgets/field.d.ts +6 -1
  280. package/dist/widgets/field.d.ts.map +1 -1
  281. package/dist/widgets/field.js +8 -2
  282. package/dist/widgets/field.js.map +1 -1
  283. package/dist/widgets/filePicker.d.ts +5 -0
  284. package/dist/widgets/filePicker.d.ts.map +1 -0
  285. package/dist/widgets/filePicker.js +136 -0
  286. package/dist/widgets/filePicker.js.map +1 -0
  287. package/dist/widgets/protocol.d.ts +0 -6
  288. package/dist/widgets/protocol.d.ts.map +1 -1
  289. package/dist/widgets/protocol.js +0 -6
  290. package/dist/widgets/protocol.js.map +1 -1
  291. package/dist/widgets/select.js +1 -1
  292. package/dist/widgets/select.js.map +1 -1
  293. package/dist/widgets/splitPane.d.ts.map +1 -1
  294. package/dist/widgets/splitPane.js.map +1 -1
  295. package/dist/widgets/table.d.ts.map +1 -1
  296. package/dist/widgets/table.js +43 -1
  297. package/dist/widgets/table.js.map +1 -1
  298. package/dist/widgets/tree.d.ts.map +1 -1
  299. package/dist/widgets/types/advanced.d.ts +611 -0
  300. package/dist/widgets/types/advanced.d.ts.map +1 -0
  301. package/dist/widgets/types/advanced.js +2 -0
  302. package/dist/widgets/types/advanced.js.map +1 -0
  303. package/dist/widgets/types/base.d.ts +933 -0
  304. package/dist/widgets/types/base.d.ts.map +1 -0
  305. package/dist/widgets/types/base.js +2 -0
  306. package/dist/widgets/types/base.js.map +1 -0
  307. package/dist/widgets/types/forms.d.ts +136 -0
  308. package/dist/widgets/types/forms.d.ts.map +1 -0
  309. package/dist/widgets/types/forms.js +2 -0
  310. package/dist/widgets/types/forms.js.map +1 -0
  311. package/dist/widgets/types/navigation.d.ts +83 -0
  312. package/dist/widgets/types/navigation.d.ts.map +1 -0
  313. package/dist/widgets/types/navigation.js +2 -0
  314. package/dist/widgets/types/navigation.js.map +1 -0
  315. package/dist/widgets/types/overlaysShell.d.ts +223 -0
  316. package/dist/widgets/types/overlaysShell.d.ts.map +1 -0
  317. package/dist/widgets/types/overlaysShell.js +2 -0
  318. package/dist/widgets/types/overlaysShell.js.map +1 -0
  319. package/dist/widgets/types/table.d.ts +104 -0
  320. package/dist/widgets/types/table.d.ts.map +1 -0
  321. package/dist/widgets/types/table.js +2 -0
  322. package/dist/widgets/types/table.js.map +1 -0
  323. package/dist/widgets/types/tree.d.ts +64 -0
  324. package/dist/widgets/types/tree.d.ts.map +1 -0
  325. package/dist/widgets/types/tree.js +2 -0
  326. package/dist/widgets/types/tree.js.map +1 -0
  327. package/dist/widgets/types.d.ts +14 -2123
  328. package/dist/widgets/types.d.ts.map +1 -1
  329. package/dist/widgets/ui.d.ts +37 -843
  330. package/dist/widgets/ui.d.ts.map +1 -1
  331. package/dist/widgets/ui.js +37 -1262
  332. package/dist/widgets/ui.js.map +1 -1
  333. package/package.json +2 -2
  334. package/dist/constraints/aggregation.d.ts +0 -17
  335. package/dist/constraints/aggregation.d.ts.map +0 -1
  336. package/dist/constraints/aggregation.js +0 -59
  337. package/dist/constraints/aggregation.js.map +0 -1
  338. package/dist/renderer/renderToDrawlist/overflowCulling.d.ts +0 -3
  339. package/dist/renderer/renderToDrawlist/overflowCulling.d.ts.map +0 -1
  340. package/dist/renderer/renderToDrawlist/overflowCulling.js +0 -81
  341. package/dist/renderer/renderToDrawlist/overflowCulling.js.map +0 -1
  342. package/dist/widgets/tests/protocol.test.d.ts +0 -2
  343. package/dist/widgets/tests/protocol.test.d.ts.map +0 -1
  344. package/dist/widgets/tests/protocol.test.js +0 -120
  345. package/dist/widgets/tests/protocol.test.js.map +0 -1
@@ -22,237 +22,35 @@
22
22
  * @see docs/guide/lifecycle-and-updates.md
23
23
  */
24
24
  import { ZrUiError } from "../abi.js";
25
- import { BACKEND_DRAWLIST_VERSION_MARKER, BACKEND_FPS_CAP_MARKER, BACKEND_MAX_EVENT_BYTES_MARKER, FRAME_ACCEPTED_ACK_MARKER, } from "../backend.js";
25
+ import { BACKEND_FPS_CAP_MARKER, BACKEND_MAX_EVENT_BYTES_MARKER, } from "../backend.js";
26
26
  import { describeThrown } from "../debug/describeThrown.js";
27
- import { createManagerState, getBindings, getMode, getPendingChord, registerBindings, registerModes, removeBindingsBySource, resetChordState, routeKeyEvent, setMode, } from "../keybindings/index.js";
28
- import { ZR_MOD_CTRL } from "../keybindings/keyCodes.js";
29
- import { normalizeBreakpointThresholds, } from "../layout/responsive.js";
30
- import { PERF_ENABLED, perfMarkEnd, perfMarkStart, perfNow, perfRecord } from "../perf/perf.js";
31
- import { parseEventBatchV1 } from "../protocol/zrev_v1.js";
27
+ import { createManagerState, getBindings, getMode, getPendingChord, setMode, } from "../keybindings/index.js";
28
+ import { PERF_ENABLED, perfMarkStart } from "../perf/perf.js";
32
29
  import { createRouterIntegration } from "../router/integration.js";
33
- import { DEFAULT_TERMINAL_PROFILE, terminalProfileFromCaps, } from "../terminalProfile.js";
30
+ import { DEFAULT_TERMINAL_PROFILE } from "../terminalProfile.js";
34
31
  import { defaultTheme } from "../theme/defaultTheme.js";
35
- import { blendTheme, compileTheme } from "../theme/theme.js";
36
- import { ui } from "../widgets/ui.js";
37
- import { DIRTY_LAYOUT, DIRTY_RENDER, DIRTY_VIEW, buildWidgetRenderPlan, createDirtyTracker, } from "./createApp/dirtyPlan.js";
32
+ import { compileTheme } from "../theme/theme.js";
33
+ import { createRuntimeBreadcrumbHelpers, } from "./createApp/breadcrumbs.js";
34
+ import { loadTerminalProfile, readBackendDrawlistVersionMarker, readBackendPositiveIntMarker, requirePositiveInt, resolveAppConfig as resolveAppConfigImpl, } from "./createApp/config.js";
35
+ import { DIRTY_LAYOUT, DIRTY_RENDER, DIRTY_VIEW, createDirtyTracker, } from "./createApp/dirtyPlan.js";
36
+ import { createEventLoop } from "./createApp/eventLoop.js";
38
37
  import { createFocusDispatcher } from "./createApp/focusDispatcher.js";
38
+ import { createAppGuards } from "./createApp/guards.js";
39
+ import { computeKeybindingsEnabled, createAppKeybindingHelpers } from "./createApp/keybindings.js";
40
+ import { createRenderLoop } from "./createApp/renderLoop.js";
39
41
  import { createRunSignalController, readProcessLike } from "./createApp/runSignals.js";
40
- import { buildTopLevelViewErrorScreen, captureTopLevelViewError, isTopLevelQuitEvent, isTopLevelRetryEvent, isUnhandledCtrlCKeyEvent, isUnmodifiedTextQuitEvent, } from "./createApp/topLevelViewError.js";
41
42
  import { RawRenderer } from "./rawRenderer.js";
42
- import { isRuntimeBreadcrumbEventKind, mergeRuntimeBreadcrumbSnapshot, toRuntimeBreadcrumbAction, } from "./runtimeBreadcrumbs.js";
43
43
  import { AppStateMachine } from "./stateMachine.js";
44
44
  import { TurnScheduler } from "./turnScheduler.js";
45
45
  import { UpdateQueue } from "./updateQueue.js";
46
- import { WidgetRenderer, } from "./widgetRenderer.js";
47
- const ROUTE_KEYBINDING_SOURCE = "__rezi:router";
48
- /** Default configuration values. */
49
- const DEFAULT_CONFIG = Object.freeze({
50
- fpsCap: 60,
51
- maxEventBytes: 1 << 20 /* 1 MiB */,
52
- maxDrawlistBytes: 2 << 20 /* 2 MiB */,
53
- rootPadding: 0,
54
- breakpointThresholds: normalizeBreakpointThresholds(undefined),
55
- drawlistValidateParams: true,
56
- drawlistReuseOutputBuffer: true,
57
- drawlistEncodedStringCacheCap: 131072,
58
- maxFramesInFlight: 1,
59
- themeTransitionFrames: 0,
60
- internal_onRender: undefined,
61
- internal_onLayout: undefined,
62
- });
63
- const MAX_SAFE_FPS_CAP = 1000;
64
- const MAX_SAFE_EVENT_BYTES = 4 << 20; /* 4 MiB */
65
- const SYNC_FRAME_ACK_MARKER = "__reziSyncFrameAck";
46
+ import { WidgetRenderer } from "./widgetRenderer.js";
66
47
  export const APP_INTERNAL_REQUEST_VIEW_LAYOUT_MARKER = "__reziRequestViewLayout";
67
48
  export const APP_INTERNAL_SET_RUNTIME_BREADCRUMB_HOOKS_MARKER = "__reziSetRuntimeBreadcrumbHooks";
68
49
  function invalidProps(detail) {
69
50
  throw new ZrUiError("ZRUI_INVALID_PROPS", detail);
70
51
  }
71
- function requirePositiveInt(name, v) {
72
- if (!Number.isInteger(v) || v <= 0)
73
- invalidProps(`${name} must be a positive integer`);
74
- return v;
75
- }
76
- function requirePositiveIntAtMost(name, v, max) {
77
- const parsed = requirePositiveInt(name, v);
78
- if (parsed > max)
79
- invalidProps(`${name} must be <= ${String(max)}`);
80
- return parsed;
81
- }
82
- function requireNonNegativeInt(name, v) {
83
- if (!Number.isInteger(v) || v < 0)
84
- invalidProps(`${name} must be a non-negative integer`);
85
- return v;
86
- }
87
- function isSyncFrameAck(p) {
88
- return (typeof p === "object" &&
89
- p !== null &&
90
- p[SYNC_FRAME_ACK_MARKER] === true);
91
- }
92
- function getAcceptedFrameAck(p) {
93
- if (typeof p !== "object" || p === null)
94
- return null;
95
- const marker = p[FRAME_ACCEPTED_ACK_MARKER];
96
- if (typeof marker !== "object" || marker === null)
97
- return null;
98
- if (typeof marker.then !== "function")
99
- return null;
100
- return marker;
101
- }
102
- function readBackendPositiveIntMarker(backend, marker) {
103
- const value = backend[marker];
104
- if (value === undefined)
105
- return null;
106
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
107
- invalidProps(`backend marker ${marker} must be a positive integer when present`);
108
- }
109
- return value;
110
- }
111
- function readBackendDrawlistVersionMarker(backend) {
112
- const value = backend[BACKEND_DRAWLIST_VERSION_MARKER];
113
- if (value === undefined)
114
- return null;
115
- if (value !== 1) {
116
- invalidProps(`backend marker ${BACKEND_DRAWLIST_VERSION_MARKER} must be 1 (received ${String(value)})`);
117
- }
118
- return 1;
119
- }
120
- function monotonicNowMs() {
121
- const perf = globalThis.performance;
122
- const perfNow = perf?.now;
123
- if (typeof perfNow === "function")
124
- return perfNow.call(perf);
125
- return Date.now();
126
- }
127
- async function loadTerminalProfile(backend) {
128
- try {
129
- if (typeof backend.getTerminalProfile === "function") {
130
- return await backend.getTerminalProfile();
131
- }
132
- }
133
- catch {
134
- // fall through to caps-derived profile
135
- }
136
- try {
137
- const caps = await backend.getCaps();
138
- return terminalProfileFromCaps(caps);
139
- }
140
- catch {
141
- return DEFAULT_TERMINAL_PROFILE;
142
- }
143
- }
144
- function buildLayoutDebugOverlay(rectById) {
145
- if (rectById.size === 0)
146
- return null;
147
- const rows = [...rectById.entries()]
148
- .sort((a, b) => a[0].localeCompare(b[0]))
149
- .slice(0, 18)
150
- .map(([id, rect]) => ui.text(`${id} ${String(rect.x)},${String(rect.y)} ${String(rect.w)}x${String(rect.h)}`));
151
- const panel = ui.box({ border: "single", title: `Layout (${String(rectById.size)})`, p: 1 }, [
152
- ui.column({ gap: 0 }, rows),
153
- ]);
154
- return ui.layer({
155
- id: "rezi.layout.debug.overlay",
156
- zIndex: 2_000_000_000,
157
- modal: false,
158
- backdrop: "none",
159
- closeOnEscape: false,
160
- content: ui.column({ width: "full", height: "full", justify: "end", p: 1 }, [
161
- ui.row({ width: "full", justify: "start" }, [panel]),
162
- ]),
163
- });
164
- }
165
- /** Apply defaults to user-provided config, validating all values. */
166
52
  export function resolveAppConfig(config) {
167
- if (!config)
168
- return DEFAULT_CONFIG;
169
- const fpsCap = config.fpsCap === undefined
170
- ? DEFAULT_CONFIG.fpsCap
171
- : requirePositiveIntAtMost("fpsCap", config.fpsCap, MAX_SAFE_FPS_CAP);
172
- const maxEventBytes = config.maxEventBytes === undefined
173
- ? DEFAULT_CONFIG.maxEventBytes
174
- : requirePositiveIntAtMost("maxEventBytes", config.maxEventBytes, MAX_SAFE_EVENT_BYTES);
175
- const maxDrawlistBytes = config.maxDrawlistBytes === undefined
176
- ? DEFAULT_CONFIG.maxDrawlistBytes
177
- : requirePositiveInt("maxDrawlistBytes", config.maxDrawlistBytes);
178
- const rootPadding = config.rootPadding === undefined
179
- ? DEFAULT_CONFIG.rootPadding
180
- : requireNonNegativeInt("rootPadding", config.rootPadding);
181
- const breakpointThresholds = normalizeBreakpointThresholds(config.breakpoints);
182
- const drawlistValidateParams = config.drawlistValidateParams === undefined
183
- ? DEFAULT_CONFIG.drawlistValidateParams
184
- : config.drawlistValidateParams !== false;
185
- const drawlistReuseOutputBuffer = config.drawlistReuseOutputBuffer === undefined
186
- ? DEFAULT_CONFIG.drawlistReuseOutputBuffer
187
- : config.drawlistReuseOutputBuffer === true;
188
- const drawlistEncodedStringCacheCap = config.drawlistEncodedStringCacheCap === undefined
189
- ? DEFAULT_CONFIG.drawlistEncodedStringCacheCap
190
- : requireNonNegativeInt("drawlistEncodedStringCacheCap", config.drawlistEncodedStringCacheCap);
191
- const maxFramesInFlight = config.maxFramesInFlight === undefined
192
- ? DEFAULT_CONFIG.maxFramesInFlight
193
- : Math.min(4, Math.max(1, requirePositiveInt("maxFramesInFlight", config.maxFramesInFlight)));
194
- const themeTransitionFrames = config.themeTransitionFrames === undefined
195
- ? DEFAULT_CONFIG.themeTransitionFrames
196
- : requireNonNegativeInt("themeTransitionFrames", config.themeTransitionFrames);
197
- const internal_onRender = typeof config.internal_onRender === "function" ? config.internal_onRender : undefined;
198
- const internal_onLayout = typeof config.internal_onLayout === "function" ? config.internal_onLayout : undefined;
199
- return Object.freeze({
200
- fpsCap,
201
- maxEventBytes,
202
- maxDrawlistBytes,
203
- rootPadding,
204
- breakpointThresholds,
205
- drawlistValidateParams,
206
- drawlistReuseOutputBuffer,
207
- drawlistEncodedStringCacheCap,
208
- maxFramesInFlight,
209
- themeTransitionFrames,
210
- internal_onRender,
211
- internal_onLayout,
212
- });
213
- }
214
- /**
215
- * Convert a text codepoint to a key code for keybinding matching.
216
- * Letters are normalized to uppercase (A-Z = 65-90).
217
- * Returns null if codepoint is not matchable.
218
- */
219
- function codepointToKeyCode(codepoint) {
220
- // Lowercase letters -> uppercase
221
- if (codepoint >= 97 && codepoint <= 122) {
222
- return codepoint - 32; // 'a' (97) -> 'A' (65)
223
- }
224
- // Uppercase letters
225
- if (codepoint >= 65 && codepoint <= 90) {
226
- return codepoint;
227
- }
228
- // Digits and printable ASCII
229
- if (codepoint >= 32 && codepoint <= 126) {
230
- return codepoint;
231
- }
232
- return null;
233
- }
234
- /**
235
- * Convert text control characters into Ctrl+key key codes.
236
- *
237
- * Terminals without kitty/CSI-u often emit Ctrl+letter as text bytes:
238
- * 0x01-0x1A for Ctrl+A..Ctrl+Z, and 0x1C-0x1F for Ctrl+\..Ctrl+_.
239
- * We intentionally exclude 0x09 (Tab), 0x0D (Enter), and 0x1B (Escape)
240
- * because they have dedicated key semantics in the engine.
241
- */
242
- function codepointToCtrlKeyCode(codepoint) {
243
- if (codepoint === 9 || codepoint === 13) {
244
- return null;
245
- }
246
- if (codepoint >= 1 && codepoint <= 26) {
247
- return codepoint + 64;
248
- }
249
- if (codepoint >= 28 && codepoint <= 31) {
250
- return codepoint + 64;
251
- }
252
- return null;
253
- }
254
- function blendThemeColors(from, to, t) {
255
- return blendTheme(from, to, t);
53
+ return resolveAppConfigImpl(config);
256
54
  }
257
55
  export function createApp(opts) {
258
56
  const backend = opts.backend;
@@ -314,14 +112,26 @@ export function createApp(opts) {
314
112
  let settleActiveRun = null;
315
113
  let renderRequestQueuedForCurrentTurn = false;
316
114
  let userCommitScheduled = false;
317
- // Perf tracking: submit time for backend_ack calculation
318
- let submitFrameStartMs = null;
319
- // Perf tracking: schedule_wait measures time from render request to render start
320
115
  let scheduleWaitStartMs = null;
321
- const scheduler = new TurnScheduler((items) => processTurn(items));
116
+ const baseInternalOnRender = config.internal_onRender;
117
+ const baseInternalOnLayout = config.internal_onLayout;
118
+ let inspectorInternalOnRender;
119
+ let inspectorInternalOnLayout;
120
+ let runtimeBreadcrumbsEnabled = baseInternalOnRender !== undefined || baseInternalOnLayout !== undefined;
121
+ let keybindingState = createManagerState();
122
+ let keybindingsEnabled = false;
123
+ let breadcrumbLastEventKind = null;
124
+ let breadcrumbLastConsumptionPath = null;
125
+ let breadcrumbLastAction = null;
126
+ let breadcrumbEventTracked = false;
127
+ let deferredInlineFatal = null;
128
+ let processTurnImpl = () => undefined;
129
+ let tryRenderOnceImpl = () => undefined;
130
+ const scheduler = new TurnScheduler((items) => processTurnImpl(items));
131
+ const enqueueWorkItem = (item) => {
132
+ scheduler.enqueue(item);
133
+ };
322
134
  function markDirty(flags, schedule = true) {
323
- // Track when dirty flags are first set for schedule_wait measurement.
324
- // This captures time from "render needed" to "render started".
325
135
  const { wasDirty, flags: nextFlags } = dirtyTracker.markDirty(flags);
326
136
  if (PERF_ENABLED && !wasDirty && nextFlags !== 0 && scheduleWaitStartMs === null) {
327
137
  scheduleWaitStartMs = perfMarkStart("schedule_wait");
@@ -333,63 +143,128 @@ export function createApp(opts) {
333
143
  if (scheduler.isExecuting) {
334
144
  if (!renderRequestQueuedForCurrentTurn) {
335
145
  renderRequestQueuedForCurrentTurn = true;
336
- scheduler.enqueue({ kind: "renderRequest" });
146
+ enqueueWorkItem({ kind: "renderRequest" });
337
147
  }
338
148
  return;
339
149
  }
340
150
  if (scheduler.isScheduled)
341
151
  return;
342
- scheduler.enqueue({ kind: "renderRequest" });
152
+ enqueueWorkItem({ kind: "renderRequest" });
343
153
  }
344
- function beginThemeTransition(nextTheme) {
345
- if (config.themeTransitionFrames <= 0 || sm.state !== "Running" || mode !== "widget") {
346
- theme = nextTheme;
347
- themeTransition = null;
154
+ function requestRenderFromRenderer() {
155
+ markDirty(DIRTY_RENDER);
156
+ }
157
+ function requestViewFromRenderer() {
158
+ markDirty(DIRTY_VIEW);
159
+ }
160
+ const guards = createAppGuards({
161
+ getEventHandlerDepth: () => inEventHandlerDepth,
162
+ getLifecycleBusy: () => lifecycleBusy,
163
+ getRuntimeState: () => sm.state,
164
+ isInCommit: () => inCommit,
165
+ isInRender: () => inRender,
166
+ });
167
+ function enqueueFatal(code, detail) {
168
+ enqueueWorkItem({ kind: "fatal", code, detail });
169
+ }
170
+ function doFatal(code, detail) {
171
+ if (sm.state !== "Running")
348
172
  return;
173
+ lifecycleBusy = null;
174
+ lifecycleGeneration++;
175
+ backendStarted = false;
176
+ const fatalEv = { kind: "fatal", code, detail };
177
+ const snapshot = [];
178
+ for (const slot of handlers) {
179
+ if (slot.active.value)
180
+ snapshot.push(slot.fn);
181
+ }
182
+ for (const fn of snapshot) {
183
+ try {
184
+ fn(fatalEv);
185
+ }
186
+ catch {
187
+ // ignore
188
+ }
189
+ }
190
+ try {
191
+ sm.toFaulted();
192
+ }
193
+ catch {
194
+ // ignore
195
+ }
196
+ pollToken++;
197
+ try {
198
+ void backend
199
+ .stop()
200
+ .catch(() => undefined)
201
+ .finally(() => {
202
+ try {
203
+ backend.dispose();
204
+ }
205
+ catch {
206
+ // ignore
207
+ }
208
+ settleActiveRun?.();
209
+ });
210
+ }
211
+ catch {
212
+ try {
213
+ backend.dispose();
214
+ }
215
+ catch {
216
+ // ignore
217
+ }
218
+ settleActiveRun?.();
349
219
  }
350
- themeTransition = Object.freeze({
351
- from: theme,
352
- to: nextTheme,
353
- frame: 0,
354
- totalFrames: config.themeTransitionFrames,
355
- });
356
220
  }
357
- function advanceThemeTransitionFrame() {
358
- const active = themeTransition;
359
- if (!active)
221
+ function flushDeferredInlineFatal() {
222
+ if (deferredInlineFatal === null || inEventHandlerDepth !== 0)
360
223
  return;
361
- const nextFrame = active.frame + 1;
362
- if (nextFrame >= active.totalFrames) {
363
- theme = active.to;
364
- themeTransition = null;
224
+ const fatal = deferredInlineFatal;
225
+ deferredInlineFatal = null;
226
+ doFatal(fatal.code, fatal.detail);
227
+ }
228
+ function fatalNowOrEnqueue(code, detail) {
229
+ const canFailFastInline = scheduler.isExecuting && !inRender && !inCommit;
230
+ if (canFailFastInline && inEventHandlerDepth > 0) {
231
+ if (deferredInlineFatal === null) {
232
+ deferredInlineFatal = Object.freeze({ code, detail });
233
+ }
365
234
  return;
366
235
  }
367
- theme = blendThemeColors(active.from, active.to, nextFrame / active.totalFrames);
368
- themeTransition = Object.freeze({
369
- ...active,
370
- frame: nextFrame,
371
- });
372
- }
373
- function scheduleThemeTransitionContinuation() {
374
- if (!themeTransition || sm.state !== "Running")
236
+ if (canFailFastInline) {
237
+ doFatal(code, detail);
375
238
  return;
376
- // Theme-aware composite widgets resolve recipe styles during commit, so
377
- // transition frames must invalidate view/commit, not only render.
378
- markDirty(DIRTY_VIEW, false);
379
- renderRequestQueuedForCurrentTurn = true;
380
- scheduler.enqueue({ kind: "renderRequest" });
381
- }
382
- function requestRenderFromRenderer() {
383
- markDirty(DIRTY_RENDER);
239
+ }
240
+ enqueueFatal(code, detail);
384
241
  }
385
- function requestViewFromRenderer() {
386
- markDirty(DIRTY_VIEW);
242
+ function cleanupStartedBackendAfterAbort() {
243
+ if (!backendStarted)
244
+ return;
245
+ backendStarted = false;
246
+ try {
247
+ void backend
248
+ .stop()
249
+ .catch(() => undefined)
250
+ .finally(() => {
251
+ try {
252
+ backend.dispose();
253
+ }
254
+ catch {
255
+ // ignore
256
+ }
257
+ });
258
+ }
259
+ catch {
260
+ try {
261
+ backend.dispose();
262
+ }
263
+ catch {
264
+ // ignore
265
+ }
266
+ }
387
267
  }
388
- const baseInternalOnRender = config.internal_onRender;
389
- const baseInternalOnLayout = config.internal_onLayout;
390
- let inspectorInternalOnRender;
391
- let inspectorInternalOnLayout;
392
- let runtimeBreadcrumbsEnabled = baseInternalOnRender !== undefined || baseInternalOnLayout !== undefined;
393
268
  const rawRenderer = new RawRenderer({
394
269
  backend,
395
270
  maxDrawlistBytes: config.maxDrawlistBytes,
@@ -434,129 +309,52 @@ export function createApp(opts) {
434
309
  return routerIntegration.renderCurrentScreen(state, routeStateUpdater);
435
310
  };
436
311
  }
437
- /* --- Keybinding State --- */
438
- let keybindingState = createManagerState();
439
- let keybindingsEnabled = false;
440
- let breadcrumbLastEventKind = null;
441
- let breadcrumbLastConsumptionPath = null;
442
- let breadcrumbLastAction = null;
443
- let breadcrumbEventTracked = false;
444
- let deferredInlineFatal = null;
445
- function recomputeRuntimeBreadcrumbCollection() {
446
- const next = baseInternalOnRender !== undefined ||
447
- baseInternalOnLayout !== undefined ||
448
- inspectorInternalOnRender !== undefined ||
449
- inspectorInternalOnLayout !== undefined;
450
- if (next === runtimeBreadcrumbsEnabled)
451
- return;
452
- runtimeBreadcrumbsEnabled = next;
453
- widgetRenderer.setRuntimeBreadcrumbCaptureEnabled(next);
454
- if (!next) {
455
- breadcrumbLastEventKind = null;
456
- breadcrumbLastConsumptionPath = null;
457
- breadcrumbLastAction = null;
458
- breadcrumbEventTracked = false;
459
- }
460
- }
461
- function computeKeybindingsEnabled(state) {
462
- for (const m of state.modes.values()) {
463
- if (m.bindings.length > 0)
464
- return true;
465
- }
466
- return false;
467
- }
468
- function formatInvalidKeybindingDetail(invalidKeys) {
469
- return invalidKeys.map((invalid) => `"${invalid.key}": ${invalid.detail}`).join("; ");
470
- }
471
- function applyKeybindingState(nextState) {
312
+ const applyKeybindingState = (nextState) => {
472
313
  keybindingState = nextState;
473
314
  keybindingsEnabled = computeKeybindingsEnabled(keybindingState);
474
- }
475
- function registerAppBindings(bindings, options) {
476
- const result = registerBindings(keybindingState, bindings, {
477
- ...(options?.sourceTag === undefined ? {} : { sourceTag: options.sourceTag }),
478
- });
479
- if (result.invalidKeys.length > 0) {
480
- const method = options?.method ?? "keys";
481
- throwCode("ZRUI_INVALID_PROPS", `${method}: invalid keybinding sequence(s): ${formatInvalidKeybindingDetail(result.invalidKeys)}`);
482
- }
483
- applyKeybindingState(result.state);
484
- }
485
- function registerAppModes(modes) {
486
- let result;
487
- try {
488
- result = registerModes(keybindingState, modes);
489
- }
490
- catch (error) {
491
- throwCode("ZRUI_INVALID_PROPS", `modes: ${describeThrown(error)}`);
492
- }
493
- if (result.invalidKeys.length > 0) {
494
- throwCode("ZRUI_INVALID_PROPS", `modes: invalid keybinding sequence(s): ${formatInvalidKeybindingDetail(result.invalidKeys)}`);
495
- }
496
- applyKeybindingState(result.state);
497
- }
498
- function replaceRouteBindings(bindings) {
499
- const baseState = removeBindingsBySource(keybindingState, ROUTE_KEYBINDING_SOURCE);
500
- if (Object.keys(bindings).length === 0) {
501
- applyKeybindingState(baseState);
502
- return;
503
- }
504
- const result = registerBindings(baseState, bindings, {
505
- sourceTag: ROUTE_KEYBINDING_SOURCE,
506
- });
507
- if (result.invalidKeys.length > 0) {
508
- throwCode("ZRUI_INVALID_PROPS", `replaceRoutes: invalid keybinding sequence(s): ${formatInvalidKeybindingDetail(result.invalidKeys)}`);
509
- }
510
- applyKeybindingState(result.state);
511
- }
512
- function applyRoutedKeybindingState(routeInputState, routeNextState) {
513
- const previousChordState = keybindingState.chordState;
514
- // If handlers did not mutate keybinding state, take the routed state directly.
515
- if (keybindingState === routeInputState) {
516
- keybindingState = routeNextState;
517
- if (keybindingState.chordState !== previousChordState) {
518
- markDirty(DIRTY_VIEW);
519
- }
520
- return;
521
- }
522
- // Preserve handler-triggered mode changes (for example app.setMode() in a binding).
523
- if (keybindingState.currentMode !== routeInputState.currentMode) {
524
- return;
525
- }
526
- // For non-mode mutations (e.g. app.keys/app.modes in a handler), keep those
527
- // edits but still advance chord state from the routed event.
528
- keybindingState = Object.freeze({
529
- ...keybindingState,
530
- chordState: routeNextState.chordState,
531
- });
532
- if (keybindingState.chordState !== previousChordState) {
533
- markDirty(DIRTY_VIEW);
534
- }
535
- }
536
- function noteBreadcrumbEvent(kind) {
537
- breadcrumbEventTracked = false;
538
- if (!runtimeBreadcrumbsEnabled)
539
- return;
540
- if (!isRuntimeBreadcrumbEventKind(kind))
541
- return;
542
- breadcrumbLastEventKind = kind;
543
- breadcrumbLastConsumptionPath = null;
544
- breadcrumbEventTracked = true;
545
- }
546
- function noteBreadcrumbConsumptionPath(path) {
547
- if (!runtimeBreadcrumbsEnabled)
548
- return;
549
- if (!breadcrumbEventTracked)
550
- return;
551
- breadcrumbLastConsumptionPath = path;
552
- }
553
- function noteBreadcrumbAction(action) {
554
- if (!runtimeBreadcrumbsEnabled)
555
- return;
556
- if (!breadcrumbEventTracked)
557
- return;
558
- breadcrumbLastAction = toRuntimeBreadcrumbAction(action);
559
- }
315
+ };
316
+ const keybindingHelpers = createAppKeybindingHelpers({
317
+ getState: () => keybindingState,
318
+ markDirty: (flags) => markDirty(flags),
319
+ setState: applyKeybindingState,
320
+ throwCode: guards.throwCode,
321
+ });
322
+ const runtimeBreadcrumbHelpers = createRuntimeBreadcrumbHelpers({
323
+ getBaseInternalOnLayout: () => baseInternalOnLayout,
324
+ getBaseInternalOnRender: () => baseInternalOnRender,
325
+ getInspectorInternalOnLayout: () => inspectorInternalOnLayout,
326
+ getInspectorInternalOnRender: () => inspectorInternalOnRender,
327
+ getLastAction: () => breadcrumbLastAction,
328
+ getLastConsumptionPath: () => breadcrumbLastConsumptionPath,
329
+ getLastEventKind: () => breadcrumbLastEventKind,
330
+ getWidgetRuntimeBreadcrumbSnapshot: () => widgetRenderer.getRuntimeBreadcrumbSnapshot(),
331
+ isEnabled: () => runtimeBreadcrumbsEnabled,
332
+ isEventTracked: () => breadcrumbEventTracked,
333
+ setEnabled: (enabled) => {
334
+ runtimeBreadcrumbsEnabled = enabled;
335
+ },
336
+ setEventTracked: (tracked) => {
337
+ breadcrumbEventTracked = tracked;
338
+ },
339
+ setInspectorInternalOnLayout: (callback) => {
340
+ inspectorInternalOnLayout = callback;
341
+ },
342
+ setInspectorInternalOnRender: (callback) => {
343
+ inspectorInternalOnRender = callback;
344
+ },
345
+ setLastAction: (action) => {
346
+ breadcrumbLastAction = action;
347
+ },
348
+ setLastConsumptionPath: (path) => {
349
+ breadcrumbLastConsumptionPath = path;
350
+ },
351
+ setLastEventKind: (kind) => {
352
+ breadcrumbLastEventKind = kind;
353
+ },
354
+ setWidgetRuntimeBreadcrumbCaptureEnabled: (enabled) => {
355
+ widgetRenderer.setRuntimeBreadcrumbCaptureEnabled(enabled);
356
+ },
357
+ });
560
358
  function retryTopLevelViewError() {
561
359
  topLevelViewError = null;
562
360
  markDirty(DIRTY_VIEW | DIRTY_LAYOUT);
@@ -589,88 +387,16 @@ export function createApp(opts) {
589
387
  try {
590
388
  stopPromise = app.stop();
591
389
  }
592
- catch (e) {
593
- // Late events can race while a stop is already in-flight; avoid double-fatal.
390
+ catch (error) {
594
391
  if (lifecycleBusy === "stop")
595
392
  return;
596
- fatalNowOrEnqueue("ZRUI_BACKEND_ERROR", `stop threw after unhandled quit input: ${describeThrown(e)}`);
393
+ fatalNowOrEnqueue("ZRUI_BACKEND_ERROR", `stop threw after unhandled quit input: ${describeThrown(error)}`);
597
394
  return;
598
395
  }
599
- void stopPromise.catch((e) => {
600
- fatalNowOrEnqueue("ZRUI_BACKEND_ERROR", `stop rejected after unhandled quit input: ${describeThrown(e)}`);
396
+ void stopPromise.catch((error) => {
397
+ fatalNowOrEnqueue("ZRUI_BACKEND_ERROR", `stop rejected after unhandled quit input: ${describeThrown(error)}`);
601
398
  });
602
399
  }
603
- function buildRuntimeBreadcrumbSnapshot(renderTimeMs) {
604
- if (!runtimeBreadcrumbsEnabled)
605
- return null;
606
- const widgetSnapshot = widgetRenderer.getRuntimeBreadcrumbSnapshot();
607
- if (!widgetSnapshot)
608
- return null;
609
- return mergeRuntimeBreadcrumbSnapshot(widgetSnapshot, breadcrumbLastEventKind, breadcrumbLastConsumptionPath, breadcrumbLastAction, renderTimeMs);
610
- }
611
- function throwCode(code, detail) {
612
- throw new ZrUiError(code, detail);
613
- }
614
- function assertOperational(method) {
615
- const st = sm.state;
616
- if (st === "Disposed" || st === "Faulted") {
617
- throwCode("ZRUI_INVALID_STATE", `${method}: app is ${st}`);
618
- }
619
- if (lifecycleBusy !== null) {
620
- throwCode("ZRUI_INVALID_STATE", `${method}: lifecycle operation already in flight`);
621
- }
622
- }
623
- function assertLifecycleIdle(method) {
624
- if (lifecycleBusy !== null) {
625
- throwCode("ZRUI_INVALID_STATE", `${method}: lifecycle operation already in flight`);
626
- }
627
- }
628
- function assertNotReentrant(method) {
629
- if (inCommit || inRender || inEventHandlerDepth > 0) {
630
- throwCode("ZRUI_REENTRANT_CALL", `${method}: re-entrant call`);
631
- }
632
- }
633
- function enqueueFatal(code, detail) {
634
- scheduler.enqueue({ kind: "fatal", code, detail });
635
- }
636
- function flushDeferredInlineFatal() {
637
- if (deferredInlineFatal === null || inEventHandlerDepth !== 0)
638
- return;
639
- const fatal = deferredInlineFatal;
640
- deferredInlineFatal = null;
641
- doFatal(fatal.code, fatal.detail);
642
- }
643
- function fatalNowOrEnqueue(code, detail) {
644
- const canFailFastInline = scheduler.isExecuting && !inRender && !inCommit;
645
- if (canFailFastInline && inEventHandlerDepth > 0) {
646
- if (deferredInlineFatal === null) {
647
- deferredInlineFatal = Object.freeze({ code, detail });
648
- }
649
- return;
650
- }
651
- if (canFailFastInline) {
652
- doFatal(code, detail);
653
- return;
654
- }
655
- enqueueFatal(code, detail);
656
- }
657
- function updateDuringRenderDetail(method) {
658
- return `${method}: called during render. Hint: This usually means an onPress/onChange callback calls app.update() synchronously during the render phase. Move state updates to event handlers or useEffect.`;
659
- }
660
- function assertRouterMutationAllowed(method) {
661
- assertOperational(method);
662
- if (inCommit)
663
- throwCode("ZRUI_REENTRANT_CALL", `${method}: called during commit`);
664
- if (inRender)
665
- throwCode("ZRUI_UPDATE_DURING_RENDER", updateDuringRenderDetail(method));
666
- }
667
- function assertKeybindingMutationAllowed(method) {
668
- assertOperational(method);
669
- if (inCommit)
670
- throwCode("ZRUI_REENTRANT_CALL", `${method}: called during commit`);
671
- if (inRender)
672
- throwCode("ZRUI_UPDATE_DURING_RENDER", updateDuringRenderDetail(method));
673
- }
674
400
  if (routes !== undefined) {
675
401
  routerIntegration = createRouterIntegration({
676
402
  routes,
@@ -679,12 +405,10 @@ export function createApp(opts) {
679
405
  ? {}
680
406
  : { maxHistoryDepth: opts.routeHistoryMaxDepth }),
681
407
  getState: () => committedState,
682
- // Route transitions can swap the entire screen tree; force both commit
683
- // and layout so id->rect indexes and focus metadata stay in sync.
684
408
  requestRouteRender: () => markDirty(DIRTY_VIEW | DIRTY_LAYOUT),
685
409
  captureFocusSnapshot: () => widgetRenderer.captureFocusSnapshot(),
686
410
  restoreFocusSnapshot: (snapshot) => widgetRenderer.restoreFocusSnapshot(snapshot),
687
- assertCanMutate: assertRouterMutationAllowed,
411
+ assertCanMutate: guards.assertRouterMutationAllowed,
688
412
  });
689
413
  }
690
414
  function emit(ev) {
@@ -699,8 +423,8 @@ export function createApp(opts) {
699
423
  try {
700
424
  fn(ev);
701
425
  }
702
- catch (e) {
703
- fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `onEvent handler threw: ${describeThrown(e)}`);
426
+ catch (error) {
427
+ fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `onEvent handler threw: ${describeThrown(error)}`);
704
428
  return false;
705
429
  }
706
430
  }
@@ -714,676 +438,154 @@ export function createApp(opts) {
714
438
  function emitFocusChangeIfNeeded() {
715
439
  return focusDispatcher.emitIfChanged();
716
440
  }
717
- function doFatal(code, detail) {
718
- if (sm.state !== "Running")
719
- return;
720
- lifecycleBusy = null;
721
- lifecycleGeneration++;
722
- backendStarted = false;
723
- // 1) emit fatal to handlers (registration order, best-effort)
724
- const fatalEv = { kind: "fatal", code, detail };
725
- const snapshot = [];
726
- for (const slot of handlers) {
727
- if (slot.active.value)
728
- snapshot.push(slot.fn);
729
- }
730
- for (const fn of snapshot) {
731
- try {
732
- fn(fatalEv);
733
- }
734
- catch {
735
- // ignore
736
- }
737
- }
738
- // 2) transition to Faulted
739
- try {
740
- sm.toFaulted();
741
- }
742
- catch {
743
- // ignore
744
- }
745
- // Stop polling immediately.
746
- pollToken++;
747
- // 3) backend stop/dispose best-effort (stop then dispose)
748
- try {
749
- void backend
750
- .stop()
751
- .catch(() => undefined)
752
- .finally(() => {
753
- try {
754
- backend.dispose();
755
- }
756
- catch {
757
- // ignore
758
- }
759
- settleActiveRun?.();
760
- });
761
- }
762
- catch {
763
- try {
764
- backend.dispose();
765
- }
766
- catch {
767
- // ignore
768
- }
769
- settleActiveRun?.();
770
- }
771
- }
772
- function cleanupStartedBackendAfterAbort() {
773
- if (!backendStarted)
774
- return;
775
- backendStarted = false;
776
- try {
777
- void backend
778
- .stop()
779
- .catch(() => undefined)
780
- .finally(() => {
781
- try {
782
- backend.dispose();
783
- }
784
- catch {
785
- // ignore
786
- }
787
- });
788
- }
789
- catch {
790
- try {
791
- backend.dispose();
792
- }
793
- catch {
794
- // ignore
795
- }
796
- }
797
- }
798
- function releaseOnce(batch, releasedBatches) {
799
- let released = false;
800
- return () => {
801
- if (released)
802
- return;
803
- released = true;
804
- releasedBatches.add(batch);
805
- try {
806
- batch.release();
807
- }
808
- catch {
809
- // ignore
810
- }
811
- };
812
- }
813
- function processEventBatch(batch, releasedBatches) {
814
- const release = releaseOnce(batch, releasedBatches);
815
- const parseToken = perfMarkStart("event_parse");
816
- const parsed = parseEventBatchV1(batch.bytes, {
817
- maxTotalSize: config.maxEventBytes,
818
- timeUnwrap,
819
- });
820
- perfMarkEnd("event_parse", parseToken);
821
- if (!parsed.ok) {
822
- release();
823
- fatalNowOrEnqueue("ZRUI_PROTOCOL_ERROR", `${parsed.error.code}: ${parsed.error.detail}`);
824
- return;
825
- }
826
- const engineTruncated = (parsed.value.flags & 1) !== 0;
827
- const droppedBatches = batch.droppedBatches;
828
- try {
829
- if (engineTruncated || droppedBatches > 0) {
830
- if (!emit({ kind: "overrun", engineTruncated, droppedBatches }))
831
- return;
832
- if (sm.state !== "Running")
833
- return;
834
- }
835
- for (const ev of parsed.value.events) {
836
- // Input-priority / preemption: when an interactive input event arrives,
837
- // allow a short urgent burst even if a previous frame is still
838
- // in-flight. This keeps interactive latency resilient to transport/ack
839
- // jitter while older frames are still coalesced downstream (latest-wins)
840
- // in the backend/worker.
841
- if (ev.kind === "key" || ev.kind === "text" || ev.kind === "paste" || ev.kind === "mouse") {
842
- interactiveBudget = 2;
843
- }
844
- noteBreadcrumbEvent(ev.kind);
845
- if (!emit({ kind: "engine", event: ev }))
846
- return;
847
- if (sm.state !== "Running")
848
- return;
849
- if (ev.kind === "resize") {
850
- const prev = viewport;
851
- if (prev === null || prev.cols !== ev.cols || prev.rows !== ev.rows) {
852
- viewport = Object.freeze({ cols: ev.cols, rows: ev.rows });
853
- if (widgetRenderer.hasViewportAwareComposites()) {
854
- widgetRenderer.invalidateCompositeWidgets();
855
- markDirty(DIRTY_LAYOUT | DIRTY_VIEW);
856
- }
857
- else {
858
- markDirty(DIRTY_LAYOUT);
859
- }
860
- }
861
- }
862
- if (ev.kind === "tick" && mode === "widget") {
863
- // Tick events drive render-only animation frames for animated widgets
864
- // (currently spinner). Throttle to avoid repaint storms/flicker.
865
- //
866
- // Prefer backend tick timestamps when they advance, but fall back to
867
- // local monotonic time for runtimes/terminals where tick time is
868
- // constant or non-monotonic.
869
- if (widgetRenderer.hasAnimatedWidgets()) {
870
- const tickMs = ev.timeMs;
871
- const perfMs = perfNow();
872
- const eventClockAdvances = tickMs > lastObservedSpinnerTickEventMs;
873
- if (eventClockAdvances)
874
- lastObservedSpinnerTickEventMs = tickMs;
875
- const elapsedMs = eventClockAdvances
876
- ? tickMs - lastSpinnerRenderTickMs
877
- : perfMs - lastSpinnerRenderPerfMs;
878
- if (elapsedMs >= spinnerTickMinIntervalMs) {
879
- lastSpinnerRenderTickMs = tickMs;
880
- lastSpinnerRenderPerfMs = perfMs;
881
- markDirty(DIRTY_RENDER);
882
- }
883
- }
884
- }
885
- if (mode === "widget" && topLevelViewError !== null) {
886
- if (isTopLevelRetryEvent(ev)) {
887
- noteBreadcrumbConsumptionPath("widgetRouting");
888
- retryTopLevelViewError();
889
- continue;
890
- }
891
- if (isTopLevelQuitEvent(ev)) {
892
- noteBreadcrumbConsumptionPath("widgetRouting");
893
- quitFromTopLevelViewError();
894
- continue;
895
- }
896
- if (ev.kind === "key" ||
897
- ev.kind === "text" ||
898
- ev.kind === "paste" ||
899
- ev.kind === "mouse") {
900
- noteBreadcrumbConsumptionPath("widgetRouting");
901
- continue;
902
- }
903
- }
904
- const isWidgetRoutableEvent = ev.kind === "key" || ev.kind === "text" || ev.kind === "paste" || ev.kind === "mouse";
905
- if (mode === "widget" && isWidgetRoutableEvent) {
906
- if (keybindingsEnabled) {
907
- if (ev.kind === "mouse" &&
908
- ev.mouseKind === 3 &&
909
- keybindingState.chordState.pendingKeys.length > 0) {
910
- keybindingState = Object.freeze({
911
- ...keybindingState,
912
- chordState: resetChordState(),
913
- });
914
- }
915
- // Route key events through keybinding system first
916
- if (ev.kind === "key") {
917
- const bypass = widgetRenderer.shouldBypassKeybindings(ev);
918
- if (!bypass) {
919
- const keyCtx = Object.freeze({
920
- state: committedState,
921
- update: app.update,
922
- focusedId: widgetRenderer.getFocusedId(),
923
- });
924
- const routeInputState = keybindingState;
925
- const keyResult = routeKeyEvent(routeInputState, ev, keyCtx);
926
- applyRoutedKeybindingState(routeInputState, keyResult.nextState);
927
- if (keyResult.handlerError !== undefined) {
928
- fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `keybinding handler threw: ${describeThrown(keyResult.handlerError)}`);
929
- return;
930
- }
931
- if (keyResult.consumed) {
932
- noteBreadcrumbConsumptionPath("keybindings");
933
- continue; // Skip default widget routing
934
- }
935
- }
936
- }
937
- // Also route text events through keybinding system for single-character bindings.
938
- // Printable text is guarded during overlays, but Ctrl+text control chars are not.
939
- if (ev.kind === "text") {
940
- const ctrlKeyCode = codepointToCtrlKeyCode(ev.codepoint);
941
- const shouldRouteCtrlText = ctrlKeyCode !== null;
942
- const shouldRoutePrintableText = !shouldRouteCtrlText && !widgetRenderer.hasActiveOverlay();
943
- if (shouldRouteCtrlText || shouldRoutePrintableText) {
944
- const keyCode = shouldRouteCtrlText
945
- ? ctrlKeyCode
946
- : codepointToKeyCode(ev.codepoint);
947
- const mods = shouldRouteCtrlText ? ZR_MOD_CTRL : 0;
948
- if (keyCode !== null) {
949
- // Create a synthetic key event for keybinding matching
950
- const syntheticKeyEvent = {
951
- kind: "key",
952
- action: "down",
953
- key: keyCode,
954
- mods,
955
- timeMs: ev.timeMs,
956
- };
957
- const keyCtx = Object.freeze({
958
- state: committedState,
959
- update: app.update,
960
- focusedId: widgetRenderer.getFocusedId(),
961
- });
962
- const routeInputState = keybindingState;
963
- const keyResult = routeKeyEvent(routeInputState, syntheticKeyEvent, keyCtx);
964
- applyRoutedKeybindingState(routeInputState, keyResult.nextState);
965
- if (keyResult.handlerError !== undefined) {
966
- fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `keybinding handler threw: ${describeThrown(keyResult.handlerError)}`);
967
- return;
968
- }
969
- if (keyResult.consumed) {
970
- noteBreadcrumbConsumptionPath("keybindings");
971
- continue; // Skip default widget routing
972
- }
973
- }
974
- }
975
- }
976
- }
977
- let routed;
978
- try {
979
- noteBreadcrumbConsumptionPath("widgetRouting");
980
- routed = widgetRenderer.routeEngineEvent(ev);
981
- }
982
- catch (e) {
983
- fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `widget routing threw: ${describeThrown(e)}`);
984
- return;
985
- }
986
- if (sm.state !== "Running")
987
- return;
988
- if (!emitFocusChangeIfNeeded())
989
- return;
990
- if (routed.needsRender)
991
- markDirty(DIRTY_RENDER);
992
- if (routed.action) {
993
- noteBreadcrumbAction(routed.action);
994
- if (!emit({ kind: "action", ...routed.action }))
995
- return;
996
- if (sm.state !== "Running")
997
- return;
998
- }
999
- if (routed.action === undefined &&
1000
- !routed.needsRender &&
1001
- routed.consumed !== true &&
1002
- (isUnmodifiedTextQuitEvent(ev) || isUnhandledCtrlCKeyEvent(ev))) {
1003
- noteBreadcrumbConsumptionPath("widgetRouting");
1004
- stopFromUnhandledQuitEvent();
1005
- }
1006
- }
1007
- }
1008
- }
1009
- finally {
1010
- release();
1011
- }
1012
- }
1013
- function commitUpdates() {
1014
- const drained = updates.drain();
1015
- if (drained.length === 0)
1016
- return;
1017
- const commitToken = perfMarkStart("commit");
1018
- inCommit = true;
1019
- try {
1020
- let next = committedState;
1021
- for (const u of drained) {
1022
- if (typeof u === "function") {
1023
- next = u(next);
1024
- }
1025
- else {
1026
- next = u;
1027
- }
1028
- }
1029
- if (next !== committedState) {
1030
- committedState = next;
1031
- markDirty(DIRTY_VIEW, false);
1032
- }
1033
- }
1034
- catch (e) {
1035
- fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `state updater threw: ${describeThrown(e)}`);
1036
- }
1037
- finally {
1038
- inCommit = false;
1039
- perfMarkEnd("commit", commitToken);
1040
- }
1041
- }
1042
- function scheduleFrameSettlement(p, submitStart, submitEnd) {
1043
- if (isSyncFrameAck(p)) {
1044
- if (PERF_ENABLED && submitStart !== null) {
1045
- const ackNow = perfNow();
1046
- perfRecord("backend_ack", ackNow - submitStart);
1047
- if (submitEnd !== null) {
1048
- perfRecord("frame_build", submitEnd - submitStart);
1049
- perfRecord("worker_roundtrip", ackNow - submitEnd);
1050
- }
1051
- }
1052
- framesInFlight = Math.max(0, framesInFlight - 1);
1053
- return;
1054
- }
1055
- const acceptedAck = getAcceptedFrameAck(p);
1056
- const ackPromise = acceptedAck ?? p;
1057
- void ackPromise.then(() => {
1058
- if (PERF_ENABLED && submitStart !== null) {
1059
- const ackNow = perfNow();
1060
- // backend_ack: total time from frame build start to backend ack.
1061
- // Equals frame_build + worker_roundtrip (kept for backward compat).
1062
- perfRecord("backend_ack", ackNow - submitStart);
1063
- if (submitEnd !== null) {
1064
- // frame_build: synchronous TS pipeline (view/commit/layout/render/build).
1065
- perfRecord("frame_build", submitEnd - submitStart);
1066
- // worker_roundtrip: async transport from requestFrame to backend ack.
1067
- perfRecord("worker_roundtrip", ackNow - submitEnd);
1068
- }
1069
- }
1070
- scheduler.enqueue({ kind: "frameDone" });
1071
- }, (err) => scheduler.enqueue({ kind: "frameError", error: err }));
1072
- if (acceptedAck !== null) {
1073
- void p.then(() => { }, (err) => scheduler.enqueue({
1074
- kind: "fatal",
1075
- code: "ZRUI_BACKEND_ERROR",
1076
- detail: `requestFrame completion rejected after accepted ack: ${describeThrown(err)}`,
1077
- }));
1078
- }
1079
- }
1080
- function emitInternalRenderMetrics(renderTime, runtimeBreadcrumbs = null) {
1081
- if (baseInternalOnRender === undefined && inspectorInternalOnRender === undefined)
1082
- return true;
1083
- try {
1084
- const clampedRenderTime = Math.max(0, renderTime);
1085
- if (runtimeBreadcrumbs) {
1086
- const payload = {
1087
- renderTime: clampedRenderTime,
1088
- runtimeBreadcrumbs,
1089
- };
1090
- baseInternalOnRender?.(payload);
1091
- inspectorInternalOnRender?.(payload);
1092
- }
1093
- else {
1094
- const payload = { renderTime: clampedRenderTime };
1095
- baseInternalOnRender?.(payload);
1096
- inspectorInternalOnRender?.(payload);
1097
- }
1098
- return true;
1099
- }
1100
- catch (e) {
1101
- fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `onRender callback threw: ${describeThrown(e)}`);
1102
- return false;
1103
- }
1104
- }
1105
- function emitInternalLayoutSnapshot(runtimeBreadcrumbs = null) {
1106
- if (baseInternalOnLayout === undefined && inspectorInternalOnLayout === undefined)
1107
- return true;
1108
- try {
1109
- const idRects = widgetRenderer.getRectByIdIndex();
1110
- if (runtimeBreadcrumbs) {
1111
- const payload = {
1112
- idRects,
1113
- runtimeBreadcrumbs,
1114
- };
1115
- baseInternalOnLayout?.(payload);
1116
- inspectorInternalOnLayout?.(payload);
1117
- }
1118
- else {
1119
- const payload = { idRects };
1120
- baseInternalOnLayout?.(payload);
1121
- inspectorInternalOnLayout?.(payload);
1122
- }
1123
- return true;
1124
- }
1125
- catch (e) {
1126
- fatalNowOrEnqueue("ZRUI_USER_CODE_THROW", `onLayout callback threw: ${describeThrown(e)}`);
1127
- return false;
1128
- }
1129
- }
1130
- function tryRenderOnce() {
1131
- if (sm.state !== "Running")
1132
- return;
1133
- // During stop(), we may still receive a few late event batches, but we must not
1134
- // submit new frames (backend may be tearing down).
1135
- if (lifecycleBusy === "stop")
1136
- return;
1137
- if (dirtyTracker.getFlags() === 0)
1138
- return;
1139
- const maxInFlight = config.maxFramesInFlight + (interactiveBudget > 0 ? 1 : 0);
1140
- if (framesInFlight >= maxInFlight)
1141
- return;
1142
- if (mode === null)
1143
- return;
1144
- // Record schedule_wait: time from render request to render start
1145
- if (PERF_ENABLED && scheduleWaitStartMs !== null) {
1146
- perfMarkEnd("schedule_wait", scheduleWaitStartMs);
1147
- scheduleWaitStartMs = null;
1148
- }
1149
- const dirtyVersionStart = dirtyTracker.snapshotVersions();
1150
- const snapshot = committedState;
1151
- const hooks = {
1152
- enterRender: () => {
1153
- inRender = true;
1154
- },
1155
- exitRender: () => {
1156
- inRender = false;
1157
- },
1158
- };
1159
- if (mode === "raw") {
1160
- const df = drawFn;
1161
- if (!df)
1162
- return;
1163
- const renderStart = perfNow();
1164
- const submitToken = perfMarkStart("submit_frame");
1165
- const res = rawRenderer.submitFrame(df, hooks);
1166
- perfMarkEnd("submit_frame", submitToken);
1167
- if (!res.ok) {
1168
- fatalNowOrEnqueue(res.code, res.detail);
1169
- return;
1170
- }
1171
- if (!emitInternalRenderMetrics(perfNow() - renderStart))
1172
- return;
1173
- submitFrameStartMs = PERF_ENABLED ? submitToken : null;
1174
- const buildEndMs = PERF_ENABLED ? perfNow() : null;
1175
- framesInFlight++;
1176
- if (interactiveBudget > 0)
1177
- interactiveBudget--;
1178
- scheduleFrameSettlement(res.inFlight, submitFrameStartMs, buildEndMs);
1179
- dirtyTracker.clearConsumedFlags(DIRTY_RENDER | DIRTY_LAYOUT | DIRTY_VIEW, dirtyVersionStart);
1180
- return;
1181
- }
1182
- const vf = viewFn;
1183
- if (!vf)
1184
- return;
1185
- if (!viewport)
1186
- return;
1187
- const pendingDirtyFlags = dirtyTracker.getFlags();
1188
- if ((pendingDirtyFlags & (DIRTY_VIEW | DIRTY_LAYOUT | DIRTY_RENDER)) === 0)
1189
- return;
1190
- // Compute render plan from dirty flags. Render-only turns (e.g., focus change)
1191
- // skip view/commit/layout. Layout-only turns (e.g., resize without state change)
1192
- // skip view/commit. Commit turns now rely on WidgetRenderer layout signatures
1193
- // to decide whether relayout is required, instead of forcing layout by default.
1194
- // First-frame/bootstrap safety is handled inside submitFrame(): it falls back
1195
- // to full pipeline when committedRoot or layoutTree is null.
1196
- const frameNowMs = monotonicNowMs();
1197
- const plan = buildWidgetRenderPlan(pendingDirtyFlags, frameNowMs);
1198
- advanceThemeTransitionFrame();
1199
- const resilientView = (state) => {
1200
- if (topLevelViewError !== null) {
1201
- return buildTopLevelViewErrorScreen(topLevelViewError);
1202
- }
1203
- try {
1204
- return vf(state);
1205
- }
1206
- catch (e) {
1207
- topLevelViewError = captureTopLevelViewError(e);
1208
- return buildTopLevelViewErrorScreen(topLevelViewError);
1209
- }
1210
- };
1211
- const renderStart = perfNow();
1212
- const submitToken = perfMarkStart("submit_frame");
1213
- const frameView = debugLayoutEnabled
1214
- ? (state) => {
1215
- const root = resilientView(state);
1216
- const overlay = buildLayoutDebugOverlay(widgetRenderer.getRectByIdIndex());
1217
- if (!overlay)
1218
- return root;
1219
- return ui.layers([root, overlay]);
1220
- }
1221
- : resilientView;
1222
- const res = widgetRenderer.submitFrame(frameView, snapshot, viewport, theme, hooks, plan);
1223
- perfMarkEnd("submit_frame", submitToken);
1224
- if (!res.ok) {
1225
- fatalNowOrEnqueue(res.code, res.detail);
1226
- return;
1227
- }
1228
- if (!emitFocusChangeIfNeeded())
1229
- return;
1230
- const renderTime = perfNow() - renderStart;
1231
- const runtimeBreadcrumbs = buildRuntimeBreadcrumbSnapshot(Math.max(0, renderTime));
1232
- if (!emitInternalRenderMetrics(renderTime, runtimeBreadcrumbs))
1233
- return;
1234
- if (!emitInternalLayoutSnapshot(runtimeBreadcrumbs))
1235
- return;
1236
- submitFrameStartMs = PERF_ENABLED ? submitToken : null;
1237
- const buildEndMs = PERF_ENABLED ? perfNow() : null;
1238
- framesInFlight++;
1239
- if (interactiveBudget > 0)
1240
- interactiveBudget--;
1241
- scheduleFrameSettlement(res.inFlight, submitFrameStartMs, buildEndMs);
1242
- let consumedDirtyFlags = DIRTY_RENDER;
1243
- if (plan.layout)
1244
- consumedDirtyFlags |= DIRTY_LAYOUT;
1245
- if (plan.commit)
1246
- consumedDirtyFlags |= DIRTY_VIEW;
1247
- dirtyTracker.clearConsumedFlags(consumedDirtyFlags, dirtyVersionStart);
1248
- scheduleThemeTransitionContinuation();
1249
- if (dirtyTracker.getFlags() !== 0 && !renderRequestQueuedForCurrentTurn) {
1250
- renderRequestQueuedForCurrentTurn = true;
1251
- scheduler.enqueue({ kind: "renderRequest" });
1252
- }
1253
- }
1254
- function drainIgnored(items, releasedBatches) {
1255
- for (const it of items) {
1256
- if (it.kind === "eventBatch" && !releasedBatches.has(it.batch)) {
1257
- releasedBatches.add(it.batch);
1258
- try {
1259
- it.batch.release();
1260
- }
1261
- catch {
1262
- // ignore
1263
- }
1264
- }
1265
- }
1266
- }
1267
- function processTurn(items) {
1268
- renderRequestQueuedForCurrentTurn = false;
1269
- const releasedBatches = new Set();
1270
- const st = sm.state;
1271
- if (st === "Disposed" || st === "Faulted") {
1272
- drainIgnored(items, releasedBatches);
1273
- return;
1274
- }
1275
- let sawKick = false;
1276
- for (const item of items) {
1277
- if (sm.state === "Faulted" || sm.state === "Disposed") {
1278
- drainIgnored(items, releasedBatches);
1279
- return;
1280
- }
1281
- switch (item.kind) {
1282
- case "fatal": {
1283
- doFatal(item.code, item.detail);
1284
- drainIgnored(items, releasedBatches);
1285
- return;
1286
- }
1287
- case "eventBatch": {
1288
- if (sm.state !== "Running") {
1289
- releasedBatches.add(item.batch);
1290
- try {
1291
- item.batch.release();
1292
- }
1293
- catch {
1294
- // ignore
1295
- }
1296
- break;
1297
- }
1298
- processEventBatch(item.batch, releasedBatches);
1299
- if (sm.state !== "Running") {
1300
- drainIgnored(items, releasedBatches);
1301
- return;
1302
- }
1303
- commitUpdates();
1304
- break;
1305
- }
1306
- case "userCommit": {
1307
- userCommitScheduled = false;
1308
- if (sm.state === "Running")
1309
- commitUpdates();
1310
- break;
1311
- }
1312
- case "kick": {
1313
- sawKick = true;
1314
- break;
1315
- }
1316
- case "renderRequest": {
1317
- break;
1318
- }
1319
- case "frameDone": {
1320
- framesInFlight = Math.max(0, framesInFlight - 1);
1321
- break;
1322
- }
1323
- case "frameError": {
1324
- framesInFlight = Math.max(0, framesInFlight - 1);
1325
- // If we are intentionally stopping, treat requestFrame rejections as
1326
- // part of shutdown (not a fatal backend error).
1327
- if (lifecycleBusy === "stop")
1328
- break;
1329
- doFatal("ZRUI_BACKEND_ERROR", `requestFrame rejected: ${describeThrown(item.error)}`);
1330
- break;
1331
- }
1332
- }
1333
- }
1334
- if (sm.state !== "Running")
1335
- return;
1336
- if (sawKick)
1337
- commitUpdates();
1338
- tryRenderOnce();
1339
- }
1340
- async function pollLoop(token) {
1341
- while (sm.state === "Running" && token === pollToken) {
1342
- let batch;
1343
- try {
1344
- batch = await backend.pollEvents();
1345
- }
1346
- catch (e) {
1347
- if (sm.state === "Running" && token === pollToken) {
1348
- fatalNowOrEnqueue("ZRUI_BACKEND_ERROR", `pollEvents rejected: ${describeThrown(e)}`);
1349
- }
1350
- return;
1351
- }
1352
- if (token !== pollToken || sm.state !== "Running") {
1353
- try {
1354
- batch.release();
1355
- }
1356
- catch {
1357
- // ignore
1358
- }
1359
- return;
1360
- }
1361
- scheduler.enqueue({ kind: "eventBatch", batch });
1362
- }
1363
- }
441
+ const renderLoop = createRenderLoop({
442
+ buildRuntimeBreadcrumbSnapshot: runtimeBreadcrumbHelpers.buildRuntimeBreadcrumbSnapshot,
443
+ config,
444
+ dirtyTracker,
445
+ emitFocusChangeIfNeeded,
446
+ enqueueWorkItem,
447
+ fatalNowOrEnqueue,
448
+ getBaseInternalOnLayout: () => baseInternalOnLayout,
449
+ getBaseInternalOnRender: () => baseInternalOnRender,
450
+ getCommittedState: () => committedState,
451
+ getDebugLayoutEnabled: () => debugLayoutEnabled,
452
+ getDrawFn: () => drawFn,
453
+ getFramesInFlight: () => framesInFlight,
454
+ getInspectorInternalOnLayout: () => inspectorInternalOnLayout,
455
+ getInspectorInternalOnRender: () => inspectorInternalOnRender,
456
+ getInteractiveBudget: () => interactiveBudget,
457
+ getLifecycleBusy: () => lifecycleBusy,
458
+ getMode: () => mode,
459
+ getRenderRequestQueuedForCurrentTurn: () => renderRequestQueuedForCurrentTurn,
460
+ getScheduleWaitStartMs: () => scheduleWaitStartMs,
461
+ getTheme: () => theme,
462
+ getThemeTransition: () => themeTransition,
463
+ getTopLevelViewError: () => topLevelViewError,
464
+ getViewFn: () => viewFn,
465
+ getViewport: () => viewport,
466
+ isRunning: () => sm.state === "Running",
467
+ markDirty,
468
+ rawRenderer,
469
+ setFramesInFlight: (next) => {
470
+ framesInFlight = next;
471
+ },
472
+ setInRender: (next) => {
473
+ inRender = next;
474
+ },
475
+ setInteractiveBudget: (next) => {
476
+ interactiveBudget = next;
477
+ },
478
+ setRenderRequestQueuedForCurrentTurn: (next) => {
479
+ renderRequestQueuedForCurrentTurn = next;
480
+ },
481
+ setScheduleWaitStartMs: (next) => {
482
+ scheduleWaitStartMs = next;
483
+ },
484
+ setTheme: (next) => {
485
+ theme = next;
486
+ },
487
+ setThemeTransition: (next) => {
488
+ themeTransition = next;
489
+ },
490
+ setTopLevelViewError: (next) => {
491
+ topLevelViewError = next;
492
+ },
493
+ widgetRenderer,
494
+ });
495
+ tryRenderOnceImpl = renderLoop.tryRenderOnce;
496
+ const eventLoop = createEventLoop({
497
+ backend,
498
+ config,
499
+ doFatal,
500
+ emit,
501
+ emitFocusChangeIfNeeded,
502
+ enqueueWorkItem,
503
+ fatalNowOrEnqueue,
504
+ getAppUpdate: () => app.update,
505
+ getCommittedState: () => committedState,
506
+ getFramesInFlight: () => framesInFlight,
507
+ getInteractiveBudget: () => interactiveBudget,
508
+ getKeybindingState: () => keybindingState,
509
+ getKeybindingsEnabled: () => keybindingsEnabled,
510
+ getLastObservedSpinnerTickEventMs: () => lastObservedSpinnerTickEventMs,
511
+ getLastSpinnerRenderPerfMs: () => lastSpinnerRenderPerfMs,
512
+ getLastSpinnerRenderTickMs: () => lastSpinnerRenderTickMs,
513
+ getLifecycleBusy: () => lifecycleBusy,
514
+ getMode: () => mode,
515
+ getPollToken: () => pollToken,
516
+ getRenderRequestQueuedForCurrentTurn: () => renderRequestQueuedForCurrentTurn,
517
+ getRuntimeState: () => sm.state,
518
+ getTopLevelViewError: () => topLevelViewError,
519
+ getViewport: () => viewport,
520
+ keybindingHelpers,
521
+ markDirty,
522
+ noteBreadcrumbAction: runtimeBreadcrumbHelpers.noteBreadcrumbAction,
523
+ noteBreadcrumbConsumptionPath: runtimeBreadcrumbHelpers.noteBreadcrumbConsumptionPath,
524
+ noteBreadcrumbEvent: runtimeBreadcrumbHelpers.noteBreadcrumbEvent,
525
+ quitFromTopLevelViewError,
526
+ retryTopLevelViewError,
527
+ setCommittedState: (next) => {
528
+ committedState = next;
529
+ },
530
+ setFramesInFlight: (next) => {
531
+ framesInFlight = next;
532
+ },
533
+ setInCommit: (next) => {
534
+ inCommit = next;
535
+ },
536
+ setInteractiveBudget: (next) => {
537
+ interactiveBudget = next;
538
+ },
539
+ setKeybindingState: applyKeybindingState,
540
+ setLastObservedSpinnerTickEventMs: (next) => {
541
+ lastObservedSpinnerTickEventMs = next;
542
+ },
543
+ setLastSpinnerRenderPerfMs: (next) => {
544
+ lastSpinnerRenderPerfMs = next;
545
+ },
546
+ setLastSpinnerRenderTickMs: (next) => {
547
+ lastSpinnerRenderTickMs = next;
548
+ },
549
+ setRenderRequestQueuedForCurrentTurn: (next) => {
550
+ renderRequestQueuedForCurrentTurn = next;
551
+ },
552
+ setUserCommitScheduled: (next) => {
553
+ userCommitScheduled = next;
554
+ },
555
+ setViewport: (next) => {
556
+ viewport = next;
557
+ },
558
+ spinnerTickMinIntervalMs,
559
+ stopFromUnhandledQuitEvent,
560
+ timeUnwrap,
561
+ tryRenderOnce: () => tryRenderOnceImpl(),
562
+ updates,
563
+ widgetRenderer,
564
+ });
565
+ processTurnImpl = eventLoop.processTurn;
1364
566
  const app = {
1365
567
  view(fn) {
1366
- assertOperational("view");
1367
- assertLifecycleIdle("view");
568
+ guards.assertOperational("view");
569
+ guards.assertLifecycleIdle("view");
1368
570
  sm.assertOneOf(["Created", "Stopped"], "view: must be Created or Stopped");
1369
- assertNotReentrant("view");
571
+ guards.assertNotReentrant("view");
1370
572
  if (routes !== undefined) {
1371
- throwCode("ZRUI_MODE_CONFLICT", "view: routes are configured in createApp(); screen rendering is managed by router");
573
+ guards.throwCode("ZRUI_MODE_CONFLICT", "view: routes are configured in createApp(); screen rendering is managed by router");
1372
574
  }
1373
575
  if (mode === "raw")
1374
- throwCode("ZRUI_MODE_CONFLICT", "view: draw mode already selected");
576
+ guards.throwCode("ZRUI_MODE_CONFLICT", "view: draw mode already selected");
1375
577
  mode = "widget";
1376
578
  viewFn = fn;
1377
579
  },
1378
580
  replaceView(fn) {
1379
- assertOperational("replaceView");
1380
- assertLifecycleIdle("replaceView");
1381
- assertNotReentrant("replaceView");
581
+ guards.assertOperational("replaceView");
582
+ guards.assertLifecycleIdle("replaceView");
583
+ guards.assertNotReentrant("replaceView");
1382
584
  if (routes !== undefined) {
1383
- throwCode("ZRUI_MODE_CONFLICT", "replaceView: routes are configured in createApp(); screen rendering is managed by router");
585
+ guards.throwCode("ZRUI_MODE_CONFLICT", "replaceView: routes are configured in createApp(); screen rendering is managed by router");
1384
586
  }
1385
587
  if (mode === "raw") {
1386
- throwCode("ZRUI_MODE_CONFLICT", "replaceView: draw mode already selected");
588
+ guards.throwCode("ZRUI_MODE_CONFLICT", "replaceView: draw mode already selected");
1387
589
  }
1388
590
  if (mode === null)
1389
591
  mode = "widget";
@@ -1395,17 +597,18 @@ export function createApp(opts) {
1395
597
  }
1396
598
  },
1397
599
  replaceRoutes(nextRoutes) {
1398
- assertOperational("replaceRoutes");
1399
- assertLifecycleIdle("replaceRoutes");
1400
- assertNotReentrant("replaceRoutes");
1401
- if (!routerIntegration || routes === undefined) {
1402
- throwCode("ZRUI_MODE_CONFLICT", "replaceRoutes: app was created without routes; use replaceView for view-mode apps");
600
+ guards.assertOperational("replaceRoutes");
601
+ guards.assertLifecycleIdle("replaceRoutes");
602
+ guards.assertNotReentrant("replaceRoutes");
603
+ const activeRouterIntegration = routerIntegration;
604
+ if (activeRouterIntegration === null || routes === undefined) {
605
+ throw new ZrUiError("ZRUI_MODE_CONFLICT", "replaceRoutes: app was created without routes; use replaceView for view-mode apps");
1403
606
  }
1404
607
  if (mode === "raw") {
1405
- throwCode("ZRUI_MODE_CONFLICT", "replaceRoutes: draw mode already selected");
608
+ guards.throwCode("ZRUI_MODE_CONFLICT", "replaceRoutes: draw mode already selected");
1406
609
  }
1407
- const nextRouteKeybindings = routerIntegration.replaceRoutes(nextRoutes);
1408
- replaceRouteBindings(nextRouteKeybindings);
610
+ const nextRouteKeybindings = activeRouterIntegration.replaceRoutes(nextRoutes);
611
+ keybindingHelpers.replaceRouteBindings(nextRouteKeybindings);
1409
612
  topLevelViewError = null;
1410
613
  if (sm.state === "Running") {
1411
614
  widgetRenderer.forceFullRenderNextFrame();
@@ -1413,19 +616,19 @@ export function createApp(opts) {
1413
616
  }
1414
617
  },
1415
618
  draw(fn) {
1416
- assertOperational("draw");
1417
- assertLifecycleIdle("draw");
619
+ guards.assertOperational("draw");
620
+ guards.assertLifecycleIdle("draw");
1418
621
  sm.assertOneOf(["Created", "Stopped"], "draw: must be Created or Stopped");
1419
- assertNotReentrant("draw");
622
+ guards.assertNotReentrant("draw");
1420
623
  if (mode === "widget")
1421
- throwCode("ZRUI_MODE_CONFLICT", "draw: view mode already selected");
624
+ guards.throwCode("ZRUI_MODE_CONFLICT", "draw: view mode already selected");
1422
625
  mode = "raw";
1423
626
  drawFn = fn;
1424
627
  },
1425
628
  onEvent(handler) {
1426
- assertOperational("onEvent");
629
+ guards.assertOperational("onEvent");
1427
630
  if (inCommit || inRender)
1428
- throwCode("ZRUI_REENTRANT_CALL", "onEvent: re-entrant call");
631
+ guards.throwCode("ZRUI_REENTRANT_CALL", "onEvent: re-entrant call");
1429
632
  const active = { value: true };
1430
633
  handlers.push({ fn: handler, active });
1431
634
  return () => {
@@ -1433,32 +636,34 @@ export function createApp(opts) {
1433
636
  };
1434
637
  },
1435
638
  onFocusChange(handler) {
1436
- assertOperational("onFocusChange");
639
+ guards.assertOperational("onFocusChange");
1437
640
  if (inCommit || inRender) {
1438
- throwCode("ZRUI_REENTRANT_CALL", "onFocusChange: re-entrant call");
641
+ guards.throwCode("ZRUI_REENTRANT_CALL", "onFocusChange: re-entrant call");
1439
642
  }
1440
643
  return focusDispatcher.register(handler);
1441
644
  },
1442
645
  update(updater) {
1443
- assertOperational("update");
1444
- assertLifecycleIdle("update");
646
+ guards.assertOperational("update");
647
+ guards.assertLifecycleIdle("update");
1445
648
  if (inCommit)
1446
- throwCode("ZRUI_REENTRANT_CALL", "update: called during commit");
1447
- if (inRender)
1448
- throwCode("ZRUI_UPDATE_DURING_RENDER", updateDuringRenderDetail("update"));
649
+ guards.throwCode("ZRUI_REENTRANT_CALL", "update: called during commit");
650
+ if (inRender) {
651
+ guards.throwCode("ZRUI_UPDATE_DURING_RENDER", guards.updateDuringRenderDetail("update"));
652
+ }
1449
653
  updates.enqueue(updater);
1450
654
  if (sm.state === "Running" && inEventHandlerDepth === 0 && !userCommitScheduled) {
1451
655
  userCommitScheduled = true;
1452
- scheduler.enqueue({ kind: "userCommit" });
656
+ enqueueWorkItem({ kind: "userCommit" });
1453
657
  }
1454
658
  },
1455
659
  setTheme(next) {
1456
- assertOperational("setTheme");
1457
- assertLifecycleIdle("setTheme");
660
+ guards.assertOperational("setTheme");
661
+ guards.assertLifecycleIdle("setTheme");
1458
662
  if (inCommit)
1459
- throwCode("ZRUI_REENTRANT_CALL", "setTheme: called during commit");
1460
- if (inRender)
1461
- throwCode("ZRUI_UPDATE_DURING_RENDER", updateDuringRenderDetail("setTheme"));
663
+ guards.throwCode("ZRUI_REENTRANT_CALL", "setTheme: called during commit");
664
+ if (inRender) {
665
+ guards.throwCode("ZRUI_UPDATE_DURING_RENDER", guards.updateDuringRenderDetail("setTheme"));
666
+ }
1462
667
  const nextTheme = compileTheme(next);
1463
668
  if (nextTheme === themeTransition?.to)
1464
669
  return;
@@ -1466,14 +671,14 @@ export function createApp(opts) {
1466
671
  themeTransition = null;
1467
672
  return;
1468
673
  }
1469
- beginThemeTransition(nextTheme);
674
+ renderLoop.beginThemeTransition(nextTheme);
1470
675
  requestViewFromRenderer();
1471
676
  },
1472
677
  debugLayout(enabled) {
1473
- assertOperational("debugLayout");
1474
- assertLifecycleIdle("debugLayout");
678
+ guards.assertOperational("debugLayout");
679
+ guards.assertLifecycleIdle("debugLayout");
1475
680
  if (mode === "raw") {
1476
- throwCode("ZRUI_MODE_CONFLICT", "debugLayout: not available in draw mode");
681
+ guards.throwCode("ZRUI_MODE_CONFLICT", "debugLayout: not available in draw mode");
1477
682
  }
1478
683
  const next = enabled === undefined ? !debugLayoutEnabled : enabled === true;
1479
684
  if (next === debugLayoutEnabled)
@@ -1483,23 +688,25 @@ export function createApp(opts) {
1483
688
  return debugLayoutEnabled;
1484
689
  },
1485
690
  start() {
1486
- assertOperational("start");
1487
- assertNotReentrant("start");
691
+ guards.assertOperational("start");
692
+ guards.assertNotReentrant("start");
1488
693
  sm.assertOneOf(["Created", "Stopped"], "start: must be Created or Stopped");
1489
694
  if (mode === null)
1490
- throwCode("ZRUI_NO_RENDER_MODE", "start: no render mode selected");
695
+ guards.throwCode("ZRUI_NO_RENDER_MODE", "start: no render mode selected");
1491
696
  lifecycleBusy = "start";
1492
697
  const startGeneration = ++lifecycleGeneration;
1493
- let p;
698
+ let promise = null;
1494
699
  try {
1495
- p = backend.start();
700
+ promise = backend.start();
1496
701
  }
1497
- catch (e) {
702
+ catch (error) {
1498
703
  if (lifecycleGeneration === startGeneration)
1499
704
  lifecycleBusy = null;
1500
- throwCode("ZRUI_BACKEND_ERROR", `backend.start threw: ${describeThrown(e)}`);
705
+ guards.throwCode("ZRUI_BACKEND_ERROR", `backend.start threw: ${describeThrown(error)}`);
1501
706
  }
1502
- return p.then(async () => {
707
+ if (promise === null)
708
+ throw new Error("start: backend.start did not return a promise");
709
+ return promise.then(async () => {
1503
710
  try {
1504
711
  backendStarted = true;
1505
712
  if (lifecycleGeneration !== startGeneration) {
@@ -1517,27 +724,27 @@ export function createApp(opts) {
1517
724
  sm.toRunning();
1518
725
  markDirty(DIRTY_VIEW, false);
1519
726
  pollToken++;
1520
- void pollLoop(pollToken);
1521
- scheduler.enqueue({ kind: "kick" });
727
+ void eventLoop.pollLoop(pollToken);
728
+ enqueueWorkItem({ kind: "kick" });
1522
729
  }
1523
730
  finally {
1524
731
  if (lifecycleGeneration === startGeneration && lifecycleBusy === "start") {
1525
732
  lifecycleBusy = null;
1526
733
  }
1527
734
  }
1528
- }, (e) => {
735
+ }, (error) => {
1529
736
  if (lifecycleGeneration !== startGeneration)
1530
737
  return;
1531
738
  lifecycleBusy = null;
1532
- throw new ZrUiError("ZRUI_BACKEND_ERROR", `backend.start rejected: ${describeThrown(e)}`);
739
+ throw new ZrUiError("ZRUI_BACKEND_ERROR", `backend.start rejected: ${describeThrown(error)}`);
1533
740
  });
1534
741
  },
1535
742
  run() {
1536
- assertOperational("run");
1537
- assertNotReentrant("run");
743
+ guards.assertOperational("run");
744
+ guards.assertNotReentrant("run");
1538
745
  sm.assertOneOf(["Created", "Stopped"], "run: must be Created or Stopped");
1539
746
  if (mode === null)
1540
- throwCode("ZRUI_NO_RENDER_MODE", "run: no render mode selected");
747
+ guards.throwCode("ZRUI_NO_RENDER_MODE", "run: no render mode selected");
1541
748
  const proc = readProcessLike();
1542
749
  let runSettle = null;
1543
750
  const runController = createRunSignalController({
@@ -1575,42 +782,40 @@ export function createApp(opts) {
1575
782
  try {
1576
783
  startPromise = app.start();
1577
784
  }
1578
- catch (e) {
785
+ catch (error) {
1579
786
  runController.detach();
1580
- throw e;
787
+ throw error;
1581
788
  }
1582
789
  return startPromise.then(() => {
1583
790
  if (!runController.canRegisterSignals) {
1584
791
  runController.settle();
1585
792
  }
1586
793
  return runController.promise;
1587
- }, (e) => {
794
+ }, (error) => {
1588
795
  runController.detach();
1589
- throw e;
796
+ throw error;
1590
797
  });
1591
798
  },
1592
799
  stop() {
1593
- assertOperational("stop");
1594
- assertNotReentrant("stop");
800
+ guards.assertOperational("stop");
801
+ guards.assertNotReentrant("stop");
1595
802
  sm.assertOneOf(["Running"], "stop: must be Running");
1596
803
  lifecycleBusy = "stop";
1597
804
  const stopGeneration = ++lifecycleGeneration;
1598
- // Stop polling immediately so in-flight pollEvents rejections from backend.stop()
1599
- // are treated as part of shutdown (not a fatal backend error).
1600
805
  pollToken++;
1601
- // Clear any in-flight frames so a shutdown doesn't strand the app in a state
1602
- // where a future start() cannot submit frames.
1603
806
  framesInFlight = 0;
1604
- let p;
807
+ let promise = null;
1605
808
  try {
1606
- p = backend.stop();
809
+ promise = backend.stop();
1607
810
  }
1608
- catch (e) {
811
+ catch (error) {
1609
812
  if (lifecycleGeneration === stopGeneration)
1610
813
  lifecycleBusy = null;
1611
- throwCode("ZRUI_BACKEND_ERROR", `backend.stop threw: ${describeThrown(e)}`);
814
+ guards.throwCode("ZRUI_BACKEND_ERROR", `backend.stop threw: ${describeThrown(error)}`);
1612
815
  }
1613
- return p.then(() => {
816
+ if (promise === null)
817
+ throw new Error("stop: backend.stop did not return a promise");
818
+ return promise.then(() => {
1614
819
  try {
1615
820
  if (lifecycleGeneration !== stopGeneration)
1616
821
  return;
@@ -1624,16 +829,16 @@ export function createApp(opts) {
1624
829
  lifecycleBusy = null;
1625
830
  }
1626
831
  }
1627
- }, (e) => {
832
+ }, (error) => {
1628
833
  if (lifecycleGeneration !== stopGeneration)
1629
834
  return;
1630
835
  lifecycleBusy = null;
1631
- throw new ZrUiError("ZRUI_BACKEND_ERROR", `backend.stop rejected: ${describeThrown(e)}`);
836
+ throw new ZrUiError("ZRUI_BACKEND_ERROR", `backend.stop rejected: ${describeThrown(error)}`);
1632
837
  });
1633
838
  },
1634
839
  dispose() {
1635
840
  if (inCommit || inRender || inEventHandlerDepth > 0) {
1636
- throwCode("ZRUI_REENTRANT_CALL", "dispose: re-entrant call");
841
+ guards.throwCode("ZRUI_REENTRANT_CALL", "dispose: re-entrant call");
1637
842
  }
1638
843
  const st0 = sm.state;
1639
844
  if (st0 === "Disposed")
@@ -1665,22 +870,20 @@ export function createApp(opts) {
1665
870
  }
1666
871
  settleActiveRun?.();
1667
872
  },
1668
- /* --- Keybinding API --- */
1669
873
  keys(bindings) {
1670
- assertKeybindingMutationAllowed("keys");
1671
- assertLifecycleIdle("keys");
1672
- registerAppBindings(bindings);
874
+ guards.assertKeybindingMutationAllowed("keys");
875
+ guards.assertLifecycleIdle("keys");
876
+ keybindingHelpers.registerAppBindings(bindings);
1673
877
  },
1674
878
  modes(modes) {
1675
- assertKeybindingMutationAllowed("modes");
1676
- assertLifecycleIdle("modes");
1677
- registerAppModes(modes);
879
+ guards.assertKeybindingMutationAllowed("modes");
880
+ guards.assertLifecycleIdle("modes");
881
+ keybindingHelpers.registerAppModes(modes);
1678
882
  },
1679
883
  setMode(modeName) {
1680
- assertKeybindingMutationAllowed("setMode");
1681
- assertLifecycleIdle("setMode");
1682
- keybindingState = setMode(keybindingState, modeName);
1683
- keybindingsEnabled = computeKeybindingsEnabled(keybindingState);
884
+ guards.assertKeybindingMutationAllowed("setMode");
885
+ guards.assertLifecycleIdle("setMode");
886
+ applyKeybindingState(setMode(keybindingState, modeName));
1684
887
  },
1685
888
  getMode() {
1686
889
  return getMode(keybindingState);
@@ -1703,8 +906,7 @@ export function createApp(opts) {
1703
906
  };
1704
907
  routeStateUpdater = app.update;
1705
908
  if (routerIntegration) {
1706
- const routeKeybindings = routerIntegration.routeKeybindings;
1707
- replaceRouteBindings(routeKeybindings);
909
+ keybindingHelpers.replaceRouteBindings(routerIntegration.routeKeybindings);
1708
910
  }
1709
911
  Object.defineProperty(app, APP_INTERNAL_REQUEST_VIEW_LAYOUT_MARKER, {
1710
912
  value: () => {
@@ -1718,11 +920,7 @@ export function createApp(opts) {
1718
920
  });
1719
921
  Object.defineProperty(app, APP_INTERNAL_SET_RUNTIME_BREADCRUMB_HOOKS_MARKER, {
1720
922
  value: (hooks) => {
1721
- inspectorInternalOnRender =
1722
- typeof hooks?.onRender === "function" ? hooks.onRender : undefined;
1723
- inspectorInternalOnLayout =
1724
- typeof hooks?.onLayout === "function" ? hooks.onLayout : undefined;
1725
- recomputeRuntimeBreadcrumbCollection();
923
+ runtimeBreadcrumbHelpers.setInspectorHooks(hooks);
1726
924
  },
1727
925
  enumerable: false,
1728
926
  configurable: false,