mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
Refactor: Remove maxVisibleRows from List and Table (not in SwiftUI API)
List and Table now always fill available height like SwiftUI. The TUI-specific maxVisibleRows parameter was removed for API parity.
This commit is contained in:
@@ -101,9 +101,6 @@ public struct List<SelectionValue: Hashable & Sendable, Content: View, Footer: V
|
||||
/// Whether the list is disabled.
|
||||
var isDisabled: Bool
|
||||
|
||||
/// The maximum number of visible rows (nil = use available height).
|
||||
let maxVisibleRows: Int?
|
||||
|
||||
/// The placeholder text shown when the list is empty.
|
||||
let emptyPlaceholder: String
|
||||
|
||||
@@ -120,7 +117,6 @@ public struct List<SelectionValue: Hashable & Sendable, Content: View, Footer: V
|
||||
selectionMode: selectionMode,
|
||||
focusID: focusID,
|
||||
isDisabled: isDisabled,
|
||||
maxVisibleRows: maxVisibleRows,
|
||||
emptyPlaceholder: emptyPlaceholder,
|
||||
showFooterSeparator: showFooterSeparator
|
||||
)
|
||||
@@ -136,7 +132,7 @@ extension List {
|
||||
/// - title: The title displayed in the border.
|
||||
/// - selection: A binding to the selected item's ID (nil = no selection).
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - showFooterSeparator: Whether to show separator before footer (default: true).
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
@@ -145,7 +141,7 @@ extension List {
|
||||
_ title: String,
|
||||
selection: Binding<SelectionValue?>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
showFooterSeparator: Bool = true,
|
||||
@ViewBuilder content: () -> Content,
|
||||
@@ -158,7 +154,7 @@ extension List {
|
||||
self.multiSelection = nil
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = showFooterSeparator
|
||||
}
|
||||
@@ -168,7 +164,7 @@ extension List {
|
||||
/// - Parameters:
|
||||
/// - selection: A binding to the selected item's ID (nil = no selection).
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - showFooterSeparator: Whether to show separator before footer (default: true).
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
@@ -176,7 +172,7 @@ extension List {
|
||||
public init(
|
||||
selection: Binding<SelectionValue?>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
showFooterSeparator: Bool = true,
|
||||
@ViewBuilder content: () -> Content,
|
||||
@@ -189,7 +185,7 @@ extension List {
|
||||
self.multiSelection = nil
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = showFooterSeparator
|
||||
}
|
||||
@@ -204,14 +200,14 @@ extension List where Footer == EmptyView {
|
||||
/// - title: The title displayed in the border.
|
||||
/// - selection: A binding to the selected item's ID (nil = no selection).
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
public init(
|
||||
_ title: String,
|
||||
selection: Binding<SelectionValue?>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
@@ -222,7 +218,7 @@ extension List where Footer == EmptyView {
|
||||
self.multiSelection = nil
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = false
|
||||
}
|
||||
@@ -232,13 +228,13 @@ extension List where Footer == EmptyView {
|
||||
/// - Parameters:
|
||||
/// - selection: A binding to the selected item's ID (nil = no selection).
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
public init(
|
||||
selection: Binding<SelectionValue?>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
@@ -249,7 +245,7 @@ extension List where Footer == EmptyView {
|
||||
self.multiSelection = nil
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = false
|
||||
}
|
||||
@@ -264,7 +260,7 @@ extension List {
|
||||
/// - title: The title displayed in the border.
|
||||
/// - selection: A binding to the set of selected item IDs.
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - showFooterSeparator: Whether to show separator before footer (default: true).
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
@@ -273,7 +269,7 @@ extension List {
|
||||
_ title: String,
|
||||
selection: Binding<Set<SelectionValue>>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
showFooterSeparator: Bool = true,
|
||||
@ViewBuilder content: () -> Content,
|
||||
@@ -286,7 +282,7 @@ extension List {
|
||||
self.multiSelection = selection
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = showFooterSeparator
|
||||
}
|
||||
@@ -296,7 +292,7 @@ extension List {
|
||||
/// - Parameters:
|
||||
/// - selection: A binding to the set of selected item IDs.
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - showFooterSeparator: Whether to show separator before footer (default: true).
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
@@ -304,7 +300,7 @@ extension List {
|
||||
public init(
|
||||
selection: Binding<Set<SelectionValue>>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
showFooterSeparator: Bool = true,
|
||||
@ViewBuilder content: () -> Content,
|
||||
@@ -317,7 +313,7 @@ extension List {
|
||||
self.multiSelection = selection
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = showFooterSeparator
|
||||
}
|
||||
@@ -332,14 +328,14 @@ extension List where Footer == EmptyView {
|
||||
/// - title: The title displayed in the border.
|
||||
/// - selection: A binding to the set of selected item IDs.
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
public init(
|
||||
_ title: String,
|
||||
selection: Binding<Set<SelectionValue>>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
@@ -350,7 +346,7 @@ extension List where Footer == EmptyView {
|
||||
self.multiSelection = selection
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = false
|
||||
}
|
||||
@@ -360,13 +356,13 @@ extension List where Footer == EmptyView {
|
||||
/// - Parameters:
|
||||
/// - selection: A binding to the set of selected item IDs.
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - content: A ViewBuilder that defines the list content.
|
||||
public init(
|
||||
selection: Binding<Set<SelectionValue>>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
emptyPlaceholder: String = "No items",
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
@@ -377,7 +373,7 @@ extension List where Footer == EmptyView {
|
||||
self.multiSelection = selection
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
self.showFooterSeparator = false
|
||||
}
|
||||
@@ -409,7 +405,6 @@ private struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Foo
|
||||
let selectionMode: SelectionMode
|
||||
let focusID: String?
|
||||
let isDisabled: Bool
|
||||
let maxVisibleRows: Int?
|
||||
let emptyPlaceholder: String
|
||||
let showFooterSeparator: Bool
|
||||
|
||||
@@ -436,7 +431,7 @@ private struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Foo
|
||||
} else {
|
||||
// Calculate viewport height (reserve space for scroll indicators if needed)
|
||||
let availableHeight = context.availableHeight
|
||||
let viewportHeight = maxVisibleRows ?? max(1, availableHeight - 4) // Reserve for border + indicators
|
||||
let viewportHeight = max(1, availableHeight - 4) // Reserve for border + indicators
|
||||
|
||||
// Get or create persistent focusID
|
||||
let focusIDKey = StateStorage.StateKey(identity: context.identity, propertyIndex: 1)
|
||||
|
||||
@@ -69,9 +69,6 @@ public struct Table<Value: Identifiable & Sendable>: View where Value.ID: Hashab
|
||||
/// Whether the table is disabled.
|
||||
var isDisabled: Bool
|
||||
|
||||
/// The maximum number of visible rows (nil = use available height).
|
||||
let maxVisibleRows: Int?
|
||||
|
||||
/// The placeholder text shown when the table is empty.
|
||||
let emptyPlaceholder: String
|
||||
|
||||
@@ -87,7 +84,6 @@ public struct Table<Value: Identifiable & Sendable>: View where Value.ID: Hashab
|
||||
selectionMode: selectionMode,
|
||||
focusID: focusID,
|
||||
isDisabled: isDisabled,
|
||||
maxVisibleRows: maxVisibleRows,
|
||||
emptyPlaceholder: emptyPlaceholder,
|
||||
columnSpacing: columnSpacing
|
||||
)
|
||||
@@ -103,7 +99,7 @@ extension Table {
|
||||
/// - data: The data items to display.
|
||||
/// - selection: A binding to the selected item's ID (nil = no selection).
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - columnSpacing: Spacing between columns (default: 2).
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - columns: A builder that defines the table columns.
|
||||
@@ -111,7 +107,7 @@ extension Table {
|
||||
_ data: [Value],
|
||||
selection: Binding<Value.ID?>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
columnSpacing: Int = 2,
|
||||
emptyPlaceholder: String = "No items",
|
||||
@TableColumnBuilder<Value> columns: () -> [TableColumn<Value>]
|
||||
@@ -122,7 +118,7 @@ extension Table {
|
||||
self.multiSelection = nil
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.columnSpacing = columnSpacing
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
}
|
||||
@@ -137,7 +133,7 @@ extension Table {
|
||||
/// - data: The data items to display.
|
||||
/// - selection: A binding to the set of selected item IDs.
|
||||
/// - focusID: The unique focus identifier (default: auto-generated).
|
||||
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
|
||||
|
||||
/// - columnSpacing: Spacing between columns (default: 2).
|
||||
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
|
||||
/// - columns: A builder that defines the table columns.
|
||||
@@ -145,7 +141,7 @@ extension Table {
|
||||
_ data: [Value],
|
||||
selection: Binding<Set<Value.ID>>,
|
||||
focusID: String? = nil,
|
||||
maxVisibleRows: Int? = nil,
|
||||
|
||||
columnSpacing: Int = 2,
|
||||
emptyPlaceholder: String = "No items",
|
||||
@TableColumnBuilder<Value> columns: () -> [TableColumn<Value>]
|
||||
@@ -156,7 +152,7 @@ extension Table {
|
||||
self.multiSelection = selection
|
||||
self.focusID = focusID
|
||||
self.isDisabled = false
|
||||
self.maxVisibleRows = maxVisibleRows
|
||||
|
||||
self.columnSpacing = columnSpacing
|
||||
self.emptyPlaceholder = emptyPlaceholder
|
||||
}
|
||||
@@ -187,7 +183,6 @@ private struct _TableCore<Value: Identifiable & Sendable>: View, Renderable wher
|
||||
let selectionMode: SelectionMode
|
||||
let focusID: String?
|
||||
let isDisabled: Bool
|
||||
let maxVisibleRows: Int?
|
||||
let emptyPlaceholder: String
|
||||
let columnSpacing: Int
|
||||
|
||||
@@ -220,7 +215,7 @@ private struct _TableCore<Value: Identifiable & Sendable>: View, Renderable wher
|
||||
} else {
|
||||
// Calculate viewport height
|
||||
let availableHeight = context.availableHeight
|
||||
let viewportHeight = maxVisibleRows ?? max(1, availableHeight - 6) // Reserve for border + header + indicators
|
||||
let viewportHeight = max(1, availableHeight - 6) // Reserve for border + header + indicators
|
||||
|
||||
// Get or create persistent focusID
|
||||
let focusIDKey = StateStorage.StateKey(identity: context.identity, propertyIndex: 1)
|
||||
|
||||
@@ -51,8 +51,7 @@ struct ListPage: View {
|
||||
HStack(spacing: 2) {
|
||||
List(
|
||||
"Single Selection",
|
||||
selection: $singleSelection,
|
||||
maxVisibleRows: 6
|
||||
selection: $singleSelection
|
||||
) {
|
||||
ForEach(FileItem.sampleFiles) { file in
|
||||
HStack(spacing: 1) {
|
||||
@@ -64,8 +63,7 @@ struct ListPage: View {
|
||||
|
||||
List(
|
||||
"Multi Selection",
|
||||
selection: $multiSelection,
|
||||
maxVisibleRows: 6
|
||||
selection: $multiSelection
|
||||
) {
|
||||
ForEach(FileItem.sampleFiles) { file in
|
||||
HStack(spacing: 1) {
|
||||
|
||||
@@ -54,8 +54,7 @@ struct TablePage: View {
|
||||
.foregroundStyle(.palette.foregroundSecondary)
|
||||
Table(
|
||||
FileEntry.sampleFiles,
|
||||
selection: $singleSelection,
|
||||
maxVisibleRows: 6
|
||||
selection: $singleSelection
|
||||
) {
|
||||
TableColumn("Name", value: \FileEntry.name)
|
||||
TableColumn("Size", value: \FileEntry.size)
|
||||
@@ -71,8 +70,7 @@ struct TablePage: View {
|
||||
.foregroundStyle(.palette.foregroundSecondary)
|
||||
Table(
|
||||
FileEntry.sampleFiles,
|
||||
selection: $multiSelection,
|
||||
maxVisibleRows: 4
|
||||
selection: $multiSelection
|
||||
) {
|
||||
TableColumn("Name", value: \FileEntry.name)
|
||||
TableColumn("Type", value: \FileEntry.type)
|
||||
|
||||
@@ -132,12 +132,11 @@ struct ListRenderingTests {
|
||||
|
||||
@Test("Scroll indicators appear when needed")
|
||||
func scrollIndicatorsAppear() {
|
||||
let context = createTestContext(height: 5)
|
||||
|
||||
struct Item: Identifiable {
|
||||
let id: Int
|
||||
let name: String
|
||||
}
|
||||
// Create list with more items than will fit in available height
|
||||
let items = (0..<20).map { Item(id: $0, name: "Item \($0)") }
|
||||
|
||||
var selection: Int?
|
||||
@@ -145,18 +144,19 @@ struct ListRenderingTests {
|
||||
selection: Binding(
|
||||
get: { selection },
|
||||
set: { selection = $0 }
|
||||
),
|
||||
maxVisibleRows: 3
|
||||
)
|
||||
) {
|
||||
ForEach(items) { item in
|
||||
Text(item.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Use a small height context so scrolling is triggered
|
||||
let context = createTestContext(width: 40, height: 8)
|
||||
let buffer = renderToBuffer(list, context: context)
|
||||
let content = buffer.lines.joined()
|
||||
|
||||
// Should have "more below" indicator
|
||||
// Should have "more below" indicator since we have 20 items in height 8
|
||||
#expect(content.contains("▼") || content.contains("more below"))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user