rotor-framework 0.7.0 β†’ 0.7.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.
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.0-blue?label=Documents%20TAG)
99
+ ![Version](https://img.shields.io/badge/version-v0.7.2-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.0",
3
+ "version": "0.7.2",
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.0
7
+ ' Version 0.7.2
8
8
  ' Β© 2025 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
9
9
  ' =========================================================================
10
10
 
@@ -86,7 +86,7 @@ namespace Rotor
86
86
  class Framework
87
87
 
88
88
  name = "Rotor Framework"
89
- version = "0.7.0"
89
+ version = "0.7.2"
90
90
 
91
91
  config = {
92
92
  tasks: invalid, ' @array (optional)
@@ -118,10 +118,6 @@ namespace Rotor
118
118
  ttsService as object
119
119
  dispatcherProvider as object
120
120
  animatorProvider as object
121
- info = {
122
- device: {}
123
- }
124
-
125
121
  ' plugin adapter workspace
126
122
  plugins = {}
127
123
 
@@ -132,9 +128,6 @@ namespace Rotor
132
128
 
133
129
  enableRendering = true
134
130
 
135
- ' helper vars
136
- deviceInfo as object
137
-
138
131
  '----------------------------------------------------------------------
139
132
  ' new - Initializes the Rotor Framework instance
140
133
  '
@@ -161,8 +154,6 @@ namespace Rotor
161
154
 
162
155
  m.builder.init(m)
163
156
 
164
- m.initInfo()
165
-
166
157
  ' set root node
167
158
  if m.config.rootNode = invalid
168
159
  rootNode = globalScope.top
@@ -325,15 +316,6 @@ namespace Rotor
325
316
  return m.animatorProvider.getFactory(animatorId, m)
326
317
  end function
327
318
 
328
- ' ---------------------------------------------------------------------
329
- ' getInfo - Gets framework and device information
330
- '
331
- ' @returns {object} Info object containing device details
332
- '
333
- public function getInfo() as object
334
- return m.info
335
- end function
336
-
337
319
  ' ---------------------------------------------------------------------
338
320
  ' getNodePoolInfo - Gets node pool statistics
339
321
  '
@@ -366,32 +348,6 @@ namespace Rotor
366
348
  m.builder.pluginAdapter.registerPlugins(plugins)
367
349
  end sub
368
350
 
369
- ' ---------------------------------------------------------------------
370
- ' initInfo - Initializes device and framework information
371
- '
372
- ' Collects device information (graphics platform, model, OS version, locale)
373
- ' and stores it in m.info for later access.
374
- '
375
- sub initInfo()
376
- ' Device info
377
- di = CreateObject("roDeviceInfo")
378
- ' Make it available on framework instance
379
- m.deviceInfo = di
380
- ' Generate app info
381
- m.info.device.append({
382
- graphicsPlatform: di.GetGraphicsPlatform(), ' ["opengl"|"directfb"]
383
- modelDisplayName: di.GetModelDisplayName(),
384
- OSVersion: di.GetOSVersion(),
385
- currentLocale: di.GetCurrentLocale(), ' Example: "en_US"
386
- countryCode: di.GetCountryCode() ' A value that indicates the Streaming Store associated with a user's Roku account.
387
- })
388
- #if debug
389
- print `[DEVICEINFO] Graphics Platform: ${m.info.device.graphicsPlatform}`
390
- print `[DEVICEINFO] modelDisplayName: ${m.info.device.modelDisplayName}`
391
- print `[DEVICEINFO] OSVersion: ${m.info.device.OSVersion.major}.${m.info.device.OSVersion.minor}.${m.info.device.OSVersion.revision}.${m.info.device.OSVersion.build}`
392
- #end if
393
- end sub
394
-
395
351
  ' ---------------------------------------------------------------------
396
352
  ' setupTaskForSyncing - Prepares a task node for cross-thread syncing
397
353
  '
@@ -619,9 +575,6 @@ namespace Rotor
619
575
  }
620
576
  m.config.rootNode = invalid
621
577
 
622
- m.info.Clear()
623
- m.deviceInfo = invalid
624
-
625
578
  end sub
626
579
 
627
580
  end class
@@ -646,7 +599,15 @@ namespace Rotor
646
599
 
647
600
  taskNode = sync.taskNode
648
601
 
649
- if sync.type = Rotor.Const.ThreadSyncType.TASK_SYNCING
602
+ if sync.type = Rotor.Const.ThreadSyncType.DISPATCH
603
+ ' Cross-thread dispatch from task thread to render thread
604
+ dispatcherId = sync.payload.dispatcherId
605
+ intent = sync.payload.intent
606
+ dispatcherInstance = framework.dispatcherProvider.stack.LookupCI(dispatcherId)
607
+ if dispatcherInstance <> invalid
608
+ dispatcherInstance.dispatch(intent)
609
+ end if
610
+ else if sync.type = Rotor.Const.ThreadSyncType.TASK_SYNCING
650
611
  framework.handleTaskSyncing(sync)
651
612
  else if sync.type = Rotor.Const.ThreadSyncType.TASK_SYNCED
652
613
  framework.handleTaskSynced(taskNode.taskId)
@@ -4,7 +4,7 @@
4
4
  ' β–β–›β–€β–šβ––β–β–Œ β–β–Œ β–ˆ β–β–Œ β–β–Œβ–β–›β–€β–šβ–– β–β–›β–€β–€β–˜β–β–›β–€β–šβ––β–β–›β–€β–œβ–Œβ–β–Œ β–β–Œβ–β–›β–€β–€β–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–›β–€β–šβ––β–β–›β–šβ––
5
5
  ' β–β–Œ β–β–Œβ–β–šβ–„β–žβ–˜ β–ˆ β–β–šβ–„β–žβ–˜β–β–Œ β–β–Œ β–β–Œ β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–™β–„β–„β––β–β–™β–ˆβ–Ÿβ–Œβ–β–šβ–„β–žβ–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œ
6
6
  ' Rotor Frameworkβ„’
7
- ' Version 0.7.0
7
+ ' Version 0.7.2
8
8
  ' Β© 2025 BalΓ‘zs MolnΓ‘r β€” Apache License 2.0
9
9
  ' =========================================================================
10
10
 
@@ -70,7 +70,7 @@ namespace Rotor
70
70
  class FrameworkTask
71
71
 
72
72
  name = "Rotor Framework"
73
- version = "0.7.0"
73
+ version = "0.7.2"
74
74
 
75
75
  config = {
76
76
  tasks: invalid, ' optional
@@ -120,7 +120,9 @@ namespace Rotor
120
120
  m.scope.animators.delete(m.animatorId)
121
121
  end if
122
122
 
123
- m.scope.animators = {}
123
+ if m.scope.animators = invalid
124
+ m.scope.animators = {}
125
+ end if
124
126
  m.scope.animators[m.animatorId] = {}
125
127
 
126
128
  parsedParams = Rotor.animatorParamsIntegrationHelper(params)
@@ -171,7 +173,9 @@ namespace Rotor
171
173
  m.scope.animators.delete(m.animatorId)
172
174
  end if
173
175
 
174
- m.scope.animators = {}
176
+ if m.scope.animators = invalid
177
+ m.scope.animators = {}
178
+ end if
175
179
  m.scope.animators[m.animatorId] = {}
176
180
 
177
181
  return {
@@ -36,11 +36,6 @@ namespace Rotor.ViewBuilder
36
36
  return GetGlobalAA().rotor_framework_helper.frameworkInstance
37
37
  end function
38
38
 
39
- ' getInfo - Returns framework info *'
40
- widget.getInfo = function() as object
41
- return m.getFrameworkInstance().getInfo()
42
- end function
43
-
44
39
  ' refresh - Refreshes specific widget properties by key paths *'
45
40
  widget.refresh = sub(featureKeyPaths as object)
46
41
  iterateKeyPaths = Rotor.Utils.ensureArray(featureKeyPaths)
@@ -195,7 +190,7 @@ namespace Rotor.ViewBuilder
195
190
  if template <> invalid and template.Count() > 0
196
191
  templateChildren = template.children
197
192
  template.delete("children")
198
- config = Rotor.Utils.deepExtendAA(config, template)
193
+ config = Rotor.Utils.deepExtendAA(template, config)
199
194
  config.children = templateChildren
200
195
  end if
201
196
 
@@ -31,8 +31,9 @@ namespace Rotor.ViewBuilder
31
31
  ' @param {Rotor.Framework} frameworkInstance - Framework instance reference
32
32
  '
33
33
  sub init(frameworkInstance as Rotor.Framework)
34
- ' Set current locale from framework device info
35
- currentLocale = frameworkInstance.getInfo().device.currentLocale
34
+ ' Set current locale from device info
35
+ deviceInfo = CreateObject("roDeviceInfo")
36
+ currentLocale = deviceInfo.GetCurrentLocale()
36
37
  ' Store current locale
37
38
  m.setLocale(currentLocale)
38
39
  end sub
@@ -12,6 +12,7 @@ namespace Rotor
12
12
  ' - Interpolates @-prefixed expressions (e.g., @viewModelState.value)
13
13
  ' - Supports dynamic field resolution from widget context
14
14
  ' - Automatically updates fields on widget lifecycle changes
15
+ ' - Converts \n to Chr(10) for multi-line text support
15
16
  '
16
17
  ' Expression Syntax:
17
18
  ' @key.path - Resolves from widget.viewModelState
@@ -27,8 +28,11 @@ namespace Rotor
27
28
 
28
29
  ' Regex pattern for matching @ and $ prefixed expressions
29
30
  ' @ = viewModelState, $ = widget properties
30
- ' Matches: @ or $ followed by any characters except space, @, $, or comma
31
- configRegex = /([\@\$])([^\s\@\$\,]*)/i
31
+ ' Matches: @ or $ followed by valid key path characters (letters, numbers, underscore, dot)
32
+ configRegex = /([\@\$])([a-zA-Z0-9_\.]*)/i
33
+
34
+ ' Regex pattern for matching \n (literal backslash-n) to convert to Chr(10)
35
+ newlineRegex = /\\n/
32
36
 
33
37
  ' =============================================================
34
38
  ' LIFECYCLE HOOKS
@@ -160,6 +164,11 @@ namespace Rotor
160
164
  end for
161
165
  end if
162
166
 
167
+ ' Step 3: Convert \n to Chr(10) for multi-line text
168
+ if Rotor.Utils.isString(value)
169
+ value = m.newlineRegex.ReplaceAll(value, Chr(10))
170
+ end if
171
+
163
172
  parsedFields[fieldId] = value
164
173
  else
165
174
  ' Step 3: Direct value assignment
@@ -321,7 +321,58 @@ namespace Rotor
321
321
  '
322
322
  triggerKeyPress: function(key) as object
323
323
  return m.getFrameworkInstance().plugins[PRIMARY_FOCUS_PLUGIN_KEY].onKeyEventHandler(key, true)
324
- end function
324
+ end function,
325
+
326
+ ' ---------------------------------------------------------------------
327
+ ' setGroupLastFocusedHID - Updates the lastFocusedHID of this widget's focus group
328
+ '
329
+ ' If called on a focusGroup widget, updates its own lastFocusedHID.
330
+ ' If called on a focusItem widget, finds and updates the parent group's lastFocusedHID.
331
+ '
332
+ ' @param {string} id - The widget id to set as lastFocusedHID
333
+ '
334
+ setGroupLastFocusedHID: sub(id as string)
335
+ plugin = m.getFrameworkInstance().plugins[PRIMARY_FOCUS_PLUGIN_KEY]
336
+
337
+ ' Determine ancestorHID for search context
338
+ ' If this is a focus item, use parent group's HID to find siblings
339
+ ancestorGroups = plugin.findAncestorGroups(m.HID)
340
+ if ancestorGroups.count() > 0
341
+ ancestorHID = ancestorGroups[0]
342
+ else
343
+ ancestorHID = m.HID
344
+ end if
345
+
346
+ ' Resolve id to HID - check focusItemStack first, then groupStack
347
+ focusItem = plugin.focusItemStack.getByNodeId(id, ancestorHID)
348
+ if focusItem <> invalid
349
+ resolvedHID = focusItem.HID
350
+ else
351
+ group = plugin.groupStack.getByNodeId(id, ancestorHID)
352
+ if group <> invalid
353
+ resolvedHID = group.HID
354
+ else
355
+ return
356
+ end if
357
+ end if
358
+
359
+ ' Check if this widget is a group
360
+ group = plugin.groupStack.get(m.HID)
361
+ if group <> invalid
362
+ group.setLastFocusedHID(resolvedHID)
363
+ return
364
+ end if
365
+
366
+ ' This is a focus item - find parent group
367
+ ancestorGroups = plugin.findAncestorGroups(m.HID)
368
+ if ancestorGroups.count() > 0
369
+ parentGroupHID = ancestorGroups[0]
370
+ parentGroup = plugin.groupStack.get(parentGroupHID)
371
+ if parentGroup <> invalid
372
+ parentGroup.setLastFocusedHID(resolvedHID)
373
+ end if
374
+ end if
375
+ end sub
325
376
 
326
377
  }
327
378
 
@@ -1130,7 +1181,7 @@ namespace Rotor
1130
1181
 
1131
1182
  class BaseFocusConfig
1132
1183
 
1133
- autoSetIsFocusedOnContext as boolean
1184
+ autoSetIsFocusedState as boolean
1134
1185
  staticDirection as object
1135
1186
 
1136
1187
  sub new (config as object)
@@ -1142,7 +1193,7 @@ namespace Rotor
1142
1193
  m.node = m.widget.node
1143
1194
  m.isFocused = config.isFocused ?? false
1144
1195
 
1145
- m.autoSetIsFocusedOnContext = config.autoSetIsFocusedOnContext ?? true
1196
+ m.autoSetIsFocusedState = config.autoSetIsFocusedState ?? true
1146
1197
 
1147
1198
  m.isEnabled = config.isEnabled ?? true
1148
1199
  m.staticDirection = {}
@@ -1160,7 +1211,7 @@ namespace Rotor
1160
1211
  Rotor.Utils.setCustomFields(m.node, { "isFocused": false }, true, true)
1161
1212
 
1162
1213
  ' convenience (usually this is used on viewModelState)
1163
- if false = m.widget.viewModelState.DoesExist("isFocused") and true = m.autoSetIsFocusedOnContext
1214
+ if false = m.widget.viewModelState.DoesExist("isFocused") and true = m.autoSetIsFocusedState
1164
1215
  m.widget.viewModelState.isFocused = false ' as default
1165
1216
  end if
1166
1217
 
@@ -1346,7 +1397,7 @@ namespace Rotor
1346
1397
 
1347
1398
  m.isFocused = isFocused
1348
1399
 
1349
- if m.autoSetIsFocusedOnContext
1400
+ if m.autoSetIsFocusedState
1350
1401
  m.widget.viewModelState.isInFocusChain = isFocused
1351
1402
  end if
1352
1403
  m.node.setField("isFocused", isFocused)
@@ -1470,7 +1521,7 @@ namespace Rotor
1470
1521
 
1471
1522
  m.isFocused = isFocused
1472
1523
 
1473
- if m.autoSetIsFocusedOnContext
1524
+ if m.autoSetIsFocusedState
1474
1525
  m.widget.viewModelState.isFocused = isFocused
1475
1526
  end if
1476
1527