1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! View rendering functions for list components.
//!
//! This module handles all visual rendering aspects of the list including:
//! - Header rendering (title or filter input)
//! - Item rendering with viewport management
//! - Footer rendering (status bar and help)
//! - Complete view composition
use super::types::{FilterState, Item};
use super::Model;
impl<I: Item + Send + Sync + 'static> Model<I> {
/// Renders the list header based on the current filtering state.
///
/// The header changes based on the current state:
/// - **Unfiltered**: Shows the list title
/// - **Filtering**: Shows the filter input interface ("Filter: > ___")
/// - **FilterApplied**: Shows title with filtered item count ("Title (filtered: N)")
///
/// # Returns
///
/// A styled string containing the appropriate header content.
pub(super) fn view_header(&self) -> String {
let mut sections = Vec::new();
if self.filter_state == FilterState::Filtering {
// Show filter input interface when actively filtering
sections.push(format!("Filter: {}", self.filter_input.view()));
} else if self.show_title {
let mut title = self.title.clone();
// When a filter is applied, show the number of matched items
if self.filter_state == FilterState::FilterApplied {
let filter_info = format!(" ({} matched)", self.len());
title = format!("{}{}", title, filter_info);
}
// Add styled title
sections.push(self.styles.title.render(&title));
}
// Add status bar in header position (like Go version) when not filtering
if self.show_status_bar && self.filter_state != FilterState::Filtering {
let status = self.view_status_line();
if !status.is_empty() {
// Apply status bar style with proper padding (matching Go version)
sections.push(self.styles.status_bar.render(&status));
}
}
sections.join("\n")
}
/// Renders the visible items section of the list.
///
/// This method handles the core item rendering logic including:
/// - Viewport-based item selection for display
/// - Delegate-based item rendering with original indices
/// - Empty state handling
/// - Proper spacing and layout
///
/// ## Viewport Management
///
/// The rendering system uses viewport-based scrolling to show only the items
/// that fit within the available display space. Items are rendered starting
/// from `viewport_start` and continuing until the display area is filled or
/// all items are shown.
///
/// ## Index Semantics
///
/// **CRITICAL**: The delegate's `render` method receives the *original* item index
/// from the full items list, not a viewport-relative or filtered-relative index.
/// This design ensures that:
/// - Cursor highlighting works correctly (`index == m.cursor`)
/// - Filter highlighting can find matches by searching filtered_items
/// - Navigation state remains consistent across viewport changes
///
/// ## Empty State Display
///
/// When no items are available (either empty list or no filter matches),
/// displays an appropriate message styled with the list's no-items style.
///
/// # Returns
///
/// A formatted string containing all visible items with proper styling and spacing.
pub(super) fn view_items(&self) -> String {
if self.is_empty() {
return self.styles.no_items.render("No items.");
}
// Calculate how many items can fit in the viewport
let item_height = self.delegate.height() + self.delegate.spacing();
if item_height == 0 {
return String::new();
}
// Calculate available height for items using the same logic as update_pagination()
let mut header_height = 0;
if self.show_title {
header_height += self.calculate_element_height("title");
}
if self.show_status_bar {
header_height += self.calculate_element_height("status_bar");
}
let mut footer_height = 0;
if self.show_help {
footer_height += self.calculate_element_height("help");
}
if self.show_pagination {
footer_height += self.calculate_element_height("pagination");
}
let available_height = self.height.saturating_sub(header_height + footer_height);
let max_visible_items = (available_height / item_height).max(1);
// Determine which items to render based on viewport position
let items_to_render: Vec<(usize, &I)> = if self.filter_state == FilterState::Unfiltered {
// For unfiltered lists, use original items with their indices
self.items
.iter()
.enumerate()
.skip(self.viewport_start)
.take(max_visible_items)
.collect()
} else {
// For filtered lists, use filtered items but preserve original indices
self.filtered_items
.iter()
.skip(self.viewport_start)
.take(max_visible_items)
.map(|fi| (fi.index, &fi.item))
.collect()
};
// Render each visible item using the delegate
let mut rendered_items = Vec::new();
for (original_index, item) in items_to_render {
let rendered = self.delegate.render(self, original_index, item);
if !rendered.is_empty() {
rendered_items.push(rendered);
// Add spacing between items if specified by delegate
let spacing = self.delegate.spacing();
for _ in 0..spacing {
rendered_items.push(String::new());
}
}
}
// Join items with newlines, removing any trailing empty lines from spacing
let mut result = rendered_items.join("\n");
while result.ends_with('\n') {
result.pop();
}
result
}
/// Renders the status line for header display (matching Go version layout).
///
/// This creates a simple status line showing item counts that appears in the header
/// area between the title and items, just like the Go version.
///
/// # Returns
///
/// A formatted string containing just the item count status.
pub(super) fn view_status_line(&self) -> String {
let total_items = self.items.len();
let visible_items = self.len();
// Simple item count status (like Go version)
let singular = self.status_item_singular.as_deref().unwrap_or("item");
let plural = self.status_item_plural.as_deref().unwrap_or("items");
let noun = if visible_items == 1 { singular } else { plural };
if total_items == 0 {
"No items".to_string()
} else if self.is_empty() && self.filter_state == FilterState::FilterApplied {
"Nothing matched".to_string()
} else if self.filter_state == FilterState::FilterApplied {
// When filtering, show filter query and results like Go
let query = self.filter_input.value();
let num_filtered = total_items.saturating_sub(visible_items);
if !query.is_empty() && num_filtered > 0 {
format!(
"\"{}\" {} {} • {} filtered",
query, visible_items, noun, num_filtered
)
} else if !query.is_empty() {
format!("\"{}\" {} {}", query, visible_items, noun)
} else {
format!("{} {}", visible_items, noun)
}
} else {
// Normal unfiltered state - simple count
format!("{} {}", visible_items, noun)
}
}
/// Renders the footer containing only help information (matching Go version layout).
///
/// The footer now only includes help text, as status information has been moved
/// to the header area to match the Go version's layout.
///
/// The help content automatically adapts to the current filtering state,
/// showing relevant key bindings for the user's current context.
///
/// # Returns
///
/// A formatted string containing just the help information,
/// or an empty string if help is disabled.
pub(super) fn view_footer(&self) -> String {
if !self.show_help {
return String::new();
}
let help_content = self.help.view(self);
self.styles.help_style.render(&help_content)
}
}