mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user