rotor-framework 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,7 +96,7 @@ You can find [🌱](./docs/ai/readme.opt.yaml) symbols in all documentation page
96
96
 
97
97
  ## 📚 Learn More
98
98
 
99
- ![Version](https://img.shields.io/badge/version-v0.7.2-blue?label=Documents%20TAG)
99
+ ![Version](https://img.shields.io/badge/version-v0.7.3-blue?label=Documents%20TAG)
100
100
 
101
101
  ### Framework Core
102
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rotor-framework",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Roku toolkit library providing a ViewBuilder, full UI lifecycle with focus handling and many core features, plus MVI-based state management.",
5
5
  "author": "Balázs Molnár",
6
6
  "license": "Apache-2.0",
@@ -4,7 +4,7 @@
4
4
  ' ▐▛▀▚▖▐▌ ▐▌ █ ▐▌ ▐▌▐▛▀▚▖ ▐▛▀▀▘▐▛▀▚▖▐▛▀▜▌▐▌ ▐▌▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▛▚▖
5
5
  ' ▐▌ ▐▌▝▚▄▞▘ █ ▝▚▄▞▘▐▌ ▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌
6
6
  ' Rotor Framework™
7
- ' Version 0.7.2
7
+ ' Version 0.7.3
8
8
  ' © 2025 Balázs Molnár — Apache License 2.0
9
9
  ' =========================================================================
10
10
 
@@ -21,8 +21,8 @@ import "engine/animator/Animator.bs"
21
21
 
22
22
  ' base classes
23
23
  import "base/BaseWidget.bs"
24
- import "base/DispatcherCreator.bs"
25
- import "base/DispatcherExternal.bs"
24
+ import "base/DispatcherOriginal.bs"
25
+ import "base/DispatcherCrossThread.bs"
26
26
  import "base/BaseReducer.bs"
27
27
  import "base/BaseModel.bs"
28
28
  import "base/BaseStack.bs"
@@ -86,7 +86,7 @@ namespace Rotor
86
86
  class Framework
87
87
 
88
88
  name = "Rotor Framework"
89
- version = "0.7.2"
89
+ version = "0.7.3"
90
90
 
91
91
  config = {
92
92
  tasks: invalid, ' @array (optional)
@@ -121,10 +121,8 @@ namespace Rotor
121
121
  ' plugin adapter workspace
122
122
  plugins = {}
123
123
 
124
- ' sync
125
- taskOperationalFlag = {}
126
- taskSyncReadyFlag = {}
127
- taskNodes = {}
124
+ ' sync - task tracking with phase: pending → operational → synced
125
+ tasks = {} ' taskId -> { node, phase, dispatchers[] }
128
126
 
129
127
  enableRendering = true
130
128
 
@@ -360,15 +358,17 @@ namespace Rotor
360
358
  taskNode = rootNode.createChild(taskName)
361
359
  taskId = Rotor.Utils.getUUIDHex(16)
362
360
 
363
- m.taskNodes[taskId] = taskNode ' collection for later usage
364
- m.taskOperationalFlag[taskId] = false ' collection for later usage
361
+ m.tasks[taskId] = {
362
+ node: taskNode,
363
+ phase: "pending",
364
+ dispatchers: []
365
+ }
365
366
 
366
367
  Rotor.Utils.setCustomFields(taskNode, {
367
368
  taskId: taskId,
368
369
  rootNode: rootNode
369
370
  })
370
371
 
371
- ' taskNode.observeFieldScoped("rotorSync", "Rotor_syncCallback")
372
372
  taskNode.control = "RUN"
373
373
  end sub
374
374
 
@@ -378,136 +378,108 @@ namespace Rotor
378
378
  ' @param {object} taskList - Array of task names to set up
379
379
  '
380
380
  sub setupAdditionalTasks(taskList as object)
381
- ' check additional tasks
382
381
  if taskList = invalid then return
383
-
384
- taskNames = Rotor.Utils.ensureArray(taskList)
385
- for each taskName in taskNames
382
+ for each taskName in Rotor.Utils.ensureArray(taskList)
386
383
  m.setupTaskForSyncing(taskName)
387
384
  end for
388
385
  end sub
389
386
 
390
387
  ' ---------------------------------------------------------------------
391
- ' areAllTasksOperational - Checks if all task threads are operational
388
+ ' allTasksInPhase - Checks if all tasks are in given phase
392
389
  '
393
- ' @returns {boolean} True if all tasks have signaled operational status
390
+ ' @param {string} phase - Phase to check ("pending", "operational", "synced")
391
+ ' @returns {boolean} True if all tasks are in the given phase
394
392
  '
395
- function areAllTasksOperational() as boolean
396
- ' Check if all nodes ready (very basic logic (< future improvement)
397
- if m.taskOperationalFlag.Count() = 0 then return false
398
-
399
- for each flag in m.taskOperationalFlag.Items()
400
- if flag.value <> true then return false
393
+ function allTasksInPhase(phase as string) as boolean
394
+ if m.tasks.Count() = 0 then return false
395
+ for each taskId in m.tasks
396
+ if m.tasks[taskId].phase <> phase then return false
401
397
  end for
402
-
403
398
  return true
404
399
  end function
405
400
 
406
401
  ' ---------------------------------------------------------------------
407
- ' collectExternalDispatcherRegistrations - Collects external dispatcher info for tasks
408
- '
409
- ' Creates a map of which external dispatchers need to be registered in which tasks.
410
- '
411
- ' @returns {object} AA mapping taskId to array of dispatcher registration configs
412
- '
413
- function collectExternalDispatcherRegistrations() as object
414
- registrations = {}
415
- dispatcherIds = m.dispatcherProvider.getAll().keys()
416
-
417
- for each taskEntry in m.taskNodes.Items()
418
- taskNode = taskEntry.value
419
-
420
- for each dispatcherId in dispatcherIds
421
- dispatcherInstance = m.dispatcherProvider.get(dispatcherId)
422
- externalTaskNode = dispatcherInstance.taskNode
423
-
424
- if not taskNode.isSameNode(externalTaskNode)
425
- m.taskSyncReadyFlag[taskNode.taskId] = false ' collection for later usage
426
- if registrations[taskNode.taskId] = invalid then registrations[taskNode.taskId] = []
427
- registrations[taskNode.taskId].push({
428
- dispatcherId: dispatcherId,
429
- externalTaskNode: externalTaskNode
430
- })
431
- end if
432
- end for
402
+ ' getDispatchersNotOnTask - Gets dispatchers that are NOT on given task
403
+ '
404
+ ' @param {object} taskNode - Task node to exclude
405
+ ' @returns {object} Array of { dispatcherId, stateNode } for foreign dispatchers
406
+ '
407
+ function getDispatchersNotOnTask(taskNode as object) as object
408
+ result = []
409
+ for each dispatcherId in m.dispatcherProvider.getAll().keys()
410
+ stateNode = m.dispatcherProvider.get(dispatcherId).stateNode
411
+ if not taskNode.isSameNode(stateNode)
412
+ result.push({ dispatcherId: dispatcherId, stateNode: stateNode })
413
+ end if
433
414
  end for
434
-
435
- return registrations
415
+ return result
436
416
  end function
437
417
 
438
418
  ' ---------------------------------------------------------------------
439
- ' dispatchExternalDispatcherRegistrations - Sends dispatcher registration to tasks
440
- '
441
- ' @param {object} registrations - Map of taskId to dispatcher registration configs
442
- '
443
- sub dispatchExternalDispatcherRegistrations(registrations as object)
444
- for each taskId in registrations
445
- taskNode = m.taskNodes[taskId]
446
- taskNode.setField("rotorSync", {
447
- type: Rotor.Const.ThreadSyncType.REGISTER_EXTERNAL_DISPATCHER,
448
- externalDispatcherList: registrations[taskId]
449
- })
419
+ ' broadcastDispatchersToTasks - Sends foreign dispatchers to each task
420
+ '
421
+ ' For each task, sends list of dispatchers that exist on other threads.
422
+ '
423
+ sub broadcastDispatchersToTasks()
424
+ for each taskId in m.tasks
425
+ task = m.tasks[taskId]
426
+ foreignDispatchers = m.getDispatchersNotOnTask(task.node)
427
+
428
+ if foreignDispatchers.Count() > 0
429
+ task.node.setField("rotorSync", {
430
+ type: Rotor.Const.ThreadSyncType.REGISTER_CROSS_THREAD_DISPATCHER,
431
+ crossThreadDispatcherList: foreignDispatchers
432
+ })
433
+ else
434
+ task.phase = "synced"
435
+ end if
450
436
  end for
451
- end sub
452
437
 
453
- ' ---------------------------------------------------------------------
454
- ' haveAllTasksSynced - Checks if all tasks have completed syncing
455
- '
456
- ' @returns {boolean} True if all tasks have synced successfully
457
- '
458
- function haveAllTasksSynced() as boolean
459
- ' Check if all nodes ready (very basic logic (< future improvement)
460
- if m.taskSyncReadyFlag.Count() = 0 then return false
461
-
462
- for each flag in m.taskSyncReadyFlag.Items()
463
- if flag.value <> true then return false
464
- end for
465
-
466
- return true
467
- end function
438
+ ' If no tasks needed dispatchers, we're done
439
+ if m.allTasksInPhase("synced")
440
+ m.isReady()
441
+ end if
442
+ end sub
468
443
 
469
444
  ' ---------------------------------------------------------------------
470
445
  ' handleTaskSyncing - Handles task syncing phase
471
446
  '
472
- ' Registers external dispatchers and sets up cross-thread communication.
447
+ ' Called when a task reports its dispatchers. Registers cross-thread
448
+ ' dispatchers and broadcasts dispatcher map when all tasks are ready.
473
449
  '
474
- ' @param {object} sync - Sync payload from task thread
450
+ ' @param {object} sync - Sync payload { taskNode, dispatcherIds, tasks }
475
451
  '
476
452
  sub handleTaskSyncing(sync as object)
477
453
  taskNode = sync.taskNode
454
+ taskId = taskNode.taskId
478
455
 
456
+ ' Setup any additional tasks this task declared
479
457
  m.setupAdditionalTasks(sync.tasks)
480
458
 
481
- ' register incoming dispatchers
482
- dispatcherIds = sync.dispatcherIds
483
- if dispatcherIds <> invalid
484
- m.dispatcherProvider.registerExternalDispatchers(dispatcherIds, taskNode)
485
- end if
459
+ ' Store dispatcher IDs for this task
460
+ m.tasks[taskId].dispatchers = sync.dispatcherIds ?? []
461
+ m.tasks[taskId].phase = "operational"
486
462
 
487
- ' update task status
488
- m.taskOperationalFlag[taskNode.taskId] = true
463
+ ' Register cross-thread dispatchers on render thread
464
+ if sync.dispatcherIds <> invalid
465
+ m.dispatcherProvider.registerCrossThreadDispatchers(sync.dispatcherIds, taskNode)
466
+ end if
489
467
 
490
- if not m.areAllTasksOperational() then return
468
+ ' Wait for all tasks to be operational
469
+ if not m.allTasksInPhase("operational") then return
491
470
 
492
- ' if allTasksRunning then create external dispatchers in all tasks
493
- registrations = m.collectExternalDispatcherRegistrations()
494
- if registrations.Count() > 0
495
- m.dispatchExternalDispatcherRegistrations(registrations)
496
- else
497
- m.isReady()
498
- end if
471
+ ' All tasks ready - broadcast dispatcher map
472
+ m.broadcastDispatchersToTasks()
499
473
  end sub
500
474
 
501
475
  ' ---------------------------------------------------------------------
502
476
  ' handleTaskSynced - Handles task synced confirmation
503
477
  '
504
- ' Marks task as synced and checks if all tasks are ready.
505
- '
506
478
  ' @param {string} taskId - ID of the task that has synced
507
479
  '
508
480
  sub handleTaskSynced(taskId as string)
509
- m.taskSyncReadyFlag[taskId] = true
510
- if m.haveAllTasksSynced()
481
+ m.tasks[taskId].phase = "synced"
482
+ if m.allTasksInPhase("synced")
511
483
  m.isReady()
512
484
  end if
513
485
  end sub
@@ -549,15 +521,15 @@ namespace Rotor
549
521
  public sub destroy()
550
522
 
551
523
  rootNode = m.getRootNode()
552
- for each taskId in m.taskNodes
553
- taskNode = m.taskNodes[taskId]
554
- taskNode.setField("rotorSync", {
524
+ for each taskId in m.tasks
525
+ task = m.tasks[taskId]
526
+ task.node.setField("rotorSync", {
555
527
  type: Rotor.Const.ThreadSyncType.DESTROY
556
528
  })
557
- rootNode.removeChild(taskNode)
529
+ rootNode.removeChild(task.node)
558
530
  end for
559
531
 
560
- m.taskNodes.Clear()
532
+ m.tasks.Clear()
561
533
 
562
534
  rootNode.unobserveFieldScoped("rotorSync")
563
535
 
@@ -4,7 +4,7 @@
4
4
  ' ▐▛▀▚▖▐▌ ▐▌ █ ▐▌ ▐▌▐▛▀▚▖ ▐▛▀▀▘▐▛▀▚▖▐▛▀▜▌▐▌ ▐▌▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▛▚▖
5
5
  ' ▐▌ ▐▌▝▚▄▞▘ █ ▝▚▄▞▘▐▌ ▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌
6
6
  ' Rotor Framework™
7
- ' Version 0.7.2
7
+ ' Version 0.7.3
8
8
  ' © 2025 Balázs Molnár — Apache License 2.0
9
9
  ' =========================================================================
10
10
 
@@ -16,8 +16,8 @@ import "engine/providers/DispatcherProvider.bs"
16
16
  import "engine/providers/Dispatcher.bs"
17
17
 
18
18
  ' base classes
19
- import "base/DispatcherCreator.bs"
20
- import "base/DispatcherExternal.bs"
19
+ import "base/DispatcherOriginal.bs"
20
+ import "base/DispatcherCrossThread.bs"
21
21
  import "base/BaseReducer.bs"
22
22
  import "base/BaseModel.bs"
23
23
  import "base/BaseStack.bs"
@@ -70,7 +70,7 @@ namespace Rotor
70
70
  class FrameworkTask
71
71
 
72
72
  name = "Rotor Framework"
73
- version = "0.7.2"
73
+ version = "0.7.3"
74
74
 
75
75
  config = {
76
76
  tasks: invalid, ' optional
@@ -87,6 +87,7 @@ namespace Rotor
87
87
  dispatcherProvider as object
88
88
  port as object
89
89
  asyncTransferRegistry = {} ' transferId -> { dispatcherId, context }
90
+ deviceInfoRegistry = {} ' deviceInfoId -> { dispatcherId, context, deviceInfo }
90
91
  onTick as function
91
92
 
92
93
  ' ---------------------------------------------------------------------
@@ -148,6 +149,27 @@ namespace Rotor
148
149
  }
149
150
  end sub
150
151
 
152
+ ' ---------------------------------------------------------------------
153
+ ' registerDeviceInfo - Registers a roDeviceInfo for event listening
154
+ '
155
+ ' This method associates a roDeviceInfo object with a dispatcher ID.
156
+ ' When device info events occur (linkStatus, etc.), the framework will
157
+ ' route them to the correct dispatcher's asyncReducerCallback.
158
+ '
159
+ ' @param {string} deviceInfoId - Unique identifier for this registration
160
+ ' @param {string} dispatcherId - Dispatcher ID that will handle events
161
+ ' @param {object} deviceInfo - The roDeviceInfo object (must have SetMessagePort called)
162
+ ' @param {object} context - Optional user data to pass back in callback
163
+ '
164
+ public sub registerDeviceInfo(deviceInfoId as string, dispatcherId as string, deviceInfo as object, context = invalid as dynamic)
165
+ deviceInfo.SetMessagePort(m.port)
166
+ m.deviceInfoRegistry[deviceInfoId] = {
167
+ dispatcherId: dispatcherId,
168
+ deviceInfo: deviceInfo,
169
+ context: context
170
+ }
171
+ end sub
172
+
151
173
  ' ---------------------------------------------------------------------
152
174
  ' sync - Starts the message loop for cross-thread communication
153
175
  '
@@ -205,10 +227,10 @@ namespace Rotor
205
227
  ' taskIntent = Rotor.Utils.deepCopy(intent)
206
228
  dispatcherInstance.dispatch(intent)
207
229
 
208
- else if sync.type = Rotor.Const.ThreadSyncType.REGISTER_EXTERNAL_DISPATCHER
230
+ else if sync.type = Rotor.Const.ThreadSyncType.REGISTER_CROSS_THREAD_DISPATCHER
209
231
 
210
- for each item in sync.externalDispatcherList
211
- m.dispatcherProvider.registerExternalDispatchers(item.dispatcherId, item.externalTaskNode)
232
+ for each item in sync.crossThreadDispatcherList
233
+ m.dispatcherProvider.registerCrossThreadDispatchers(item.dispatcherId, item.stateNode)
212
234
  end for
213
235
 
214
236
  m.notifySyncStatus(Rotor.Const.ThreadSyncType.TASK_SYNCED)
@@ -251,6 +273,22 @@ namespace Rotor
251
273
  end if
252
274
 
253
275
  end if
276
+ else if msgType = "roDeviceInfoEvent"
277
+ ' Handle device info events (ignore appFocused, etc.)
278
+ if m.deviceInfoRegistry.count() > 0
279
+ eventInfo = msg.GetInfo()
280
+ ' Only process linkStatus events, ignore appFocused and other events
281
+ if eventInfo?.linkStatus <> invalid
282
+ ' Route to all registered device info dispatchers
283
+ for each deviceInfoId in m.deviceInfoRegistry
284
+ registryEntry = m.deviceInfoRegistry[deviceInfoId]
285
+ dispatcherInstance = m.dispatcherProvider.get(registryEntry.dispatcherId)
286
+ if dispatcherInstance <> invalid
287
+ dispatcherInstance.asyncReducerCallback(eventInfo, registryEntry?.context as dynamic)
288
+ end if
289
+ end for
290
+ end if
291
+ end if
254
292
  end if
255
293
  end if
256
294
  end while
@@ -317,6 +355,7 @@ namespace Rotor
317
355
  '
318
356
  public sub destroy()
319
357
  m.asyncTransferRegistry.clear()
358
+ m.deviceInfoRegistry.clear()
320
359
  m.dispatcherProvider.destroy()
321
360
 
322
361
  globalScope = GetGlobalAA()
@@ -87,22 +87,24 @@ namespace Rotor
87
87
  ' These are optionally injected by plugins
88
88
 
89
89
  ' Focus Plugin Methods (from FocusPlugin.bs)
90
- optional enableFocusNavigation as function ' Enable focus navigation
90
+ optional enableFocusNavigation as function ' Enable focus navigation
91
91
  optional isFocusNavigationEnabled as function ' Check if focus enabled
92
- optional setFocus as function ' Set focus on widget
93
- optional getFocusedWidget as function ' Get currently focused widget
94
- optional proceedLongPress as function ' Trigger long press
95
- optional isLongPressActive as function ' Check if long press active
92
+ optional setFocus as function ' Set focus on widget
93
+ optional getFocusedWidget as function ' Get currently focused widget
94
+ optional proceedLongPress as function ' Trigger long press
95
+ optional isLongPressActive as function ' Check if long press active
96
+ optional triggerKeyPress as function ' Trigger key press event
97
+ optional setGroupLastFocusedId as function ' Set the last focused widget ID within the current focus group (used to restore focus when re-entering the group)
96
98
 
97
99
  ' =============================================================
98
100
  ' PRIVATE PROPERTIES (Engine Use Only)
99
101
  ' =============================================================
100
102
 
101
- private HID as string ' Hierarchical ID (e.g., "0.header.logo")
102
- private vmHID as string ' Owning ViewModel's HID
103
- private isRootChild as boolean ' True if direct child of root
103
+ private HID as string ' Hierarchical ID (e.g., "0.header.logo")
104
+ private vmHID as string ' Owning ViewModel's HID
105
+ private isRootChild as boolean ' True if direct child of root
104
106
  private childrenHIDhash as object ' Hash of child HIDs for fast lookup
105
- private parentHID as string ' Parent's HID
107
+ private parentHID as string ' Parent's HID
106
108
 
107
109
  end class
108
110
 
@@ -16,10 +16,15 @@ end interface
16
16
  namespace Rotor
17
17
 
18
18
  ' =====================================================================
19
- ' ListenerForDispatchers - Base class for dispatcher state change listener management
19
+ ' DispatcherCore - Core base class for dispatcher implementations
20
20
  '
21
- ' Provides listener registration, notification, and lifecycle management
22
- ' for both internal and external dispatchers.
21
+ ' Provides shared properties and listener management for both
22
+ ' DispatcherOriginal and DispatcherCrossThread.
23
+ '
24
+ ' Shared Properties:
25
+ ' - dispatcherId: Unique identifier for this dispatcher
26
+ ' - stateNode: SceneGraph node for state exposure
27
+ ' - listeners: Array of registered state change listeners
23
28
  '
24
29
  ' Listener Features:
25
30
  ' - State mapping: mapStateToProps for updating widget props
@@ -33,12 +38,15 @@ namespace Rotor
33
38
  ' 3. Invoke callback functions
34
39
  ' 4. Remove 'once' listeners
35
40
  ' =====================================================================
36
- class ListenerForDispatchers
41
+ class DispatcherCore
37
42
 
38
43
  ' =============================================================
39
44
  ' MEMBER VARIABLES
40
45
  ' =============================================================
41
46
 
47
+ dispatcherId as string ' Dispatcher identifier
48
+ stateNode as object ' Node for cross-thread state exposure
49
+
42
50
  listeners = [] ' Array of listener configurations
43
51
 
44
52
  ' =============================================================
@@ -1,9 +1,9 @@
1
- import "ListenerForDispatchers.bs"
1
+ import "DispatcherCore.bs"
2
2
 
3
3
  namespace Rotor
4
4
 
5
5
  ' =====================================================================
6
- ' DispatcherExternal - External dispatcher for cross-thread MVI state management
6
+ ' DispatcherCrossThread - Cross-thread dispatcher for MVI state management
7
7
  '
8
8
  ' This dispatcher is used when accessing dispatchers from a different thread
9
9
  ' than where they were created (render thread accessing task thread dispatcher
@@ -11,25 +11,22 @@ namespace Rotor
11
11
  '
12
12
  ' Responsibilities:
13
13
  ' - Dispatches intents across thread boundaries via rotorSync field
14
- ' - Observes state changes on task node field
14
+ ' - Observes state changes on state node field
15
15
  ' - Manages cross-thread listener notifications
16
16
  ' - Lazy observer setup (only when listeners are added)
17
17
  ' - Auto cleanup when last listener is removed
18
18
  '
19
19
  ' MVI Cross-Thread Flow:
20
20
  ' Render Thread: dispatch(intent) → rotorSync field → Task Thread
21
- ' Task Thread: reduce state → task node field → Render Thread observer callback
21
+ ' Task Thread: reduce state → state node field → Render Thread observer callback
22
22
  ' =====================================================================
23
- class DispatcherExternal extends ListenerForDispatchers
23
+ class DispatcherCrossThread extends DispatcherCore
24
24
 
25
25
  ' =============================================================
26
26
  ' MEMBER VARIABLES
27
27
  ' =============================================================
28
28
 
29
- dispatcherId as string ' Dispatcher identifier
30
- taskNode as object ' Task node for cross-thread communication
31
29
  threadType as string ' Current thread type (RENDER or TASK)
32
- isExternal = true ' True for external dispatchers
33
30
  isStateObserved = false ' Tracks if observer is active
34
31
 
35
32
  ' =============================================================
@@ -37,18 +34,18 @@ namespace Rotor
37
34
  ' =============================================================
38
35
 
39
36
  ' ---------------------------------------------------------------------
40
- ' Constructor - Initializes external dispatcher
37
+ ' Constructor - Initializes cross-thread dispatcher
41
38
  '
42
39
  ' @param {string} dispatcherId - Dispatcher identifier
43
- ' @param {object} taskNode - Task node for cross-thread communication
40
+ ' @param {object} stateNode - Node for cross-thread state communication
44
41
  ' @param {string} threadType - Current thread type (RENDER or TASK)
45
42
  '
46
- sub new(dispatcherId as string, taskNode as object, threadType as string)
43
+ sub new(dispatcherId as string, stateNode as object, threadType as string)
47
44
  super()
48
45
 
49
46
  ' Store dispatcher configuration
50
47
  m.dispatcherId = dispatcherId
51
- m.taskNode = taskNode
48
+ m.stateNode = stateNode
52
49
  m.threadType = threadType
53
50
  end sub
54
51
 
@@ -73,7 +70,7 @@ namespace Rotor
73
70
  end if
74
71
 
75
72
  ' Send intent to task thread via rotorSync field
76
- m.taskNode.setField("rotorSync", {
73
+ m.stateNode.setField("rotorSync", {
77
74
  type: Rotor.Const.ThreadSyncType.DISPATCH,
78
75
  payload: {
79
76
  dispatcherId: m.dispatcherId,
@@ -87,17 +84,17 @@ namespace Rotor
87
84
  ' =============================================================
88
85
 
89
86
  ' ---------------------------------------------------------------------
90
- ' getState - Returns current state from task node field (optionally mapped)
87
+ ' getState - Returns current state from state node field (optionally mapped)
91
88
  '
92
- ' Reads state from task node field set by internal dispatcher.
89
+ ' Reads state from state node field set by internal dispatcher.
93
90
  '
94
91
  ' @param {dynamic} mapStateToProps - Optional mapping function
95
92
  ' @param {object} callerScope - Caller scope for mapping function
96
93
  ' @returns {object} Current state
97
94
  '
98
95
  function getState(mapStateToProps = invalid as Dynamic, callerScope = invalid as object) as object
99
- ' Read state from task node field
100
- state = m.taskNode.getField(m.dispatcherId)
96
+ ' Read state from state node field
97
+ state = m.stateNode.getField(m.dispatcherId)
101
98
 
102
99
  ' Apply optional state mapping
103
100
  m.runMapStateToProps(state, mapStateToProps, callerScope)
@@ -150,7 +147,7 @@ namespace Rotor
150
147
  ' =============================================================
151
148
 
152
149
  ' ---------------------------------------------------------------------
153
- ' setupObserver - Sets up observer for state changes on task node field
150
+ ' setupObserver - Sets up observer for state changes on state node field
154
151
  '
155
152
  ' Thread-specific observer setup:
156
153
  ' - RENDER thread: Uses observeFieldScoped with callback name
@@ -159,12 +156,12 @@ namespace Rotor
159
156
  sub setupObserver()
160
157
  if m.threadType = Rotor.Const.ThreadType.RENDER
161
158
  ' Render thread: use native SceneGraph observer
162
- m.taskNode.observeFieldScoped(m.dispatcherId, "Rotor_dispatcherStateCallback")
159
+ m.stateNode.observeFieldScoped(m.dispatcherId, "Rotor_dispatcherStateCallback")
163
160
  else
164
161
  ' Task thread: use framework observer system
165
162
  fieldId = m.dispatcherId
166
163
  frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
167
- frameworkInstance.addObserver(fieldId, m.taskNode)
164
+ frameworkInstance.addObserver(fieldId, m.stateNode)
168
165
  end if
169
166
 
170
167
  m.isStateObserved = true
@@ -178,12 +175,12 @@ namespace Rotor
178
175
  sub removeObserver()
179
176
  if m.threadType = Rotor.Const.ThreadType.RENDER
180
177
  ' Render thread: unobserve SceneGraph field
181
- m.taskNode.unobserveFieldScoped(m.dispatcherId)
178
+ m.stateNode.unobserveFieldScoped(m.dispatcherId)
182
179
  else
183
180
  ' Task thread: remove framework observer
184
181
  fieldId = m.dispatcherId
185
182
  frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
186
- frameworkInstance.removeObserver(fieldId, m.taskNode)
183
+ frameworkInstance.removeObserver(fieldId, m.stateNode)
187
184
  end if
188
185
 
189
186
  m.isStateObserved = false
@@ -194,7 +191,7 @@ namespace Rotor
194
191
  ' =============================================================
195
192
 
196
193
  ' ---------------------------------------------------------------------
197
- ' destroy - Cleans up external dispatcher resources
194
+ ' destroy - Cleans up cross-thread dispatcher resources
198
195
  '
199
196
  ' Cleanup steps:
200
197
  ' 1. Call parent cleanup
@@ -210,16 +207,16 @@ namespace Rotor
210
207
  ' Remove observer if active
211
208
  if m.isStateObserved = true
212
209
  if m.threadType = Rotor.Const.ThreadType.RENDER
213
- m.taskNode.unobserveFieldScoped(m.dispatcherId)
210
+ m.stateNode.unobserveFieldScoped(m.dispatcherId)
214
211
  else
215
212
  fieldId = m.dispatcherId
216
- frameworkInstance.removeObserver(fieldId, m.taskNode)
213
+ frameworkInstance.removeObserver(fieldId, m.stateNode)
217
214
  end if
218
215
  end if
219
216
 
220
217
  ' Clear all references
221
218
  m.listeners.Clear()
222
- m.taskNode = invalid
219
+ m.stateNode = invalid
223
220
 
224
221
  ' Deregister from provider
225
222
  frameworkInstance.dispatcherProvider.deregisterDispatcher(m.dispatcherId)
@@ -235,7 +232,7 @@ namespace Rotor
235
232
  ' dispatcherStateCallback - Global callback function for dispatcher state changes on render thread
236
233
  '
237
234
  ' This function is called by SceneGraph's observeFieldScoped when a dispatcher's
238
- ' state field changes on the task node. It routes the state change to the
235
+ ' state field changes on the state node. It routes the state change to the
239
236
  ' appropriate dispatcher instance for listener notification.
240
237
  '
241
238
  ' Process flow:
@@ -1,4 +1,4 @@
1
- import "ListenerForDispatchers.bs"
1
+ import "DispatcherCore.bs"
2
2
 
3
3
  namespace Rotor
4
4
 
@@ -13,11 +13,11 @@ namespace Rotor
13
13
  ' @returns {Dispatcher} Dispatcher facade instance
14
14
  ' =====================================================================
15
15
  sub createDispatcher(customDispatcherId as string, modelInstance as Model, reducerInstance as Reducer)
16
- m.dispatcherInstance = new Rotor.DispatcherCreator(customDispatcherId, modelInstance, reducerInstance)
16
+ m.dispatcherInstance = new Rotor.DispatcherOriginal(customDispatcherId, modelInstance, reducerInstance)
17
17
  end sub
18
18
 
19
19
  ' =====================================================================
20
- ' DispatcherCreator - Internal dispatcher implementation for MVI pattern state management
20
+ ' DispatcherOriginal - Internal dispatcher implementation for MVI pattern state management
21
21
  '
22
22
  ' Responsibilities:
23
23
  ' - Dispatches intents to reducer
@@ -29,21 +29,17 @@ namespace Rotor
29
29
  ' MVI Flow:
30
30
  ' dispatch(intent) → reducer.reduce() → update state → notify listeners
31
31
  ' =====================================================================
32
- class DispatcherCreator extends ListenerForDispatchers
32
+ class DispatcherOriginal extends DispatcherCore
33
33
 
34
34
  ' =============================================================
35
35
  ' MEMBER VARIABLES
36
36
  ' =============================================================
37
37
 
38
38
  middlewares = [] ' Middleware array (unused, handled by Reducer)
39
- isExternal = false ' False for internal dispatchers
40
39
  isDispatchingInProgress = false ' Prevents reentrant dispatch
41
40
  dispatchQueue = [] ' Queue for intents during active dispatch
42
- dispatcherId as string ' Dispatcher identifier
43
- taskNode as object ' Task node for cross-thread state exposure
44
41
  reducerInstance as object ' Reducer instance
45
42
  modelInstance as object ' Model instance holding state
46
- listeners = [] ' State change listeners
47
43
 
48
44
  ' =============================================================
49
45
  ' CONSTRUCTOR
@@ -62,7 +58,7 @@ namespace Rotor
62
58
  m.dispatcherId = dispatcherId
63
59
 
64
60
  globalScope = GetGlobalAA()
65
- m.taskNode = globalScope.top
61
+ m.stateNode = globalScope.top
66
62
 
67
63
  ' Link reducer to this dispatcher
68
64
  m.reducerInstance = reducerInstance
@@ -71,7 +67,7 @@ namespace Rotor
71
67
  m.modelInstance = modelInstance
72
68
 
73
69
  ' Create field on task node for render thread communication
74
- m.taskNode.addField(m.dispatcherId, "node", true)
70
+ m.stateNode.addField(m.dispatcherId, "node", true)
75
71
 
76
72
  ' Expose initial state
77
73
  m.exposeState()
@@ -94,7 +90,7 @@ namespace Rotor
94
90
  sub exposeState()
95
91
  newState = CreateObject("roSGNode", "ContentNode")
96
92
  newState.addFields(m.modelInstance.state)
97
- m.taskNode.setField(m.dispatcherId, newState)
93
+ m.stateNode.setField(m.dispatcherId, newState)
98
94
  end sub
99
95
 
100
96
  ' ---------------------------------------------------------------------
@@ -131,6 +127,7 @@ namespace Rotor
131
127
  end if
132
128
 
133
129
  if m.isDispatchingInProgress = false
130
+
134
131
  ' Execute reducer
135
132
  currentState = m.modelInstance.state
136
133
  newState = m.reducerInstance.reduce(currentState, intent)
@@ -148,7 +145,7 @@ namespace Rotor
148
145
  m.dispatch(intent)
149
146
  end if
150
147
  else
151
- ' Queue intent to prevent reentrancy
148
+ ' Queue intent for later processing
152
149
  m.dispatchQueue.push(intent)
153
150
  end if
154
151
  end sub
@@ -183,7 +180,7 @@ namespace Rotor
183
180
  frameworkInstance = GetGlobalAA().rotor_framework_helper.frameworkInstance
184
181
  frameworkInstance.dispatcherProvider.deregisterDispatcher(m.dispatcherId)
185
182
 
186
- m.taskNode = invalid
183
+ m.stateNode = invalid
187
184
  m.modelInstance = invalid
188
185
  m.reducerInstance = invalid
189
186
  end sub
@@ -57,7 +57,7 @@ namespace Rotor
57
57
 
58
58
  enum ThreadSyncType
59
59
  TASK_SYNCING = "taskSyncing"
60
- REGISTER_EXTERNAL_DISPATCHER = "registerExternalDispatcher"
60
+ REGISTER_CROSS_THREAD_DISPATCHER = "registerCrossThreadDispatcher"
61
61
  DESTROY = "destroy"
62
62
  TASK_SYNCED = "taskSynced"
63
63
  SYNC_COMPLETED = "sync_completed"
@@ -2,7 +2,7 @@
2
2
  namespace Rotor
3
3
 
4
4
  ' =====================================================================
5
- ' Dispatcher - Dispatcher Facade that provides a simplified interface for state management
5
+ ' Dispatcher FACADE - Dispatcher Facade that provides an interface for state management
6
6
  '
7
7
  ' Acts as a proxy to the underlying dispatcher instance, managing intents,
8
8
  ' listeners, and state access. The facade pattern allows for clean separation
@@ -12,11 +12,12 @@ namespace Rotor
12
12
  class Dispatcher
13
13
 
14
14
  dispatcherId as string
15
- listenerId as string
16
- listenerScope as object
17
- dispatcherInstance as object
18
15
 
19
- sub new(dispatcherInstance as dynamic, dispatcherId as string, listenerId = "" as string, listenerScope = invalid as object)
16
+ private listenerId as string
17
+ private listenerScope as object
18
+ private dispatcherInstance as object
19
+
20
+ private sub new(dispatcherInstance as dynamic, dispatcherId as string, listenerId = "" as string, listenerScope = invalid as object)
20
21
  m.dispatcherId = dispatcherId
21
22
  m.listenerScope = listenerScope
22
23
  m.listenerId = listenerId
@@ -28,7 +29,7 @@ namespace Rotor
28
29
  '
29
30
  ' @param {Intent} intent - The intent object containing action type and payload
30
31
  '
31
- sub dispatch(intent)
32
+ public sub dispatch(intent)
32
33
  m.dispatcherInstance.dispatch(intent)
33
34
  end sub
34
35
 
@@ -37,31 +38,31 @@ namespace Rotor
37
38
  '
38
39
  ' @param {Dynamic} listenerConfig - Configuration object for the listener (can be a function or object with mapStateToProps)
39
40
  '
40
- sub addListener(listenerConfig as dynamic)
41
+ public sub addListener(listenerConfig as dynamic)
41
42
  m.dispatcherInstance.addListener(listenerConfig, m.listenerId, m.listenerScope)
42
43
  end sub
43
44
 
44
- ' ---------------------------------------------------------------------
45
- ' removeAllListenersByListenerId - Removes all listeners associated with this dispatcher's listener ID
46
- '
47
- sub removeAllListenersByListenerId()
48
- m.dispatcherInstance.removeAllListenersByListenerId(m.listenerId)
49
- end sub
50
-
51
45
  ' ---------------------------------------------------------------------
52
46
  ' getState - Retrieves the current state from the dispatcher, optionally mapped through a selector function
53
47
  '
54
48
  ' @param {Dynamic} mapStateToProps - Optional function to map/select specific parts of the state
55
49
  ' @returns {Object} The current state object (mapped if selector provided)
56
50
  '
57
- function getState(mapStateToProps = invalid as Dynamic) as object
51
+ public function getState(mapStateToProps = invalid as Dynamic) as object
58
52
  return m.dispatcherInstance.getState(mapStateToProps, m.listenerScope)
59
53
  end function
60
54
 
55
+ ' ---------------------------------------------------------------------
56
+ ' removeAllListenersByListenerId - Removes all listeners associated with this dispatcher's listener ID
57
+ '
58
+ private sub removeAllListenersByListenerId()
59
+ m.dispatcherInstance.removeAllListenersByListenerId(m.listenerId)
60
+ end sub
61
+
61
62
  ' ---------------------------------------------------------------------
62
63
  ' destroy - Cleans up the dispatcher facade by removing all listeners and clearing references
63
64
  '
64
- sub destroy()
65
+ public sub destroy()
65
66
  m.removeAllListenersByListenerId()
66
67
  m.listenerScope = invalid
67
68
  m.dispatcherInstance = invalid
@@ -6,7 +6,7 @@ namespace Rotor
6
6
  '
7
7
  ' Manages a collection of dispatcher instances and provides facade access
8
8
  ' to dispatchers. Extends BaseStack to store and organize dispatchers by ID.
9
- ' Handles both internal and external dispatcher registration/deregistration.
9
+ ' Handles both local and cross-thread dispatcher registration/deregistration.
10
10
  ' =====================================================================
11
11
  class DispatcherProvider extends Rotor.BaseStack
12
12
 
@@ -59,15 +59,15 @@ namespace Rotor
59
59
  end sub
60
60
 
61
61
  ' ---------------------------------------------------------------------
62
- ' registerExternalDispatchers - Registers external dispatchers from another thread
62
+ ' registerCrossThreadDispatchers - Registers cross-thread dispatchers from another thread
63
63
  '
64
64
  ' @param {dynamic} dispatcherIds - Single ID string or array of dispatcher IDs
65
- ' @param {object} taskNode - The task node for cross-thread communication
65
+ ' @param {object} stateNode - The node for cross-thread state communication
66
66
  '
67
- sub registerExternalDispatchers(dispatcherIds as dynamic, taskNode as object)
67
+ sub registerCrossThreadDispatchers(dispatcherIds as dynamic, stateNode as object)
68
68
  dispatcherIds = Rotor.Utils.ensureArray(dispatcherIds)
69
69
  for each dispatcherId in dispatcherIds
70
- dispatcherInstance = new Rotor.DispatcherExternal(dispatcherId, taskNode, m.threadType)
70
+ dispatcherInstance = new Rotor.DispatcherCrossThread(dispatcherId, stateNode, m.threadType)
71
71
  m.set(dispatcherId, dispatcherInstance)
72
72
  end for
73
73
  end sub
@@ -114,13 +114,15 @@ namespace Rotor.ViewBuilder
114
114
  m.audioGuide = CreateObject("roAudioGuide")
115
115
  m.debounceTimer = CreateObject("roTimespan")
116
116
 
117
- if m.audioGuide = invalid
118
- #if debug
119
- ? "[TTS_SERVICE][ERROR] Failed to create roAudioGuide instance"
120
- #end if
121
- m.isEnabled = false
122
- return
123
- end if
117
+ #if not unittest
118
+ if m.audioGuide = invalid
119
+ #if debug
120
+ ? "[TTS_SERVICE][ERROR] Failed to create roAudioGuide instance"
121
+ #end if
122
+ m.isEnabled = false
123
+ return
124
+ end if
125
+ #end if
124
126
 
125
127
  ' Create timer node for threshold (one-shot, started on demand)
126
128
  rootNode = m.frameworkInstance.getRootNode()
@@ -377,7 +379,10 @@ namespace Rotor.ViewBuilder
377
379
  end if
378
380
 
379
381
  ' Speak text (Say() handles flushing if shouldFlush=true)
380
- speechId = m.audioGuide.Say(text, shouldFlush, dontRepeat)
382
+ speechId = 0
383
+ if m.audioGuide <> invalid
384
+ speechId = m.audioGuide.Say(text, shouldFlush, dontRepeat)
385
+ end if
381
386
  m.lastSpeech = text
382
387
 
383
388
  ' Set protection flag if requested
@@ -542,7 +547,9 @@ namespace Rotor.ViewBuilder
542
547
  '
543
548
  public sub stopSpeech()
544
549
  if not m.isEnabled or not m.getIsDeviceAudioGuideEnabled() then return
545
- m.audioGuide.Flush()
550
+ if m.audioGuide <> invalid
551
+ m.audioGuide.Flush()
552
+ end if
546
553
  m.preventNextFlushFlag = false
547
554
  m.pendingSpeech = invalid
548
555
  m.isPendingProtected = false
@@ -324,14 +324,14 @@ namespace Rotor
324
324
  end function,
325
325
 
326
326
  ' ---------------------------------------------------------------------
327
- ' setGroupLastFocusedHID - Updates the lastFocusedHID of this widget's focus group
327
+ ' setGroupLastFocusedId - Updates the lastFocusedHID of this widget's focus group
328
328
  '
329
329
  ' If called on a focusGroup widget, updates its own lastFocusedHID.
330
330
  ' If called on a focusItem widget, finds and updates the parent group's lastFocusedHID.
331
331
  '
332
332
  ' @param {string} id - The widget id to set as lastFocusedHID
333
333
  '
334
- setGroupLastFocusedHID: sub(id as string)
334
+ setGroupLastFocusedId: sub(id as string)
335
335
  plugin = m.getFrameworkInstance().plugins[PRIMARY_FOCUS_PLUGIN_KEY]
336
336
 
337
337
  ' Determine ancestorHID for search context
@@ -1,22 +1,21 @@
1
- import "../base/BasePlugin.bs"
2
-
3
1
  namespace Rotor
4
2
 
5
3
  ' =====================================================================
6
4
  ' FontStylePlugin - Rotor Framework plugin for font styling on Labels
7
5
  '
8
- ' Handles dynamic font styling on Label nodes with function evaluation
9
- ' and @-prefixed expression interpolation for dynamic font selection.
10
- ' Automatically updates font styles on widget lifecycle changes.
6
+ ' Applies font styles to Label nodes with @ operator resolution.
11
7
  '
12
- ' Key Features:
13
- ' - Applies font styles specifically to Label nodes
14
- ' - Evaluates function-based font style values
15
- ' - Interpolates @-prefixed expressions for dynamic font selection
16
- ' - Automatically updates font styles on widget lifecycle changes
8
+ ' Usage:
9
+ ' fontStyle: { uri: "pkg:/assets/fonts/Roboto.ttf", size: 24 }
10
+ ' fontStyle: { uri: "@l10n.fonts.regular", size: 24 }
11
+ ' fontStyle: { uri: "@fontUri", size: "@fontSize" }
12
+ ' fontStyle: function() as object
13
+ ' return { uri: m.props.fontUri, size: 24 }
14
+ ' end function
17
15
  '
18
16
  ' Expression Syntax:
19
- ' @key.path - Resolves from widget.viewModelState
17
+ ' @l10n.key.path - Resolves from widget.viewModelState.l10n
18
+ ' @key.path - Resolves from widget.viewModelState
20
19
  '
21
20
  ' Note: Only affects widgets with nodeType = "Label"
22
21
  ' =====================================================================
@@ -24,123 +23,49 @@ namespace Rotor
24
23
 
25
24
  pluginKey = "fontStyle"
26
25
 
27
- ' =============================================================
28
- ' MEMBER VARIABLES
29
- ' =============================================================
30
-
31
- ' Regex pattern for matching @-prefixed expressions
32
- ' Matches: @ followed by any characters except space, @, or comma
33
- configRegex = /(\@)([^\s\@\,]*)/i
34
-
35
- ' Regex pattern for extracting plugin key prefix (unused but kept for compatibility)
36
- pluginKeyRegex = /^[^\.]*/i
37
-
38
- ' =============================================================
39
- ' LIFECYCLE HOOKS
40
- ' =============================================================
41
-
42
26
  hooks = {
43
- ' ---------------------------------------------------------------------
44
- ' beforeMount - Sets font style when widget is mounted
45
- '
46
- ' Evaluates and applies font style after Label widget creation.
47
- '
48
- ' @param {object} scope - Plugin instance (m)
49
- ' @param {object} widget - Widget being mounted
50
- '
51
27
  beforeMount: sub(scope as object, widget as object)
52
28
  scope.setFontAttribute(widget)
53
29
  end sub,
54
30
 
55
- ' ---------------------------------------------------------------------
56
- ' beforeUpdate - Updates font style when widget config changes
57
- '
58
- ' Re-evaluates and applies font style when fontStyle property changes.
59
- '
60
- ' @param {object} scope - Plugin instance (m)
61
- ' @param {object} widget - Widget being updated
62
- ' @param {dynamic} newValue - New font style configuration
63
- ' @param {dynamic} oldValue - Previous font style configuration
64
- '
65
31
  beforeUpdate: sub(scope as object, widget as object, newValue, oldValue)
66
32
  widget[scope.pluginKey] = newValue ?? ""
67
33
  scope.setFontAttribute(widget)
68
34
  end sub
69
35
  }
70
36
 
71
- ' =============================================================
72
- ' FONT STYLE PROCESSING
73
- ' =============================================================
74
-
75
- ' ---------------------------------------------------------------------
76
- ' setFontAttribute - Evaluates and applies font style to Label node
77
- '
78
- ' Processing logic:
79
- ' 1. Checks if widget is a Label node (only Labels support font styles)
80
- ' 2. Resolves font style value through function evaluation or interpolation
81
- ' 3. Applies resolved font style to the node
82
- '
83
- ' Font Style Resolution:
84
- ' - Functions are executed in widget scope
85
- ' - @-prefixed strings are interpolated from viewModelState
86
- ' - Direct strings are used as-is
87
- '
88
- ' @param {object} widget - Widget instance
89
- '
90
37
  sub setFontAttribute(widget as object)
91
- ' Font styles only apply to Label nodes
92
- if widget.nodeType = "Label"
93
- value = widget[m.pluginKey]
94
- node = widget.node
95
-
96
- ' Step 1: Resolve function-based font style
97
- if Rotor.Utils.isFunction(value)
98
- fontStyle = Rotor.Utils.callbackScoped(value, widget)
38
+ if widget.nodeType <> "Label" then return
99
39
 
100
- ' Step 2: Process string interpolation
101
- else if Rotor.Utils.isString(value)
102
- results = m.configRegex.MatchAll(value)
40
+ value = widget[m.pluginKey]
103
41
 
104
- if results.Count() > 0 and Rotor.Utils.isString(value)
105
- for each result in results
106
- matchKey = result[2] ' The key path after @
107
- sourceTypeOperator = result[1] ' The @ symbol
108
-
109
- ' Determine source based on operator
110
- if sourceTypeOperator = "@"
111
- source = widget.viewModelState
112
- else
113
- source = widget
114
- end if
115
-
116
- if source <> invalid
117
- ' Resolve font style from key path
118
- assetValue = Rotor.Utils.getValueByKeyPath(source, matchKey)
42
+ ' Evaluate function if fontStyle is a function
43
+ if Rotor.Utils.isFunction(value)
44
+ value = Rotor.Utils.callbackScoped(value, widget)
45
+ end if
119
46
 
120
- ' Handle string vs non-string results
121
- if Rotor.Utils.isString(assetValue)
122
- ' String interpolation - replace in original string
123
- replaceRegex = CreateObject("roRegex", sourceTypeOperator + matchKey, "ig")
124
- value = replaceRegex.ReplaceAll(value, assetValue)
125
- else
126
- ' Non-string value - replace entire font style
127
- value = assetValue
128
- exit for
129
- end if
130
- end if
131
- end for
47
+ if type(value) <> "roAssociativeArray" then return
48
+
49
+ ' Resolve @ operators in properties
50
+ fontStyle = {}
51
+ for each key in value
52
+ propValue = value[key]
53
+ if Rotor.Utils.isString(propValue)
54
+ if propValue.StartsWith("@l10n.")
55
+ ' @l10n.fonts.regular -> resolve from viewModelState.l10n.fonts.regular
56
+ fontStyle[key] = Rotor.Utils.getValueByKeyPath(widget.viewModelState.l10n, propValue.Mid(6))
57
+ else if propValue.StartsWith("@")
58
+ ' @key.path -> resolve from viewModelState
59
+ fontStyle[key] = Rotor.Utils.getValueByKeyPath(widget.viewModelState, propValue.Mid(1))
60
+ else
61
+ fontStyle[key] = propValue
132
62
  end if
133
-
134
- fontStyle = value
135
-
136
- ' Step 3: Direct value assignment
137
63
  else
138
- fontStyle = value
64
+ fontStyle[key] = propValue
139
65
  end if
66
+ end for
140
67
 
141
- ' Apply font style to the Label node
142
- Rotor.Utils.setFontAttribute(node, fontStyle)
143
- end if
68
+ Rotor.Utils.setFontAttribute(widget.node, fontStyle)
144
69
  end sub
145
70
 
146
71
  end class