rotor-framework 0.3.2

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 (39) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +120 -0
  3. package/package.json +59 -0
  4. package/src/source/RotorFramework.bs +654 -0
  5. package/src/source/RotorFrameworkTask.bs +278 -0
  6. package/src/source/base/BaseModel.bs +52 -0
  7. package/src/source/base/BasePlugin.bs +48 -0
  8. package/src/source/base/BaseReducer.bs +184 -0
  9. package/src/source/base/BaseStack.bs +92 -0
  10. package/src/source/base/BaseViewModel.bs +124 -0
  11. package/src/source/base/BaseWidget.bs +104 -0
  12. package/src/source/base/DispatcherCreator.bs +193 -0
  13. package/src/source/base/DispatcherExternal.bs +260 -0
  14. package/src/source/base/ListenerForDispatchers.bs +246 -0
  15. package/src/source/engine/Constants.bs +74 -0
  16. package/src/source/engine/animator/Animator.bs +334 -0
  17. package/src/source/engine/builder/Builder.bs +213 -0
  18. package/src/source/engine/builder/NodePool.bs +236 -0
  19. package/src/source/engine/builder/PluginAdapter.bs +139 -0
  20. package/src/source/engine/builder/PostProcessor.bs +331 -0
  21. package/src/source/engine/builder/Processor.bs +156 -0
  22. package/src/source/engine/builder/Tree.bs +278 -0
  23. package/src/source/engine/builder/TreeBase.bs +313 -0
  24. package/src/source/engine/builder/WidgetCreate.bs +322 -0
  25. package/src/source/engine/builder/WidgetRemove.bs +72 -0
  26. package/src/source/engine/builder/WidgetUpdate.bs +113 -0
  27. package/src/source/engine/providers/Dispatcher.bs +72 -0
  28. package/src/source/engine/providers/DispatcherProvider.bs +95 -0
  29. package/src/source/engine/services/I18n.bs +169 -0
  30. package/src/source/libs/animate/Animate.bs +753 -0
  31. package/src/source/libs/animate/LICENSE.txt +21 -0
  32. package/src/source/plugins/DispatcherProviderPlugin.bs +127 -0
  33. package/src/source/plugins/FieldsPlugin.bs +180 -0
  34. package/src/source/plugins/FocusPlugin.bs +1522 -0
  35. package/src/source/plugins/FontStylePlugin.bs +159 -0
  36. package/src/source/plugins/ObserverPlugin.bs +548 -0
  37. package/src/source/utils/ArrayUtils.bs +495 -0
  38. package/src/source/utils/GeneralUtils.bs +181 -0
  39. package/src/source/utils/NodeUtils.bs +180 -0
