Fix: Badges now display in List rows, List fills column width in SplitView

- Badge values are now extracted from BadgeModifier during row extraction
- Added extractBadgeValue() function using Mirror reflection
- ListRow and SelectableListRow now store badge value
- withAvailableWidth() now sets hasExplicitWidth for proper List expansion
This commit is contained in:
phranck
2026-02-10 12:58:31 +01:00
parent 1a328092a6
commit 352093fbe7
5 changed files with 42 additions and 8 deletions
+7 -2
View File
@@ -46,14 +46,19 @@ public struct SelectableListRow<SelectionValue: Hashable & Sendable>: Sendable,
/// The rendered content buffer.
public let buffer: FrameBuffer
/// Creates a selectable list row with type and buffer.
/// The badge value for this row (from environment).
public let badge: BadgeValue?
/// Creates a selectable list row with type, buffer, and optional badge.
///
/// - Parameters:
/// - type: The row type (header, content, or footer).
/// - buffer: The rendered row content.
public init(type: ListRowType<SelectionValue>, buffer: FrameBuffer) {
/// - badge: The badge value for this row (default: nil).
public init(type: ListRowType<SelectionValue>, buffer: FrameBuffer, badge: BadgeValue? = nil) {
self.type = type
self.buffer = buffer
self.badge = badge
}
/// Indicates whether this row can be selected and focused.
@@ -78,6 +78,23 @@ extension BadgeValue: Equatable {
}
}
// MARK: - Badge Extraction
/// Extracts the badge value from a view if it's wrapped in a BadgeModifier.
///
/// This is used by List to extract badge values during row extraction.
@MainActor
public func extractBadgeValue<V: View>(from view: V) -> BadgeValue? {
// Use Mirror to check if the view is a BadgeModifier
let mirror = Mirror(reflecting: view)
for child in mirror.children {
if child.label == "value", let badge = child.value as? BadgeValue {
return badge
}
}
return nil
}
// MARK: - Renderable
extension BadgeModifier: Renderable {
@@ -221,11 +221,15 @@ public struct RenderContext {
/// Used by layout containers (e.g., NavigationSplitView) to constrain
/// child views to a specific column width.
///
/// This also sets `hasExplicitWidth` to true so that child views
/// (like List) know to expand to fill the available width.
///
/// - Parameter width: The new available width in characters.
/// - Returns: A new RenderContext with the updated width.
func withAvailableWidth(_ width: Int) -> Self {
var copy = self
copy.availableWidth = width
copy.hasExplicitWidth = true
return copy
}
}
+13 -5
View File
@@ -19,6 +19,9 @@ struct ListRow<ID: Hashable> {
/// The rendered content buffer for this row.
let buffer: FrameBuffer
/// The badge value for this row (from environment).
let badge: BadgeValue?
/// The height of this row in lines.
var height: Int { buffer.height }
}
@@ -615,7 +618,7 @@ private struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Foo
// Check for ListRowExtractor (ForEach)
if let extractor = content as? ListRowExtractor {
let rows: [ListRow<SelectionValue>] = extractor.extractListRows(context: context)
return rows.map { SelectableListRow(type: .content(id: $0.id), buffer: $0.buffer) }
return rows.map { SelectableListRow(type: .content(id: $0.id), buffer: $0.buffer, badge: $0.badge) }
}
// Check for ChildInfoProvider (handles TupleView with multiple children)
@@ -670,7 +673,7 @@ private struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Foo
if let extractor = section as? ListRowExtractor {
let contentRows: [ListRow<SelectionValue>] = extractor.extractListRows(context: context)
for row in contentRows {
rows.append(SelectableListRow(type: .content(id: row.id), buffer: row.buffer))
rows.append(SelectableListRow(type: .content(id: row.id), buffer: row.buffer, badge: row.badge))
}
} else {
// Fallback: render content as single row (if Section content is not ForEach)
@@ -766,8 +769,8 @@ private struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Foo
}
}
// Check for badge in environment (only for content rows, on first line only)
let badge = context.environment.badgeValue
// Check for badge on the row (only for content rows, on first line only)
let badge = row.badge
let shouldRenderBadge = badge != nil && !badge!.isHidden && row.isSelectable
// Render each line - row content keeps its own styling
@@ -855,10 +858,15 @@ extension ForEach: ListRowExtractor {
data.compactMap { element -> ListRow<RowID>? in
let elementID = element[keyPath: idKeyPath]
let view = content(element)
// Extract badge if the view is wrapped in a BadgeModifier
let badge = extractBadgeValue(from: view)
// Render the view
let buffer = TUIkit.renderToBuffer(view, context: context)
guard let rowID = elementID as? RowID else { return nil }
return ListRow(id: rowID, buffer: buffer)
return ListRow(id: rowID, buffer: buffer, badge: badge)
}
}
}
+1 -1
View File
@@ -311,7 +311,7 @@ extension Section: ListRowExtractor {
// Fallback: render content as a single row (rare case)
let buffer = TUIkit.renderToBuffer(content, context: context)
if let indexID = 0 as? RowID {
return [ListRow(id: indexID, buffer: buffer)]
return [ListRow(id: indexID, buffer: buffer, badge: nil)]
}
return []
}