mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
Refactor: Convert RadioButtonGroup to body: some View pattern
This commit is contained in:
@@ -183,106 +183,31 @@ public struct RadioButtonGroup<Value: Hashable>: View {
|
||||
self.isDisabled = isDisabled
|
||||
}
|
||||
|
||||
public var body: Never {
|
||||
fatalError("RadioButtonGroup renders via Renderable")
|
||||
public var body: some View {
|
||||
_RadioButtonGroupCore(
|
||||
selection: selection,
|
||||
items: items,
|
||||
orientation: orientation,
|
||||
focusID: focusID,
|
||||
isDisabled: isDisabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Radio Button Handler
|
||||
// MARK: - Internal Core View
|
||||
|
||||
/// Internal handler class for radio button group focus and selection management.
|
||||
///
|
||||
/// Persisted across renders via StateStorage to maintain focusedIndex and enable
|
||||
/// Tab navigation between radio button groups.
|
||||
final class RadioButtonGroupHandler: Focusable {
|
||||
let focusID: String
|
||||
var selection: Binding<AnyHashable>
|
||||
var itemValues: [AnyHashable]
|
||||
/// Internal view that handles the actual rendering of RadioButtonGroup.
|
||||
private struct _RadioButtonGroupCore<Value: Hashable>: View, Renderable {
|
||||
let selection: Binding<Value>
|
||||
let items: [RadioButtonItem<Value>]
|
||||
let orientation: RadioButtonOrientation
|
||||
var canBeFocused: Bool
|
||||
let focusID: String?
|
||||
let isDisabled: Bool
|
||||
|
||||
/// The currently focused item index within the group.
|
||||
/// Persisted across renders to maintain focus position.
|
||||
var focusedIndex: Int = 0
|
||||
|
||||
init(
|
||||
focusID: String,
|
||||
selection: Binding<AnyHashable>,
|
||||
itemValues: [AnyHashable],
|
||||
orientation: RadioButtonOrientation,
|
||||
canBeFocused: Bool
|
||||
) {
|
||||
self.focusID = focusID
|
||||
self.selection = selection
|
||||
self.itemValues = itemValues
|
||||
self.orientation = orientation
|
||||
self.canBeFocused = canBeFocused
|
||||
|
||||
// Find current focused index based on selection
|
||||
if let currentIndex = itemValues.firstIndex(of: selection.wrappedValue) {
|
||||
self.focusedIndex = currentIndex
|
||||
}
|
||||
var body: Never {
|
||||
fatalError("_RadioButtonGroupCore renders via Renderable")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Focus Lifecycle
|
||||
|
||||
extension RadioButtonGroupHandler {
|
||||
func onFocusLost() {
|
||||
// Reset focusedIndex to the selected item when the group loses focus
|
||||
if let selectedIndex = itemValues.firstIndex(of: selection.wrappedValue) {
|
||||
focusedIndex = selectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Key Event Handling
|
||||
|
||||
extension RadioButtonGroupHandler {
|
||||
func handleKeyEvent(_ event: KeyEvent) -> Bool {
|
||||
switch event.key {
|
||||
case .up:
|
||||
// Vertical: navigate focus up (don't change selection); Horizontal: consume but do nothing
|
||||
if orientation == .vertical {
|
||||
focusedIndex = focusedIndex > 0 ? focusedIndex - 1 : itemValues.count - 1
|
||||
}
|
||||
return true
|
||||
|
||||
case .down:
|
||||
// Vertical: navigate focus down (don't change selection); Horizontal: consume but do nothing
|
||||
if orientation == .vertical {
|
||||
focusedIndex = focusedIndex < itemValues.count - 1 ? focusedIndex + 1 : 0
|
||||
}
|
||||
return true
|
||||
|
||||
case .left:
|
||||
// Horizontal: navigate focus left (don't change selection); Vertical: consume but do nothing
|
||||
if orientation == .horizontal {
|
||||
focusedIndex = focusedIndex > 0 ? focusedIndex - 1 : itemValues.count - 1
|
||||
}
|
||||
return true
|
||||
|
||||
case .right:
|
||||
// Horizontal: navigate focus right (don't change selection); Vertical: consume but do nothing
|
||||
if orientation == .horizontal {
|
||||
focusedIndex = focusedIndex < itemValues.count - 1 ? focusedIndex + 1 : 0
|
||||
}
|
||||
return true
|
||||
|
||||
case .enter, .character(" "):
|
||||
// Select the currently focused item (make it the selection)
|
||||
selection.wrappedValue = itemValues[focusedIndex]
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Radio Button Group Rendering
|
||||
|
||||
extension RadioButtonGroup: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let focusManager = context.environment.focusManager
|
||||
let palette = context.environment.palette
|
||||
@@ -432,6 +357,98 @@ extension RadioButtonGroup: Renderable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Radio Button Handler
|
||||
|
||||
/// Internal handler class for radio button group focus and selection management.
|
||||
///
|
||||
/// Persisted across renders via StateStorage to maintain focusedIndex and enable
|
||||
/// Tab navigation between radio button groups.
|
||||
final class RadioButtonGroupHandler: Focusable {
|
||||
let focusID: String
|
||||
var selection: Binding<AnyHashable>
|
||||
var itemValues: [AnyHashable]
|
||||
let orientation: RadioButtonOrientation
|
||||
var canBeFocused: Bool
|
||||
|
||||
/// The currently focused item index within the group.
|
||||
/// Persisted across renders to maintain focus position.
|
||||
var focusedIndex: Int = 0
|
||||
|
||||
init(
|
||||
focusID: String,
|
||||
selection: Binding<AnyHashable>,
|
||||
itemValues: [AnyHashable],
|
||||
orientation: RadioButtonOrientation,
|
||||
canBeFocused: Bool
|
||||
) {
|
||||
self.focusID = focusID
|
||||
self.selection = selection
|
||||
self.itemValues = itemValues
|
||||
self.orientation = orientation
|
||||
self.canBeFocused = canBeFocused
|
||||
|
||||
// Find current focused index based on selection
|
||||
if let currentIndex = itemValues.firstIndex(of: selection.wrappedValue) {
|
||||
self.focusedIndex = currentIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Focus Lifecycle
|
||||
|
||||
extension RadioButtonGroupHandler {
|
||||
func onFocusLost() {
|
||||
// Reset focusedIndex to the selected item when the group loses focus
|
||||
if let selectedIndex = itemValues.firstIndex(of: selection.wrappedValue) {
|
||||
focusedIndex = selectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Key Event Handling
|
||||
|
||||
extension RadioButtonGroupHandler {
|
||||
func handleKeyEvent(_ event: KeyEvent) -> Bool {
|
||||
switch event.key {
|
||||
case .up:
|
||||
// Vertical: navigate focus up (don't change selection); Horizontal: consume but do nothing
|
||||
if orientation == .vertical {
|
||||
focusedIndex = focusedIndex > 0 ? focusedIndex - 1 : itemValues.count - 1
|
||||
}
|
||||
return true
|
||||
|
||||
case .down:
|
||||
// Vertical: navigate focus down (don't change selection); Horizontal: consume but do nothing
|
||||
if orientation == .vertical {
|
||||
focusedIndex = focusedIndex < itemValues.count - 1 ? focusedIndex + 1 : 0
|
||||
}
|
||||
return true
|
||||
|
||||
case .left:
|
||||
// Horizontal: navigate focus left (don't change selection); Vertical: consume but do nothing
|
||||
if orientation == .horizontal {
|
||||
focusedIndex = focusedIndex > 0 ? focusedIndex - 1 : itemValues.count - 1
|
||||
}
|
||||
return true
|
||||
|
||||
case .right:
|
||||
// Horizontal: navigate focus right (don't change selection); Vertical: consume but do nothing
|
||||
if orientation == .horizontal {
|
||||
focusedIndex = focusedIndex < itemValues.count - 1 ? focusedIndex + 1 : 0
|
||||
}
|
||||
return true
|
||||
|
||||
case .enter, .character(" "):
|
||||
// Select the currently focused item (make it the selection)
|
||||
selection.wrappedValue = itemValues[focusedIndex]
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Radio Button Group Convenience Modifiers
|
||||
|
||||
extension RadioButtonGroup {
|
||||
|
||||
@@ -283,8 +283,8 @@ Controls that need StateStorage/FocusManager:
|
||||
- [x] Button: Convert to body: some View
|
||||
- [x] Toggle: Convert to body: some View
|
||||
- [x] Menu: Convert to body: some View
|
||||
- [ ] RadioButtonGroup: Convert to body: some View
|
||||
- [ ] Tests pass for each
|
||||
- [x] RadioButtonGroup: Convert to body: some View
|
||||
- [x] Tests pass for each
|
||||
|
||||
### Phase 5: Complex Controls
|
||||
- [x] List: Convert to body: some View
|
||||
|
||||
Reference in New Issue
Block a user