@@ -0,0 +1,548 @@
1
+ namespace Rotor
2
+
3
+ ' =====================================================================
4
+ ' ObserverPlugin - Field change observation for roSGNodes
5
+ '
6
+ ' Rotor Framework plugin for observing field changes on roSGNodes.
7
+ ' Manages observer registration, callback routing, and cleanup through
8
+ ' widget lifecycle hooks.
9
+ '
10
+ ' Key Responsibilities:
11
+ ' - Attaches/detaches observers to widget nodes during lifecycle
12
+ ' - Routes native SceneGraph field change callbacks to registered observers
13
+ ' - Manages observer lifecycle (once, until conditions)
14
+ ' - Provides unique attachment IDs for tracking observers per node
15
+ ' =====================================================================
16
+ class ObserverPlugin extends Rotor.BasePlugin
17
+
18
+ ' =============================================================
19
+ ' MEMBER VARIABLES
20
+ ' =============================================================
21
+
22
+ observerStack as object ' ObserverStack instance managing all active observers
23
+ helperInterfaceId as object ' Unique field ID used to store plugin metadata on nodes
24
+
25
+ ' =============================================================
26
+ ' CONSTRUCTOR
27
+ ' =============================================================
28
+
29
+ ' ---------------------------------------------------------------------
30
+ ' new - Initializes the ObserverPlugin instance
31
+ '
32
+ ' @param {string} key - Plugin identifier (default: "observer")
33
+ '
34
+ sub new(key = "observer")
35
+ super(key)
36
+ end sub
37
+
38
+ ' =============================================================
39
+ ' LIFECYCLE HOOKS
40
+ ' =============================================================
41
+
42
+ hooks = {
43
+ ' ---------------------------------------------------------------------
44
+ ' beforeMount - Attaches observers when widget is mounted
45
+ '
46
+ ' Called during widget creation to register all observers defined in the widget config.
47
+ '
48
+ ' @param {object} scope - Plugin instance (m)
49
+ ' @param {object} widget - Widget being mounted
50
+ '
51
+ beforeMount: sub(scope as object, widget as object)
52
+ config = widget[scope.key]
53
+ scope.attach(widget.node, config, widget)
54
+ end sub,
55
+
56
+ ' ---------------------------------------------------------------------
57
+ ' beforeUpdate - Updates observers when widget config changes
58
+ '
59
+ ' Detaches old observers and attaches new ones based on updated configuration.
60
+ '
61
+ ' @param {object} scope - Plugin instance (m)
62
+ ' @param {object} widget - Widget being updated
63
+ ' @param {object} newValue - New observer configuration
64
+ ' @param {object} oldValue - Previous observer configuration
65
+ '
66
+ beforeUpdate: sub(scope as object, widget as object, newValue, oldValue = {})
67
+ if oldValue <> invalid
68
+ scope.detach(widget.node)
69
+ end if
70
+ widget[scope.key] = newValue
71
+ scope.attach(widget.node, newValue, widget)
72
+ end sub,
73
+
74
+ ' ---------------------------------------------------------------------
75
+ ' beforeDestroy - Detaches all observers before widget destruction
76
+ '
77
+ ' Ensures cleanup of all observers when widget is removed from scene graph.
78
+ '
79
+ ' @param {object} scope - Plugin instance (m)
80
+ ' @param {object} widget - Widget being destroyed
81
+ '
82
+ beforeDestroy: sub(scope as object, widget as object)
83
+ scope.detach(widget.node)
84
+ end sub
85
+ }
86
+
87
+ ' =============================================================
88
+ ' INITIALIZATION
89
+ ' =============================================================
90
+
91
+ ' ---------------------------------------------------------------------
92
+ ' init - Initializes plugin internal state
93
+ '
94
+ ' Creates the observer stack and sets up the helper interface ID
95
+ ' used to track observers on nodes.
96
+ '
97
+ sub init()
98
+ m.observerStack = new Rotor.ObserverPluginHelper.ObserverStack()
99
+ m.helperInterfaceId = Rotor.ObserverPluginHelper.OBSERVER_HELPER_INTERFACE + "-" + m.key
100
+ end sub
101
+
102
+ ' =============================================================
103
+ ' OBSERVER MANAGEMENT
104
+ ' =============================================================
105
+
106
+ ' ---------------------------------------------------------------------
107
+ ' attach - Attaches observers to a node
108
+ '
109
+ ' Sets up helper interface on the node (if not already present) and registers
110
+ ' all observers defined in the configuration.
111
+ '
112
+ ' @param {object} node - roSGNode to attach observers to
113
+ ' @param {object} config - Observer configuration (single object or array)
114
+ ' @param {object} listenerScope - Widget instance for callback execution
115
+ '
116
+ sub attach(node as object, config as object, listenerScope as object)
117
+ ' Determine or create attachment ID for this node
118
+ attachmentId = invalid
119
+ if node.hasField(m.helperInterfaceId)
120
+ ' Node already has helper interface - reuse existing attachmentId
121
+ pluginHelperValue = node.getField(m.helperInterfaceId)
122
+ attachmentId = pluginHelperValue.attachmentId
123
+ else
124
+ ' First time attaching to this node - create new attachmentId
125
+ attachmentId = Rotor.Utils.getUUIDHex()
126
+
127
+ if node <> invalid
128
+ ' Add helper interface field to node with plugin metadata
129
+ pluginHelperFields = Rotor.Utils.wrapObject(m.helperInterfaceId, {
130
+ pluginKey: m.key,
131
+ attachmentId: attachmentId
132
+ })
133
+ Rotor.Utils.setCustomFields(node, pluginHelperFields, true, false)
134
+ end if
135
+ end if
136
+
137
+ ' Register each observer configuration
138
+ if config <> invalid and config.Count() > 0 and attachmentId <> invalid
139
+ observerConfigs = Rotor.Utils.ensureArray(config)
140
+ for each observerConfig in observerConfigs
141
+ m.registerObserver(observerConfig, node, attachmentId, m.helperInterfaceId, listenerScope)
142
+ end for
143
+ end if
144
+ end sub
145
+
146
+ ' ---------------------------------------------------------------------
147
+ ' registerObserver - Creates and registers a single observer instance
148
+ '
149
+ ' Creates an Observer object, stores it in the stack, and sets up the native
150
+ ' SceneGraph observeFieldScoped call.
151
+ '
152
+ ' @param {object} observerConfig - Configuration for the specific observer
153
+ ' @param {object} node - roSGNode being observed
154
+ ' @param {string} attachmentId - Unique ID linking observers to this node
155
+ ' @param {string} helperInterfaceId - Helper interface field ID
156
+ ' @param {object} listenerScope - Widget scope for callback execution
157
+ '
158
+ sub registerObserver(observerConfig as object, node as object, attachmentId as string, helperInterfaceId as string, listenerScope as object)
159
+ ' Create observer instance
160
+ newObserver = new Rotor.ObserverPluginHelper.Observer(observerConfig, node, attachmentId, listenerScope, m.key)
161
+ m.observerStack.set(newObserver.id, newObserver)
162
+
163
+ ' Set up native SceneGraph observation
164
+ fieldId = observerConfig.fieldId
165
+ infoFields = newObserver.getInfoFields()
166
+ node.observeFieldScoped(fieldId, "Rotor_ObserverPluginHelper_observerNativeCallback", infoFields)
167
+ end sub
168
+
169
+ ' ---------------------------------------------------------------------
170
+ ' detach - Removes all observers associated with a node
171
+ '
172
+ ' Unobserves all fields and removes observer instances from the stack.
173
+ '
174
+ ' @param {dynamic} node - roSGNode to detach observers from
175
+ '
176
+ sub detach(node as dynamic)
177
+ ' Get plugin helper metadata from node
178
+ pluginHelperValue = node.getField(m.helperInterfaceId)
179
+ if pluginHelperValue = invalid then return
180
+
181
+ attachmentId = pluginHelperValue.attachmentId
182
+ if attachmentId = invalid then return
183
+
184
+ ' Find and remove all observers for this attachment
185
+ observers = m.observerStack.findObserverByAttachmentId(attachmentId)
186
+ for each observer in observers
187
+ if observer.node <> invalid
188
+ observer.node.unobserveFieldScoped(observer.fieldId)
189
+ end if
190
+ m.observerStack.remove(observer.id) ' Calls observer.destroy()
191
+ end for
192
+ end sub
193
+
194
+ ' =============================================================
195
+ ' CALLBACK ROUTING
196
+ ' =============================================================
197
+
198
+ ' ---------------------------------------------------------------------
199
+ ' observerCallbackRouter - Routes native callback to appropriate observers
200
+ '
201
+ ' Called by the global observerNativeCallback when a field changes.
202
+ ' Finds matching observers and triggers their callbacks.
203
+ '
204
+ ' Handles:
205
+ ' - Payload parsing via custom parsePayload functions
206
+ ' - 'once' observers (removed after first trigger)
207
+ ' - 'until' condition observers (removed when condition met)
208
+ '
209
+ ' @param {dynamic} value - New value of the observed field
210
+ ' @param {object} extraInfo - Additional info from observeFieldScoped
211
+ ' @param {string} fieldId - ID of the field that changed
212
+ ' @param {string} attachmentId - Node attachment ID
213
+ ' @param {string} pluginKey - Plugin instance key
214
+ '
215
+ sub observerCallbackRouter(value as dynamic, extraInfo as object, fieldId as string, attachmentId as string, pluginKey as string)
216
+ ' Find all observers interested in this field change
217
+ interestedObservers = m.observerStack.findObserversByAttachmentAndField(attachmentId, fieldId)
218
+
219
+ for each observer in interestedObservers
220
+ ' Build and parse payload
221
+ payload = Rotor.Utils.wrapObject(fieldId, value)
222
+ payload.append(extraInfo)
223
+ parsedPayload = observer.parsePayload(payload)
224
+
225
+ ' Execute observer callback
226
+ observer.notify(parsedPayload)
227
+
228
+ ' Handle observer removal conditions
229
+ if observer.once = true or (Rotor.Utils.isFunction(observer.until) and true = observer.until(parsedPayload))
230
+ ' Unobserve before removing to prevent race conditions
231
+ if observer.node <> invalid
232
+ observer.node.unobserveFieldScoped(observer.fieldId)
233
+ end if
234
+ m.observerStack.remove(observer.id)
235
+ end if
236
+ end for
237
+ end sub
238
+
239
+ ' =============================================================
240
+ ' CLEANUP
241
+ ' =============================================================
242
+
243
+ ' ---------------------------------------------------------------------
244
+ ' destroy - Cleans up all observers
245
+ '
246
+ ' Called when the framework instance is destroyed.
247
+ ' Unobserves all fields and removes all observer instances.
248
+ '
249
+ sub destroy()
250
+ if m.observerStack = invalid then return
251
+
252
+ ' Collect observer IDs to avoid mutation during iteration
253
+ ids = []
254
+ all = m.observerStack.getAll()
255
+ for each id in all
256
+ observer = all[id]
257
+ if observer <> invalid and observer.node <> invalid
258
+ observer.node.unobserveFieldScoped(observer.fieldId)
259
+ end if
260
+ ids.push(id)
261
+ end for
262
+
263
+ ' Remove all observers
264
+ for each id in ids
265
+ m.observerStack.remove(id) ' Calls observer.destroy()
266
+ end for
267
+
268
+ ' Clear the stack
269
+ m.observerStack.clear()
270
+ end sub
271
+
272
+ end class
273
+
274
+ ' =================================================================
275
+ ' OBSERVER PLUGIN HELPER NAMESPACE
276
+ ' =================================================================
277
+
278
+ namespace ObserverPluginHelper
279
+
280
+ ' Prefix for helper field on nodes
281
+ const OBSERVER_HELPER_INTERFACE = "rotorObserverPluginKeysHelper"
282
+
283
+ ' ---------------------------------------------------------------------
284
+ ' observerNativeCallback - Global callback for SceneGraph field changes
285
+ '
286
+ ' This function is registered with node.observeFieldScoped() and is called
287
+ ' whenever an observed field changes. It extracts metadata, identifies
288
+ ' the appropriate plugin instance, and routes to observerCallbackRouter.
289
+ '
290
+ ' @param {object} msg - roSGNodeEvent from the observed node
291
+ '
292
+ sub observerNativeCallback(msg as object)
293
+ ' Extract event data
294
+ extraInfo = msg.GetInfo()
295
+ fieldId = msg.getField()
296
+ value = msg.getData()
297
+
298
+ ' Extract plugin metadata from helper field
299
+ pluginKey = ""
300
+ attachmentId = ""
301
+ for each key in extraInfo
302
+ if Left(key, Len(OBSERVER_HELPER_INTERFACE)) = OBSERVER_HELPER_INTERFACE
303
+ helperValue = extraInfo[key]
304
+ if helperValue <> invalid
305
+ pluginKey = helperValue.pluginKey
306
+ attachmentId = helperValue.attachmentId
307
+ end if
308
+ extraInfo.delete(key) ' Remove helper from payload
309
+ exit for
310
+ end if
311
+ end for
312
+
313
+ ' Route to appropriate plugin instance
314
+ if attachmentId <> "" and pluginKey <> ""
315
+ globalScope = GetGlobalAA()
316
+ frameworkInstance = globalScope.rotor_framework_helper.frameworkInstance
317
+ plugin = invalid
318
+
319
+ ' Handle special case for Rotor Animator observers
320
+ if extraInfo?.isRotorAnimatorNode = true
321
+ plugin = frameworkInstance.animatorProvider.animatorObservber
322
+ else
323
+ plugin = frameworkInstance.plugins[pluginKey]
324
+ end if
325
+
326
+ ' Execute callback router
327
+ if plugin <> invalid
328
+ plugin.observerCallbackRouter(value, extraInfo, fieldId, attachmentId, pluginKey)
329
+ end if
330
+ end if
331
+ end sub
332
+
333
+ ' =====================================================================
334
+ ' ObserverStack - Collection manager for Observer instances
335
+ '
336
+ ' Manages a collection of Observer instances with specialized lookup methods.
337
+ '
338
+ ' Provides:
339
+ ' - Storage and retrieval of observers by ID
340
+ ' - Lookup by attachment ID and field ID
341
+ ' - Automatic cleanup on removal
342
+ ' =====================================================================
343
+ class ObserverStack extends Rotor.BaseStack
344
+
345
+ ' ---------------------------------------------------------------------
346
+ ' remove - Removes observer and triggers cleanup
347
+ '
348
+ ' Overrides base class to call observer.destroy() before removal.
349
+ '
350
+ ' @param {string} id - Observer ID to remove
351
+ '
352
+ override sub remove(id as string)
353
+ item = m.get(id)
354
+ if item <> invalid
355
+ item.destroy()
356
+ end if
357
+ super.remove(id)
358
+ end sub
359
+
360
+ ' ---------------------------------------------------------------------
361
+ ' findObserversByAttachmentAndField - Finds observers by attachment and field
362
+ '
363
+ ' Returns all observers matching both the attachment ID and field ID.
364
+ '
365
+ ' @param {string} attachmentId - Node attachment ID
366
+ ' @param {string} fieldId - Field ID
367
+ ' @returns {object} Array of matching Observer instances
368
+ '
369
+ function findObserversByAttachmentAndField(attachmentId as string, fieldId as string) as object
370
+ observers = []
371
+ for each id in m.stack
372
+ observer = m.stack[id]
373
+ if observer.fieldId = fieldId and observer.attachmentId = attachmentId
374
+ observers.push(observer)
375
+ end if
376
+ end for
377
+ return observers
378
+ end function
379
+
380
+ ' ---------------------------------------------------------------------
381
+ ' findObserverByAttachmentId - Finds all observers for an attachment
382
+ '
383
+ ' Returns all observers associated with a specific node attachment.
384
+ '
385
+ ' @param {string} attachmentId - Node attachment ID
386
+ ' @returns {object} Array of matching Observer instances
387
+ '
388
+ function findObserverByAttachmentId(attachmentId as string) as object
389
+ observers = []
390
+ for each id in m.stack
391
+ observer = m.stack[id]
392
+ if observer.attachmentId = attachmentId
393
+ observers.push(observer)
394
+ end if
395
+ end for
396
+ return observers
397
+ end function
398
+
399
+ end class
400
+
401
+ ' =====================================================================
402
+ ' Observer - Single observer configuration for a node field
403
+ '
404
+ ' Represents a single observer configuration for a node field.
405
+ '
406
+ ' Responsibilities:
407
+ ' - Stores observer configuration (callback, conditions, etc.)
408
+ ' - Sets up initial field value if provided
409
+ ' - Provides info fields for observeFieldScoped
410
+ ' - Executes callbacks in correct scope
411
+ ' - Manages cleanup of references
412
+ ' =====================================================================
413
+ class Observer
414
+
415
+ ' =============================================================
416
+ ' MEMBER VARIABLES
417
+ ' =============================================================
418
+
419
+ id as string ' Unique observer ID
420
+ node as object ' roSGNode being observed
421
+ pluginKey as string ' Key of managing ObserverPlugin
422
+ listenerScope as object ' Widget scope for callback execution
423
+ attachmentId as string ' Node attachment ID
424
+ fieldId as string ' Field name being observed
425
+ infoFields as object ' Additional fields to include in callback info
426
+ value as dynamic ' Initial field value (if any)
427
+ once as boolean ' Remove observer after first trigger
428
+ until as function ' Conditional removal function
429
+ callback as function ' Callback function to execute
430
+ parsePayload as function ' Payload transformation function
431
+ alwaysNotify as boolean ' Field alwaysNotify flag
432
+
433
+ ' =============================================================
434
+ ' CONSTRUCTOR
435
+ ' =============================================================
436
+
437
+ ' ---------------------------------------------------------------------
438
+ ' new - Creates an Observer instance
439
+ '
440
+ ' @param {object} config - Observer configuration
441
+ ' @param {object} node - roSGNode to observe
442
+ ' @param {string} attachmentId - Node attachment ID
443
+ ' @param {object} listenerScope - Widget scope for callbacks
444
+ ' @param {string} pluginKey - Managing plugin key
445
+ '
446
+ sub new(config as object, node as object, attachmentId as string, listenerScope as object, pluginKey as string)
447
+ ' Generate unique ID
448
+ m.id = (config.id ?? "ID") + "-" + Rotor.Utils.getUUIDHex()
449
+
450
+ ' Store references
451
+ m.node = node
452
+ m.pluginKey = pluginKey
453
+ m.listenerScope = listenerScope ?? {}
454
+ m.attachmentId = attachmentId
455
+
456
+ ' Extract configuration
457
+ m.fieldId = config?.fieldId ?? ""
458
+ m.infoFields = config?.infoFields ?? []
459
+ m.value = config?.value
460
+ m.alwaysNotify = config?.alwaysNotify ?? true
461
+ m.once = config?.once ?? false
462
+ m.until = config?.until
463
+
464
+ ' Set callback (required)
465
+ m.callback = config?.callback ?? sub() throw "Callback has not configured for observer"
466
+ end Sub
467
+
468
+ ' Set payload parser (optional)
469
+ m.parsePayload = config?.parsePayload ?? function(payload)
470
+ return payload
471
+ end function
472
+
473
+ ' Set up initial field value if provided
474
+ m.setupField(m.fieldId, m.value, m.alwaysNotify)
475
+ end sub
476
+
477
+ ' =============================================================
478
+ ' FIELD SETUP
479
+ ' =============================================================
480
+
481
+ ' ---------------------------------------------------------------------
482
+ ' setupField - Sets initial value on observed field
483
+ '
484
+ ' Creates or updates the field on the node if an initial value is provided.
485
+ '
486
+ ' @param {string} fieldId - Field name
487
+ ' @param {dynamic} value - Initial value (if not invalid)
488
+ ' @param {boolean} alwaysNotify - alwaysNotify flag value
489
+ '
490
+ sub setupField(fieldId as string, value as dynamic, alwaysNotify as boolean)
491
+ fields = {}
492
+ fields[m.fieldId] = value
493
+ Rotor.Utils.setCustomFields(m.node, fields, m.value <> invalid, alwaysNotify)
494
+ end sub
495
+
496
+ ' =============================================================
497
+ ' INFO FIELDS
498
+ ' =============================================================
499
+
500
+ ' ---------------------------------------------------------------------
501
+ ' getInfoFields - Builds field list for observeFieldScoped
502
+ '
503
+ ' Combines user-defined infoFields with the helper interface ID.
504
+ '
505
+ ' @returns {object} Array of field names for observeFieldScoped
506
+ '
507
+ function getInfoFields() as object
508
+ helperInterfaceId = Rotor.ObserverPluginHelper.OBSERVER_HELPER_INTERFACE + "-" + m.pluginKey
509
+ infoFields = []
510
+ infoFields.append(m.infoFields)
511
+ infoFields.push(helperInterfaceId)
512
+ return infoFields
513
+ end function
514
+
515
+ ' =============================================================
516
+ ' CALLBACK EXECUTION
517
+ ' =============================================================
518
+
519
+ ' ---------------------------------------------------------------------
520
+ ' notify - Executes observer callback
521
+ '
522
+ ' Calls the configured callback function in the correct scope with the payload.
523
+ '
524
+ ' @param {dynamic} payload - Data to pass to callback
525
+ '
526
+ sub notify(payload as dynamic)
527
+ Rotor.Utils.callbackScoped(m.callback, m.listenerScope, payload)
528
+ end sub
529
+
530
+ ' =============================================================
531
+ ' CLEANUP
532
+ ' =============================================================
533
+
534
+ ' ---------------------------------------------------------------------
535
+ ' destroy - Cleans up observer references
536
+ '
537
+ ' Clears references to prevent memory leaks.
538
+ '
539
+ sub destroy()
540
+ m.node = invalid
541
+ m.listenerScope = invalid
542
+ end sub
543
+
544
+ end class
545
+
546
+ end namespace ' ObserverPluginHelper
547
+
548
+ end namespace ' Rotor