rotor-framework 0.7.4 β†’ 0.7.6

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.4-blue?label=Documents%20TAG)
99
+ ![Version](https://img.shields.io/badge/version-v0.7.6-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.4",
3
+ "version": "0.7.6",
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.4
7
+ ' Version 0.7.6
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.4"
89
+ version = "0.7.6"
90
90
 
91
91
  config = {
92
92
  tasks: invalid, ' @array (optional)
@@ -4,7 +4,7 @@
4
4
  ' β–β–›β–€β–šβ––β–β–Œ β–β–Œ β–ˆ β–β–Œ β–β–Œβ–β–›β–€β–šβ–– β–β–›β–€β–€β–˜β–β–›β–€β–šβ––β–β–›β–€β–œβ–Œβ–β–Œ β–β–Œβ–β–›β–€β–€β–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–›β–€β–šβ––β–β–›β–šβ––
5
5
  ' β–β–Œ β–β–Œβ–β–šβ–„β–žβ–˜ β–ˆ β–β–šβ–„β–žβ–˜β–β–Œ β–β–Œ β–β–Œ β–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–Œ β–β–Œβ–β–™β–„β–„β––β–β–™β–ˆβ–Ÿβ–Œβ–β–šβ–„β–žβ–˜β–β–Œ β–β–Œβ–β–Œ β–β–Œ
6
6
  ' Rotor Frameworkβ„’
7
- ' Version 0.7.4
7
+ ' Version 0.7.6
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.4"
73
+ version = "0.7.6"
74
74
 
75
75
  config = {
76
76
  tasks: invalid, ' optional
@@ -121,6 +121,13 @@ namespace Rotor
121
121
  ' 2. group.defaultFocusId [CONFIGURED]
122
122
  ' 3. Deep search (if defaultFocusId not found immediately)
123
123
  '
124
+ ' RULE #11b: Deep Focus Tracking (trackDescendantFocus: true)
125
+ ' When a FocusGroup has trackDescendantFocus: true, it stores lastFocusedHID
126
+ ' for ANY descendant FocusItem (not just direct children).
127
+ ' - Immediate parent ALWAYS stores lastFocusedHID (default behavior)
128
+ ' - Ancestor groups with trackDescendantFocus: true ALSO store it
129
+ ' - Enables "deep focus memory" - returning to deeply nested items
130
+ '
124
131
  ' RULE #12: DefaultFocusId Targets
125
132
  ' - FocusItem node ID β†’ Focus goes directly to it
126
133
  ' - Group node ID β†’ Capturing continues on that group
@@ -636,6 +643,7 @@ namespace Rotor
636
643
  ' Record the last focused item within its parent group for potential future use (e.g., returning focus)
637
644
  lastFocusChainingGroups = m.findAncestorGroups(m.globalFocusHID)
638
645
  if lastFocusChainingGroups.Count() > 0
646
+ ' Always set on immediate parent (index 0)
639
647
  parentGroupHID = lastFocusChainingGroups[0]
640
648
  if parentGroupHID <> invalid and parentGroupHID <> ""
641
649
  group = m.groupStack.get(parentGroupHID)
@@ -652,9 +660,20 @@ namespace Rotor
652
660
  for each groupHID in focusChainGroups
653
661
  allAffectedGroups.unshift(groupHID) ' Add in reverse order (highest ancestor first)
654
662
  end for
655
- for each groupHID in lastFocusChainingGroups
663
+ for i = 0 to lastFocusChainingGroups.Count() - 1
664
+ groupHID = lastFocusChainingGroups[i]
665
+
666
+ ' Add to allAffectedGroups if not present
656
667
  if -1 = Rotor.Utils.findInArray(allAffectedGroups, groupHID)
657
- allAffectedGroups.unshift(groupHID) ' Add in reverse order if not already present
668
+ allAffectedGroups.unshift(groupHID)
669
+ end if
670
+
671
+ ' Deep remember (skip index 0 - immediate parent is handled separately)
672
+ if i > 0 and lastFocused <> invalid
673
+ ancestorGroup = m.groupStack.get(groupHID)
674
+ if ancestorGroup <> invalid and ancestorGroup.trackDescendantFocus = true
675
+ ancestorGroup.setLastFocusedHID(m.globalFocusHID)
676
+ end if
658
677
  end if
659
678
  end for
660
679
 
@@ -778,17 +797,12 @@ namespace Rotor
778
797
  if group = invalid then return ""
779
798
 
780
799
  ' Get fallback identifier for this group
781
- newHID = group.getFallbackIdentifier()
800
+ ' (enableSpatialEnter groups return the spatially closest member here)
801
+ newHID = group.getFallbackIdentifier(m.globalFocusHID)
782
802
 
783
803
  ' Check if we found a FocusItem
784
804
  if m.focusItemStack.has(newHID)
785
- ' Apply spatial enter feature if enabled
786
- if group.enableSpatialEnter = true and direction <> ""
787
- focused = m.focusItemStack.get(m.globalFocusHID)
788
- newSpatialHID = m.spatialNavigation(focused, direction, group.getGroupMembersHIDs())
789
- if newSpatialHID <> "" then newHID = newSpatialHID
790
- end if
791
-
805
+ ' noop β€” direct focusItem resolved
792
806
  else if newHID <> ""
793
807
  ' Try to find as group first, then deep search
794
808
  newHID = m.capturingFocus_recursively(newHID, direction, group.HID)
@@ -800,7 +814,8 @@ namespace Rotor
800
814
  end if
801
815
 
802
816
  ' Prevent capturing by fallback in the same group where original focus was
803
- if newHID <> "" and m.globalFocusHID <> ""
817
+ ' Skip this guard for enableSpatialEnter groups (spatial enter explicitly targets a sibling group's member)
818
+ if not group.enableSpatialEnter and newHID <> "" and m.globalFocusHID <> ""
804
819
  currentAncestors = m.findAncestorGroups(m.globalFocusHID)
805
820
  newAncestors = m.findAncestorGroups(newHID)
806
821
  if currentAncestors.Count() > 0 and newAncestors.Count() > 0
@@ -904,7 +919,6 @@ namespace Rotor
904
919
  end if
905
920
  ' Prevent any navigation if it is disabled
906
921
  #if debug
907
- if m.enableFocusNavigation = false and press = true then print "[PLUGIN][FOCUS][INFO] Focus navigation is disabled. Call enableFocusNavigation(true) to make it enabled"
908
922
  #end if
909
923
  if m.enableFocusNavigation = false then return m.parseOnKeyEventResult(key, false, false)
910
924
  ' Execute action according to key press
@@ -1279,14 +1293,15 @@ namespace Rotor
1279
1293
  m.defaultFocusId = config.defaultFocusId ?? ""
1280
1294
  m.lastFocusedHID = config.lastFocusedHID ?? ""
1281
1295
  m.enableSpatialEnter = config.enableSpatialEnter ?? false
1296
+ m.trackDescendantFocus = config.trackDescendantFocus ?? false
1282
1297
  end sub
1283
1298
 
1284
1299
  defaultFocusId as string
1285
1300
  lastFocusedHID as string
1286
1301
  enableSpatialEnter as boolean
1302
+ trackDescendantFocus as boolean
1287
1303
  focusItemsRef as object
1288
1304
  groupsRef as object
1289
-
1290
1305
  isFocusItem = false
1291
1306
  isGroup = true
1292
1307
 
@@ -1347,34 +1362,66 @@ namespace Rotor
1347
1362
  end if
1348
1363
  end function
1349
1364
 
1350
- function getFallbackIdentifier() as string
1365
+ function getFallbackIdentifier(globalFocusHID = "" as string) as string
1351
1366
  HID = ""
1352
- if m.lastFocusedHID <> ""
1367
+ ' enableSpatialEnter takes priority over lastFocusedHID
1368
+ ' (lastFocusedHID may be stale from slot recycling)
1369
+ if not m.enableSpatialEnter and m.lastFocusedHID <> ""
1353
1370
  return m.lastFocusedHID
1354
- else
1355
- if Rotor.Utils.isFunction(m.defaultFocusId)
1356
- defaultFocusId = Rotor.Utils.callbackScoped(m.defaultFocusId, m.widget) ?? ""
1357
- else
1358
- defaultFocusId = m.defaultFocusId
1359
- end if
1360
-
1361
- if defaultFocusId <> ""
1362
- focusItemsHIDlist = m.getGroupMembersHIDs()
1363
- if focusItemsHIDlist.Count() > 0
1371
+ end if
1364
1372
 
1365
- ' Try find valid HID in focusItems by node id
1366
- focusItemHID = m.findHIDinFocusItemsByNodeId(defaultFocusId, focusItemsHIDlist)
1367
- if focusItemHID <> ""
1368
- HID = focusItemHID
1373
+ ' enableSpatialEnter: return the spatially closest member to the current focus
1374
+ if m.enableSpatialEnter = true and globalFocusHID <> ""
1375
+ prevFocused = m.focusItemsRef.get(globalFocusHID)
1376
+ if prevFocused <> invalid
1377
+ prevFocused.refreshBounding()
1378
+ refPoint = prevFocused.metrics.middlePoint
1379
+ members = m.getGroupMembersHIDs()
1380
+ minDist = 2147483647
1381
+ closestHID = ""
1382
+ for each memberHID in members
1383
+ if memberHID <> globalFocusHID
1384
+ member = m.focusItemsRef.get(memberHID)
1385
+ if member <> invalid
1386
+ member.refreshBounding()
1387
+ dx = member.metrics.middlePoint.x - refPoint.x
1388
+ dy = member.metrics.middlePoint.y - refPoint.y
1389
+ dist = dx * dx + dy * dy
1390
+ if dist < minDist
1391
+ minDist = dist
1392
+ closestHID = memberHID
1393
+ end if
1394
+ end if
1369
1395
  end if
1396
+ end for
1397
+ if closestHID <> ""
1398
+ return closestHID
1399
+ end if
1400
+ end if
1401
+ end if
1370
1402
 
1371
- else
1403
+ ' Default: use defaultFocusId expression
1404
+ if Rotor.Utils.isFunction(m.defaultFocusId)
1405
+ defaultFocusId = Rotor.Utils.callbackScoped(m.defaultFocusId, m.widget) ?? ""
1406
+ else
1407
+ defaultFocusId = m.defaultFocusId
1408
+ end if
1372
1409
 
1373
- return defaultFocusId
1410
+ if defaultFocusId <> ""
1411
+ focusItemsHIDlist = m.getGroupMembersHIDs()
1412
+ if focusItemsHIDlist.Count() > 0
1374
1413
 
1414
+ ' Try find valid HID in focusItems by node id
1415
+ focusItemHID = m.findHIDinFocusItemsByNodeId(defaultFocusId, focusItemsHIDlist)
1416
+ if focusItemHID <> ""
1417
+ HID = focusItemHID
1375
1418
  end if
1376
- end if
1377
1419
 
1420
+ else
1421
+
1422
+ return defaultFocusId
1423
+
1424
+ end if
1378
1425
  end if
1379
1426
 
1380
1427
  return HID