bubbletea_widgets/list/model.rs
1//! Core Model struct and fundamental functionality.
2//!
3//! This module contains the primary Model struct definition and its core methods
4//! including construction, basic state management, and essential accessors.
5
6use super::keys::ListKeyMap;
7use super::style::ListStyles;
8use super::types::{FilterState, FilteredItem, Item, ItemDelegate};
9use crate::{help, paginator, spinner, textinput};
10
11/// A flexible, interactive list component with filtering, pagination, and customizable rendering.
12///
13/// The `Model<I>` is the main list component that can display any items implementing the `Item` trait.
14/// It provides fuzzy filtering, keyboard navigation, viewport scrolling, help integration, and
15/// customizable styling through delegates.
16///
17/// # Features
18///
19/// - **Fuzzy filtering**: Real-time search with character-level highlighting
20/// - **Smooth scrolling**: Viewport-based navigation that maintains context
21/// - **Customizable rendering**: Delegate pattern for complete visual control
22/// - **Keyboard navigation**: Vim-style keys plus standard arrow navigation
23/// - **Contextual help**: Automatic help text generation from key bindings
24/// - **Responsive design**: Adapts to different terminal sizes
25/// - **State management**: Clean separation of filtering, selection, and view states
26///
27/// # Architecture
28///
29/// The list uses a viewport-based scrolling system that maintains smooth navigation
30/// context instead of discrete page jumps. Items are rendered using delegates that
31/// control appearance and behavior, while filtering uses fuzzy matching with
32/// character-level highlighting for search results.
33///
34/// # Navigation
35///
36/// - **Up/Down**: Move cursor through items with smooth viewport scrolling
37/// - **Page Up/Down**: Jump by pages while maintaining cursor visibility
38/// - **Home/End**: Jump to first/last item
39/// - **/** : Start filtering
40/// - **Enter**: Accept filter (while filtering)
41/// - **Escape**: Cancel filter (while filtering)
42/// - **Ctrl+C**: Clear active filter
43///
44/// # Filtering
45///
46/// The list supports fuzzy filtering with real-time preview:
47/// - Type "/" to start filtering
48/// - Type characters to filter items in real-time
49/// - Matched characters are highlighted in the results
50/// - Press Enter to accept the filter or Escape to cancel
51///
52/// # Styling
53///
54/// Visual appearance is controlled through the `ListStyles` struct and item delegates.
55/// The list adapts to light/dark terminal themes automatically and supports
56/// customizable colors, borders, and typography.
57///
58/// # Examples
59///
60/// ```
61/// use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
62///
63/// let items = vec![
64/// DefaultItem::new("Task 1", "Complete documentation"),
65/// DefaultItem::new("Task 2", "Review pull requests"),
66/// ];
67/// let delegate = DefaultDelegate::new();
68/// let list = Model::new(items, delegate, 80, 24);
69/// ```
70///
71/// ## With Custom Items
72///
73/// ```
74/// use bubbletea_widgets::list::{Item, Model, DefaultDelegate};
75/// use std::fmt::Display;
76///
77/// #[derive(Clone)]
78/// struct CustomItem {
79/// title: String,
80/// priority: u8,
81/// }
82///
83/// impl Display for CustomItem {
84/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85/// write!(f, "[{}] {}", self.priority, self.title)
86/// }
87/// }
88///
89/// impl Item for CustomItem {
90/// fn filter_value(&self) -> String {
91/// format!("{} priority:{}", self.title, self.priority)
92/// }
93/// }
94///
95/// let items = vec![
96/// CustomItem { title: "Fix bug".to_string(), priority: 1 },
97/// CustomItem { title: "Add feature".to_string(), priority: 2 },
98/// ];
99/// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
100/// ```
101pub struct Model<I: Item> {
102 pub(super) title: String,
103 pub(super) items: Vec<I>,
104 pub(super) delegate: Box<dyn ItemDelegate<I> + Send + Sync>,
105
106 // Pagination
107 pub(super) paginator: paginator::Model,
108 pub(super) per_page: usize,
109
110 // UI State
111 pub(super) show_title: bool,
112 #[allow(dead_code)]
113 pub(super) spinner: spinner::Model,
114 pub(super) show_spinner: bool,
115 pub(super) width: usize,
116 pub(super) height: usize,
117 pub(super) styles: ListStyles,
118
119 // Status bar
120 pub(super) show_status_bar: bool,
121 #[allow(dead_code)]
122 pub(super) status_message_lifetime: usize,
123 pub(super) status_item_singular: Option<String>,
124 pub(super) status_item_plural: Option<String>,
125
126 // Pagination display
127 pub(super) show_pagination: bool,
128
129 // Help
130 pub(super) help: help::Model,
131 pub(super) show_help: bool,
132 pub(super) keymap: ListKeyMap,
133
134 // State
135 pub(super) filter_state: FilterState,
136 pub(super) filtered_items: Vec<FilteredItem<I>>,
137 pub(super) cursor: usize,
138 /// First visible item index for smooth scrolling.
139 ///
140 /// This field tracks the index of the first item visible in the current viewport.
141 /// It enables smooth, context-preserving scrolling behavior instead of discrete
142 /// page jumps. The viewport scrolls automatically when the cursor moves outside
143 /// the visible area, maintaining visual continuity.
144 pub(super) viewport_start: usize,
145
146 // Filter
147 pub(super) filter_input: textinput::Model,
148}
149
150impl<I: Item + Send + Sync + 'static> Model<I> {
151 /// Creates a new list with the provided items, delegate, and dimensions.
152 ///
153 /// This is the primary constructor for creating a list component. The delegate
154 /// controls how items are rendered and behave, while the dimensions determine
155 /// the initial size for layout calculations.
156 ///
157 /// # Arguments
158 ///
159 /// * `items` - Vector of items to display in the list
160 /// * `delegate` - Item delegate that controls rendering and behavior
161 /// * `width` - Initial width in terminal columns (can be updated later)
162 /// * `height` - Initial height in terminal rows (affects pagination)
163 ///
164 /// # Returns
165 ///
166 /// A new `Model<I>` configured with default settings:
167 /// - Title set to "List"
168 /// - 10 items per page
169 /// - Cursor at position 0
170 /// - All items initially visible (no filtering)
171 /// - Status bar enabled with default item names
172 ///
173 /// # Examples
174 ///
175 /// ```
176 /// use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
177 ///
178 /// let items = vec![
179 /// DefaultItem::new("First", "Description 1"),
180 /// DefaultItem::new("Second", "Description 2"),
181 /// ];
182 ///
183 /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
184 /// assert_eq!(list.len(), 2);
185 /// ```
186 pub fn new<D>(items: Vec<I>, delegate: D, width: usize, height: usize) -> Self
187 where
188 D: ItemDelegate<I> + Send + Sync + 'static,
189 {
190 let styles = ListStyles::default();
191 let mut paginator = paginator::Model::new();
192 let per_page = 10;
193 paginator.set_per_page(per_page);
194 paginator.set_total_items(items.len());
195
196 // Set dots mode by default (like Go version) and apply styled dots
197 paginator.paginator_type = paginator::Type::Dots;
198 paginator.active_dot = styles.active_pagination_dot.render("");
199 paginator.inactive_dot = styles.inactive_pagination_dot.render("");
200
201 let mut list = Self {
202 title: "List".to_string(),
203 items,
204 delegate: Box::new(delegate),
205 paginator,
206 per_page,
207 show_title: true,
208 spinner: spinner::new(&[]),
209 show_spinner: false,
210 width,
211 height,
212 styles,
213 show_status_bar: true,
214 status_message_lifetime: 1,
215 status_item_singular: None,
216 status_item_plural: None,
217 show_pagination: true,
218 help: help::Model::new(),
219 show_help: true,
220 keymap: ListKeyMap::default(),
221 filter_state: FilterState::Unfiltered,
222 filtered_items: vec![],
223 cursor: 0,
224 viewport_start: 0,
225 filter_input: textinput::new(),
226 };
227
228 // Calculate the actual pagination based on the provided height
229 list.update_pagination();
230 list
231 }
232
233 /// Sets the items displayed in the list.
234 ///
235 /// This method replaces all current items with the provided vector.
236 /// The cursor is reset to position 0, and pagination is recalculated
237 /// based on the new item count.
238 ///
239 /// # Arguments
240 ///
241 /// * `items` - Vector of new items to display
242 ///
243 /// # Examples
244 ///
245 /// ```
246 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
247 /// let mut list = Model::new(vec![], DefaultDelegate::new(), 80, 24);
248 ///
249 /// let items = vec![
250 /// DefaultItem::new("Apple", "Red fruit"),
251 /// DefaultItem::new("Banana", "Yellow fruit"),
252 /// ];
253 /// list.set_items(items);
254 /// assert_eq!(list.len(), 2);
255 /// ```
256 pub fn set_items(&mut self, items: Vec<I>) {
257 self.items = items;
258 self.cursor = 0;
259 self.update_pagination();
260 }
261
262 /// Returns a vector of currently visible items.
263 ///
264 /// The returned items reflect the current filtering state:
265 /// - When unfiltered: returns all items
266 /// - When filtered: returns only items matching the current filter
267 ///
268 /// # Returns
269 ///
270 /// A vector containing clones of all currently visible items.
271 ///
272 /// # Examples
273 ///
274 /// ```
275 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
276 /// let items = vec![
277 /// DefaultItem::new("First", "Description 1"),
278 /// DefaultItem::new("Second", "Description 2"),
279 /// ];
280 ///
281 /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
282 /// let visible = list.visible_items();
283 /// assert_eq!(visible.len(), 2);
284 /// ```
285 pub fn visible_items(&self) -> Vec<I> {
286 if self.filter_state == FilterState::Unfiltered {
287 self.items.clone()
288 } else {
289 self.filtered_items
290 .iter()
291 .map(|fi| fi.item.clone())
292 .collect()
293 }
294 }
295
296 /// Sets the filter text without applying the filter.
297 ///
298 /// This method updates the filter input text but does not trigger
299 /// the filtering process. It's primarily used for programmatic
300 /// filter setup or testing.
301 ///
302 /// # Arguments
303 ///
304 /// * `s` - The filter text to set
305 ///
306 /// # Examples
307 ///
308 /// ```
309 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
310 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
311 /// list.set_filter_text("search term");
312 /// // Filter text is set but not applied until filtering is activated
313 /// ```
314 pub fn set_filter_text(&mut self, s: &str) {
315 self.filter_input.set_value(s);
316 }
317
318 /// Sets the current filtering state.
319 ///
320 /// This method directly controls the list's filtering state without
321 /// triggering filter application. It's useful for programmatic state
322 /// management or testing specific filter conditions.
323 ///
324 /// # Arguments
325 ///
326 /// * `st` - The new filter state to set
327 ///
328 /// # Examples
329 ///
330 /// ```
331 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem, FilterState};
332 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
333 /// list.set_filter_state(FilterState::Filtering);
334 /// // List is now in filtering mode
335 /// ```
336 pub fn set_filter_state(&mut self, st: FilterState) {
337 self.filter_state = st;
338 }
339
340 /// Sets custom singular and plural names for status bar items.
341 ///
342 /// The status bar displays item counts using these names. If not set,
343 /// defaults to "item" and "items".
344 ///
345 /// # Arguments
346 ///
347 /// * `singular` - Name for single item (e.g., "task")
348 /// * `plural` - Name for multiple items (e.g., "tasks")
349 ///
350 /// # Examples
351 ///
352 /// ```
353 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
354 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
355 /// list.set_status_bar_item_name("task", "tasks");
356 /// // Status bar will now show "1 task" or "5 tasks"
357 /// ```
358 pub fn set_status_bar_item_name(&mut self, singular: &str, plural: &str) {
359 self.status_item_singular = Some(singular.to_string());
360 self.status_item_plural = Some(plural.to_string());
361 }
362
363 /// Updates the list dimensions and recalculates layout.
364 ///
365 /// This method allows dynamic resizing of the list to match terminal
366 /// size changes, similar to the Go bubbles list's `SetSize` method.
367 /// It updates both width and height, then recalculates pagination
368 /// to show the appropriate number of items.
369 ///
370 /// # Arguments
371 ///
372 /// * `width` - New width in terminal columns
373 /// * `height` - New height in terminal rows
374 ///
375 /// # Examples
376 ///
377 /// ```
378 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
379 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
380 ///
381 /// // Resize list to match new terminal size
382 /// list.set_size(100, 30);
383 /// assert_eq!(list.width(), 100);
384 /// assert_eq!(list.height(), 30);
385 /// ```
386 pub fn set_size(&mut self, width: usize, height: usize) {
387 self.width = width;
388 self.height = height;
389 self.update_pagination(); // Recalculate items per page
390 }
391
392 /// Returns the current width of the list.
393 ///
394 /// # Returns
395 ///
396 /// The current width in terminal columns.
397 pub fn width(&self) -> usize {
398 self.width
399 }
400
401 /// Returns the current height of the list.
402 ///
403 /// # Returns
404 ///
405 /// The current height in terminal rows.
406 pub fn height(&self) -> usize {
407 self.height
408 }
409
410 /// Returns the current items per page setting.
411 ///
412 /// # Returns
413 ///
414 /// The number of items displayed per page.
415 pub fn per_page(&self) -> usize {
416 self.per_page
417 }
418
419 /// Returns the total number of pages in the paginator.
420 ///
421 /// # Returns
422 ///
423 /// The total number of pages based on item count and per_page setting.
424 pub fn total_pages(&self) -> usize {
425 self.paginator.total_pages
426 }
427
428 /// Calculates the actual rendered height of UI elements based on their known configurations.
429 ///
430 /// This method mimics Go's `lipgloss.Height()` function by using the known style
431 /// configurations to determine how many terminal lines each element will actually occupy
432 /// when rendered, matching the default padding values set in style.rs.
433 ///
434 /// # Arguments
435 ///
436 /// * `element` - A string identifier for the UI element type
437 ///
438 /// # Returns
439 ///
440 /// The total number of terminal lines this element will occupy.
441 pub fn calculate_element_height(&self, element: &str) -> usize {
442 match element {
443 "title" => {
444 // title: .padding(0, 1, 1, 2) = 1 content + 0 top + 1 bottom = 2 lines
445 2
446 }
447 "status_bar" => {
448 // status_bar: .padding(0, 0, 1, 2) = 1 content + 0 top + 1 bottom = 2 lines
449 2
450 }
451 "pagination" => {
452 // pagination_style: .padding_left(2) = 1 content + 0 top + 0 bottom = 1 line
453 1
454 }
455 "help" => {
456 // help_style: .padding(1, 0, 0, 2) = 1 content + 1 top + 0 bottom = 2 lines
457 2
458 }
459 _ => 1, // Default fallback
460 }
461 }
462
463 /// Updates pagination settings based on current item count and page size.
464 ///
465 /// This method recalculates pagination after changes to item count or
466 /// page size. It's called automatically after operations that affect
467 /// the visible item count.
468 pub(super) fn update_pagination(&mut self) {
469 let total = self.len();
470 self.paginator.set_total_items(total);
471
472 // Calculate how many items can fit in the available height
473 if self.height > 0 {
474 let item_height = self.delegate.height() + self.delegate.spacing();
475
476 // Calculate actual header height based on styles (matching Go's lipgloss.Height() approach)
477 let mut header_height = 0;
478 if self.show_title {
479 header_height += self.calculate_element_height("title");
480 }
481 if self.show_status_bar {
482 header_height += self.calculate_element_height("status_bar");
483 }
484
485 // Calculate actual footer height based on styles
486 let mut footer_height = 0;
487 if self.show_help {
488 footer_height += self.calculate_element_height("help");
489 }
490 if self.show_pagination {
491 footer_height += self.calculate_element_height("pagination");
492 }
493
494 let available_height = self.height.saturating_sub(header_height + footer_height);
495 let items_per_page = if item_height > 0 {
496 (available_height / item_height).max(1)
497 } else {
498 5 // Match Go version default
499 };
500
501 self.per_page = items_per_page;
502 self.paginator.set_per_page(items_per_page);
503 // Recalculate total pages with the new per_page value
504 self.paginator.set_total_items(self.len());
505 }
506 }
507
508 /// Returns the number of currently visible items.
509 ///
510 /// This count reflects the items actually visible to the user:
511 /// - When unfiltered: returns the total number of items
512 /// - When filtering is active: returns only the count of matching items
513 ///
514 /// # Returns
515 ///
516 /// The number of items currently visible in the list.
517 ///
518 /// # Examples
519 ///
520 /// ```
521 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
522 /// let items = vec![
523 /// DefaultItem::new("Apple", "Red"),
524 /// DefaultItem::new("Banana", "Yellow"),
525 /// ];
526 /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
527 /// assert_eq!(list.len(), 2);
528 /// ```
529 pub fn len(&self) -> usize {
530 if self.filter_state == FilterState::Unfiltered {
531 self.items.len()
532 } else {
533 self.filtered_items.len()
534 }
535 }
536
537 /// Returns whether the list has no visible items.
538 ///
539 /// # Returns
540 ///
541 /// `true` if there are no currently visible items, `false` otherwise.
542 ///
543 /// # Examples
544 ///
545 /// ```
546 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
547 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
548 /// assert!(list.is_empty());
549 /// ```
550 pub fn is_empty(&self) -> bool {
551 self.len() == 0
552 }
553
554 /// Returns a reference to the currently selected item.
555 ///
556 /// The selected item is the one at the current cursor position. If the list
557 /// is empty or the cursor is out of bounds, returns `None`.
558 ///
559 /// # Returns
560 ///
561 /// A reference to the selected item, or `None` if no valid selection exists.
562 ///
563 /// # Examples
564 ///
565 /// ```
566 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
567 /// let items = vec![
568 /// DefaultItem::new("First", "Description 1"),
569 /// DefaultItem::new("Second", "Description 2"),
570 /// ];
571 /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
572 ///
573 /// if let Some(selected) = list.selected_item() {
574 /// println!("Selected: {}", selected);
575 /// }
576 /// ```
577 pub fn selected_item(&self) -> Option<&I> {
578 if self.filter_state == FilterState::Unfiltered {
579 self.items.get(self.cursor)
580 } else {
581 self.filtered_items.get(self.cursor).map(|fi| &fi.item)
582 }
583 }
584
585 /// Returns the current cursor position.
586 ///
587 /// The cursor represents the currently selected item index within the
588 /// visible (possibly filtered) list. This is always relative to the
589 /// currently visible items, not the original full list.
590 ///
591 /// # Returns
592 ///
593 /// The zero-based index of the currently selected item.
594 ///
595 /// # Examples
596 ///
597 /// ```
598 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
599 /// let items = vec![
600 /// DefaultItem::new("First", "Description"),
601 /// DefaultItem::new("Second", "Description"),
602 /// ];
603 /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
604 /// assert_eq!(list.cursor(), 0); // Initially at first item
605 /// ```
606 pub fn cursor(&self) -> usize {
607 self.cursor
608 }
609
610 /// Returns fuzzy match character indices for a given original item index.
611 ///
612 /// This method finds the character positions that matched the current filter
613 /// for a specific item identified by its original index in the full items list.
614 /// These indices can be used for character-level highlighting in custom delegates.
615 ///
616 /// # Arguments
617 ///
618 /// * `original_index` - The original index of the item in the full items list
619 ///
620 /// # Returns
621 ///
622 /// A reference to the vector of character indices that matched the filter,
623 /// or `None` if no matches exist for this item or if filtering is not active.
624 ///
625 /// # Examples
626 ///
627 /// ```
628 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
629 /// let items = vec![DefaultItem::new("Apple", "Red fruit")];
630 /// let mut list = Model::new(items, DefaultDelegate::new(), 80, 24);
631 ///
632 /// // Apply a filter first
633 /// list.set_filter_text("app");
634 /// // In a real application, this would be done through user interaction
635 ///
636 /// if let Some(matches) = list.matches_for_original_item(0) {
637 /// // matches contains the character indices that matched "app" in "Apple"
638 /// println!("Matched characters at indices: {:?}", matches);
639 /// }
640 /// ```
641 pub fn matches_for_original_item(&self, original_index: usize) -> Option<&Vec<usize>> {
642 self.filtered_items
643 .iter()
644 .find(|fi| fi.index == original_index)
645 .map(|fi| &fi.matches)
646 }
647
648 // === Builder Pattern Methods ===
649
650 /// Sets the list title (builder pattern).
651 ///
652 /// The title is displayed at the top of the list when not filtering.
653 /// During filtering, the title is replaced with the filter input interface.
654 ///
655 /// # Arguments
656 ///
657 /// * `title` - The new title for the list
658 ///
659 /// # Returns
660 ///
661 /// Self, for method chaining.
662 ///
663 /// # Examples
664 ///
665 /// ```
666 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
667 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
668 /// .with_title("My Tasks");
669 /// ```
670 pub fn with_title(mut self, title: &str) -> Self {
671 self.title = title.to_string();
672 self
673 }
674
675 /// Sets pagination display visibility (builder pattern).
676 ///
677 /// # Arguments
678 ///
679 /// * `show` - `true` to show pagination, `false` to hide it
680 ///
681 /// # Returns
682 ///
683 /// Self, for method chaining.
684 ///
685 /// # Examples
686 ///
687 /// ```
688 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
689 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
690 /// .with_show_pagination(false);
691 /// assert!(!list.show_pagination());
692 /// ```
693 pub fn with_show_pagination(mut self, show: bool) -> Self {
694 self.show_pagination = show;
695 self
696 }
697
698 /// Sets the pagination type (builder pattern).
699 ///
700 /// # Arguments
701 ///
702 /// * `pagination_type` - The type of pagination to display
703 ///
704 /// # Returns
705 ///
706 /// Self, for method chaining.
707 ///
708 /// # Examples
709 ///
710 /// ```
711 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
712 /// # use bubbletea_widgets::paginator::Type;
713 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
714 /// .with_pagination_type(Type::Dots);
715 /// assert_eq!(list.pagination_type(), Type::Dots);
716 /// ```
717 pub fn with_pagination_type(mut self, pagination_type: paginator::Type) -> Self {
718 self.paginator.paginator_type = pagination_type;
719 self
720 }
721
722 /// Sets title display visibility (builder pattern).
723 ///
724 /// # Arguments
725 ///
726 /// * `show` - `true` to show title, `false` to hide it
727 ///
728 /// # Returns
729 ///
730 /// Self, for method chaining.
731 ///
732 /// # Examples
733 ///
734 /// ```
735 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
736 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
737 /// .with_show_title(false);
738 /// assert!(!list.show_title());
739 /// ```
740 pub fn with_show_title(mut self, show: bool) -> Self {
741 self.show_title = show;
742 self
743 }
744
745 /// Sets status bar display visibility (builder pattern).
746 ///
747 /// # Arguments
748 ///
749 /// * `show` - `true` to show status bar, `false` to hide it
750 ///
751 /// # Returns
752 ///
753 /// Self, for method chaining.
754 ///
755 /// # Examples
756 ///
757 /// ```
758 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
759 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
760 /// .with_show_status_bar(false);
761 /// assert!(!list.show_status_bar());
762 /// ```
763 pub fn with_show_status_bar(mut self, show: bool) -> Self {
764 self.show_status_bar = show;
765 self
766 }
767
768 /// Sets spinner display visibility (builder pattern).
769 ///
770 /// # Arguments
771 ///
772 /// * `show` - `true` to show spinner, `false` to hide it
773 ///
774 /// # Returns
775 ///
776 /// Self, for method chaining.
777 ///
778 /// # Examples
779 ///
780 /// ```
781 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
782 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
783 /// .with_show_spinner(true);
784 /// assert!(list.show_spinner());
785 /// ```
786 pub fn with_show_spinner(mut self, show: bool) -> Self {
787 self.show_spinner = show;
788 self
789 }
790
791 /// Sets help display visibility (builder pattern).
792 ///
793 /// # Arguments
794 ///
795 /// * `show` - `true` to show help, `false` to hide it
796 ///
797 /// # Returns
798 ///
799 /// Self, for method chaining.
800 ///
801 /// # Examples
802 ///
803 /// ```
804 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
805 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
806 /// .with_show_help(true);
807 /// assert!(list.show_help());
808 /// ```
809 pub fn with_show_help(mut self, show: bool) -> Self {
810 self.show_help = show;
811 self
812 }
813
814 /// Sets the list's styling configuration (builder pattern).
815 ///
816 /// This replaces all current styles with the provided configuration.
817 ///
818 /// # Arguments
819 ///
820 /// * `styles` - The styling configuration to apply
821 ///
822 /// # Returns
823 ///
824 /// Self, for method chaining.
825 ///
826 /// # Examples
827 ///
828 /// ```
829 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
830 /// # use bubbletea_widgets::list::style::ListStyles;
831 /// let custom_styles = ListStyles::default();
832 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24)
833 /// .with_styles(custom_styles);
834 /// ```
835 pub fn with_styles(mut self, styles: ListStyles) -> Self {
836 self.styles = styles;
837 self
838 }
839
840 // === UI Component Toggles and Access ===
841
842 /// Returns whether pagination is currently shown.
843 ///
844 /// # Returns
845 ///
846 /// `true` if pagination is displayed, `false` otherwise.
847 ///
848 /// # Examples
849 ///
850 /// ```
851 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
852 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
853 /// assert!(list.show_pagination()); // pagination is shown by default
854 /// ```
855 pub fn show_pagination(&self) -> bool {
856 self.show_pagination
857 }
858
859 /// Sets whether pagination should be displayed.
860 ///
861 /// When disabled, the pagination section will not be rendered in the list view,
862 /// but pagination state and navigation will continue to work normally.
863 ///
864 /// # Arguments
865 ///
866 /// * `show` - `true` to show pagination, `false` to hide it
867 ///
868 /// # Examples
869 ///
870 /// ```
871 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
872 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
873 /// list.set_show_pagination(false);
874 /// assert!(!list.show_pagination());
875 /// ```
876 pub fn set_show_pagination(&mut self, show: bool) {
877 self.show_pagination = show;
878 }
879
880 /// Toggles pagination display on/off.
881 ///
882 /// This is a convenience method that flips the current pagination display state.
883 ///
884 /// # Returns
885 ///
886 /// The new pagination display state after toggling.
887 ///
888 /// # Examples
889 ///
890 /// ```
891 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
892 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
893 /// let new_state = list.toggle_pagination();
894 /// assert_eq!(new_state, list.show_pagination());
895 /// ```
896 pub fn toggle_pagination(&mut self) -> bool {
897 self.show_pagination = !self.show_pagination;
898 self.show_pagination
899 }
900
901 /// Returns the current pagination type (Arabic or Dots).
902 ///
903 /// # Returns
904 ///
905 /// The pagination type currently configured for this list.
906 ///
907 /// # Examples
908 ///
909 /// ```
910 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
911 /// # use bubbletea_widgets::paginator::Type;
912 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
913 /// let pagination_type = list.pagination_type();
914 /// ```
915 pub fn pagination_type(&self) -> paginator::Type {
916 self.paginator.paginator_type
917 }
918
919 /// Sets the pagination display type.
920 ///
921 /// This controls how pagination is rendered:
922 /// - `paginator::Type::Arabic`: Shows "1/5" style numbering
923 /// - `paginator::Type::Dots`: Shows "• ○ • ○ •" style dots
924 ///
925 /// # Arguments
926 ///
927 /// * `pagination_type` - The type of pagination to display
928 ///
929 /// # Examples
930 ///
931 /// ```
932 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
933 /// # use bubbletea_widgets::paginator::Type;
934 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
935 /// list.set_pagination_type(Type::Dots);
936 /// assert_eq!(list.pagination_type(), Type::Dots);
937 /// ```
938 pub fn set_pagination_type(&mut self, pagination_type: paginator::Type) {
939 self.paginator.paginator_type = pagination_type;
940 }
941
942 // === Item Manipulation Methods ===
943
944 /// Inserts an item at the specified index.
945 ///
946 /// All items at and after the specified index are shifted to the right.
947 /// The cursor and pagination are updated appropriately.
948 ///
949 /// # Arguments
950 ///
951 /// * `index` - The position to insert the item at
952 /// * `item` - The item to insert
953 ///
954 /// # Panics
955 ///
956 /// Panics if `index > len()`.
957 ///
958 /// # Examples
959 ///
960 /// ```
961 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
962 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
963 /// list.insert_item(0, DefaultItem::new("First", "Description"));
964 /// assert_eq!(list.len(), 1);
965 /// ```
966 pub fn insert_item(&mut self, index: usize, item: I) {
967 self.items.insert(index, item);
968 // Clear any active filter since item indices have changed
969 if self.filter_state != FilterState::Unfiltered {
970 self.filter_state = FilterState::Unfiltered;
971 self.filtered_items.clear();
972 }
973 // Update cursor if needed to maintain current selection
974 if index <= self.cursor {
975 self.cursor = self
976 .cursor
977 .saturating_add(1)
978 .min(self.items.len().saturating_sub(1));
979 }
980 self.update_pagination();
981 }
982
983 /// Removes and returns the item at the specified index.
984 ///
985 /// All items after the specified index are shifted to the left.
986 /// The cursor and pagination are updated appropriately.
987 ///
988 /// # Arguments
989 ///
990 /// * `index` - The position to remove the item from
991 ///
992 /// # Returns
993 ///
994 /// The removed item.
995 ///
996 /// # Panics
997 ///
998 /// Panics if `index >= len()`.
999 ///
1000 /// # Examples
1001 ///
1002 /// ```
1003 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1004 /// let mut list = Model::new(
1005 /// vec![DefaultItem::new("First", "Desc")],
1006 /// DefaultDelegate::new(), 80, 24
1007 /// );
1008 /// let removed = list.remove_item(0);
1009 /// assert_eq!(list.len(), 0);
1010 /// ```
1011 pub fn remove_item(&mut self, index: usize) -> I {
1012 if index >= self.items.len() {
1013 panic!("Index out of bounds");
1014 }
1015
1016 // Check if the item can be removed
1017 if !self.delegate.can_remove(index, &self.items[index]) {
1018 panic!("Item cannot be removed");
1019 }
1020
1021 // Call the on_remove callback before removal
1022 let item_ref = &self.items[index];
1023 let _ = self.delegate.on_remove(index, item_ref);
1024
1025 let item = self.items.remove(index);
1026 // Clear any active filter since item indices have changed
1027 if self.filter_state != FilterState::Unfiltered {
1028 self.filter_state = FilterState::Unfiltered;
1029 self.filtered_items.clear();
1030 }
1031 // Update cursor to maintain valid position
1032 if !self.items.is_empty() {
1033 if index < self.cursor {
1034 self.cursor = self.cursor.saturating_sub(1);
1035 } else if self.cursor >= self.items.len() {
1036 self.cursor = self.items.len().saturating_sub(1);
1037 }
1038 } else {
1039 self.cursor = 0;
1040 }
1041 self.update_pagination();
1042 item
1043 }
1044
1045 /// Moves an item from one position to another.
1046 ///
1047 /// The item at `from_index` is removed and inserted at `to_index`.
1048 /// The cursor is updated to follow the moved item if it was selected.
1049 ///
1050 /// # Arguments
1051 ///
1052 /// * `from_index` - The current position of the item to move
1053 /// * `to_index` - The target position to move the item to
1054 ///
1055 /// # Panics
1056 ///
1057 /// Panics if either index is out of bounds.
1058 ///
1059 /// # Examples
1060 ///
1061 /// ```
1062 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1063 /// let mut list = Model::new(
1064 /// vec![
1065 /// DefaultItem::new("First", "1"),
1066 /// DefaultItem::new("Second", "2"),
1067 /// ],
1068 /// DefaultDelegate::new(), 80, 24
1069 /// );
1070 /// list.move_item(0, 1); // Move "First" to position 1
1071 /// ```
1072 pub fn move_item(&mut self, from_index: usize, to_index: usize) {
1073 if from_index >= self.items.len() || to_index >= self.items.len() {
1074 panic!("Index out of bounds");
1075 }
1076 if from_index == to_index {
1077 return; // No movement needed
1078 }
1079
1080 let item = self.items.remove(from_index);
1081 self.items.insert(to_index, item);
1082
1083 // Clear any active filter since item indices have changed
1084 if self.filter_state != FilterState::Unfiltered {
1085 self.filter_state = FilterState::Unfiltered;
1086 self.filtered_items.clear();
1087 }
1088
1089 // Update cursor to follow the moved item if it was selected
1090 if self.cursor == from_index {
1091 self.cursor = to_index;
1092 } else if from_index < self.cursor && to_index >= self.cursor {
1093 self.cursor = self.cursor.saturating_sub(1);
1094 } else if from_index > self.cursor && to_index <= self.cursor {
1095 self.cursor = self.cursor.saturating_add(1);
1096 }
1097
1098 self.update_pagination();
1099 }
1100
1101 /// Adds an item to the end of the list.
1102 ///
1103 /// This is equivalent to `insert_item(len(), item)`.
1104 ///
1105 /// # Arguments
1106 ///
1107 /// * `item` - The item to add
1108 ///
1109 /// # Examples
1110 ///
1111 /// ```
1112 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1113 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1114 /// list.push_item(DefaultItem::new("New Item", "Description"));
1115 /// assert_eq!(list.len(), 1);
1116 /// ```
1117 pub fn push_item(&mut self, item: I) {
1118 self.items.push(item);
1119 // Clear any active filter since item indices have changed
1120 if self.filter_state != FilterState::Unfiltered {
1121 self.filter_state = FilterState::Unfiltered;
1122 self.filtered_items.clear();
1123 }
1124 self.update_pagination();
1125 }
1126
1127 /// Removes and returns the last item from the list.
1128 ///
1129 /// # Returns
1130 ///
1131 /// The last item, or `None` if the list is empty.
1132 ///
1133 /// # Examples
1134 ///
1135 /// ```
1136 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1137 /// let mut list = Model::new(
1138 /// vec![DefaultItem::new("Item", "Desc")],
1139 /// DefaultDelegate::new(), 80, 24
1140 /// );
1141 /// let popped = list.pop_item();
1142 /// assert!(popped.is_some());
1143 /// assert_eq!(list.len(), 0);
1144 /// ```
1145 pub fn pop_item(&mut self) -> Option<I> {
1146 if self.items.is_empty() {
1147 return None;
1148 }
1149
1150 let item = self.items.pop();
1151 // Clear any active filter since item indices have changed
1152 if self.filter_state != FilterState::Unfiltered {
1153 self.filter_state = FilterState::Unfiltered;
1154 self.filtered_items.clear();
1155 }
1156 // Update cursor if it's now out of bounds
1157 if self.cursor >= self.items.len() && !self.items.is_empty() {
1158 self.cursor = self.items.len() - 1;
1159 } else if self.items.is_empty() {
1160 self.cursor = 0;
1161 }
1162 self.update_pagination();
1163 item
1164 }
1165
1166 /// Returns a reference to the underlying items collection.
1167 ///
1168 /// This provides read-only access to all items in the list,
1169 /// regardless of the current filtering state.
1170 ///
1171 /// # Returns
1172 ///
1173 /// A slice containing all items in the list.
1174 ///
1175 /// # Examples
1176 ///
1177 /// ```
1178 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1179 /// let items = vec![DefaultItem::new("First", "1"), DefaultItem::new("Second", "2")];
1180 /// let list = Model::new(items.clone(), DefaultDelegate::new(), 80, 24);
1181 /// assert_eq!(list.items().len(), 2);
1182 /// assert_eq!(list.items()[0].to_string(), items[0].to_string());
1183 /// ```
1184 pub fn items(&self) -> &[I] {
1185 &self.items
1186 }
1187
1188 /// Returns a mutable reference to the underlying items collection.
1189 ///
1190 /// This provides direct mutable access to the items. Note that after
1191 /// modifying items through this method, you should call `update_pagination()`
1192 /// to ensure pagination state remains consistent.
1193 ///
1194 /// **Warning**: Direct modification may invalidate the current filter state.
1195 /// Consider using the specific item manipulation methods instead.
1196 ///
1197 /// # Returns
1198 ///
1199 /// A mutable slice containing all items in the list.
1200 ///
1201 /// # Examples
1202 ///
1203 /// ```
1204 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1205 /// let mut list = Model::new(
1206 /// vec![DefaultItem::new("First", "1")],
1207 /// DefaultDelegate::new(), 80, 24
1208 /// );
1209 /// list.items_mut()[0] = DefaultItem::new("Modified", "Updated");
1210 /// assert_eq!(list.items()[0].to_string(), "Modified");
1211 /// ```
1212 pub fn items_mut(&mut self) -> &mut Vec<I> {
1213 // Clear filter state since items may be modified
1214 if self.filter_state != FilterState::Unfiltered {
1215 self.filter_state = FilterState::Unfiltered;
1216 self.filtered_items.clear();
1217 }
1218 &mut self.items
1219 }
1220
1221 /// Returns the total number of items in the list.
1222 ///
1223 /// This returns the count of all items, not just visible items.
1224 /// For visible items count, use `len()`.
1225 ///
1226 /// # Returns
1227 ///
1228 /// The total number of items in the underlying collection.
1229 ///
1230 /// # Examples
1231 ///
1232 /// ```
1233 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1234 /// let list = Model::new(
1235 /// vec![DefaultItem::new("1", ""), DefaultItem::new("2", "")],
1236 /// DefaultDelegate::new(), 80, 24
1237 /// );
1238 /// assert_eq!(list.items_len(), 2);
1239 /// ```
1240 pub fn items_len(&self) -> usize {
1241 self.items.len()
1242 }
1243
1244 /// Returns whether the underlying items collection is empty.
1245 ///
1246 /// This checks the total items count, not just visible items.
1247 /// For visible items check, use `is_empty()`.
1248 ///
1249 /// # Returns
1250 ///
1251 /// `true` if there are no items in the underlying collection, `false` otherwise.
1252 ///
1253 /// # Examples
1254 ///
1255 /// ```
1256 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1257 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1258 /// assert!(list.items_empty());
1259 /// ```
1260 pub fn items_empty(&self) -> bool {
1261 self.items.is_empty()
1262 }
1263
1264 // === UI Component Access and Styling ===
1265
1266 /// Returns whether the title is currently shown.
1267 ///
1268 /// # Returns
1269 ///
1270 /// `true` if the title is displayed, `false` otherwise.
1271 ///
1272 /// # Examples
1273 ///
1274 /// ```
1275 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1276 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1277 /// assert!(list.show_title()); // title is shown by default
1278 /// ```
1279 pub fn show_title(&self) -> bool {
1280 self.show_title
1281 }
1282
1283 /// Sets whether the title should be displayed.
1284 ///
1285 /// When disabled, the title section will not be rendered in the list view.
1286 ///
1287 /// # Arguments
1288 ///
1289 /// * `show` - `true` to show the title, `false` to hide it
1290 ///
1291 /// # Examples
1292 ///
1293 /// ```
1294 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1295 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1296 /// list.set_show_title(false);
1297 /// assert!(!list.show_title());
1298 /// ```
1299 pub fn set_show_title(&mut self, show: bool) {
1300 self.show_title = show;
1301 }
1302
1303 /// Toggles title display on/off.
1304 ///
1305 /// This is a convenience method that flips the current title display state.
1306 ///
1307 /// # Returns
1308 ///
1309 /// The new title display state after toggling.
1310 ///
1311 /// # Examples
1312 ///
1313 /// ```
1314 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1315 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1316 /// let new_state = list.toggle_title();
1317 /// assert_eq!(new_state, list.show_title());
1318 /// ```
1319 pub fn toggle_title(&mut self) -> bool {
1320 self.show_title = !self.show_title;
1321 self.show_title
1322 }
1323
1324 /// Returns whether the status bar is currently shown.
1325 ///
1326 /// # Returns
1327 ///
1328 /// `true` if the status bar is displayed, `false` otherwise.
1329 ///
1330 /// # Examples
1331 ///
1332 /// ```
1333 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1334 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1335 /// assert!(list.show_status_bar()); // status bar is shown by default
1336 /// ```
1337 pub fn show_status_bar(&self) -> bool {
1338 self.show_status_bar
1339 }
1340
1341 /// Sets whether the status bar should be displayed.
1342 ///
1343 /// When disabled, the status bar section will not be rendered in the list view.
1344 ///
1345 /// # Arguments
1346 ///
1347 /// * `show` - `true` to show the status bar, `false` to hide it
1348 ///
1349 /// # Examples
1350 ///
1351 /// ```
1352 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1353 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1354 /// list.set_show_status_bar(false);
1355 /// assert!(!list.show_status_bar());
1356 /// ```
1357 pub fn set_show_status_bar(&mut self, show: bool) {
1358 self.show_status_bar = show;
1359 }
1360
1361 /// Toggles status bar display on/off.
1362 ///
1363 /// This is a convenience method that flips the current status bar display state.
1364 ///
1365 /// # Returns
1366 ///
1367 /// The new status bar display state after toggling.
1368 ///
1369 /// # Examples
1370 ///
1371 /// ```
1372 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1373 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1374 /// let new_state = list.toggle_status_bar();
1375 /// assert_eq!(new_state, list.show_status_bar());
1376 /// ```
1377 pub fn toggle_status_bar(&mut self) -> bool {
1378 self.show_status_bar = !self.show_status_bar;
1379 self.show_status_bar
1380 }
1381
1382 /// Returns whether the spinner is currently shown.
1383 ///
1384 /// # Returns
1385 ///
1386 /// `true` if the spinner is displayed, `false` otherwise.
1387 ///
1388 /// # Examples
1389 ///
1390 /// ```
1391 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1392 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1393 /// assert!(!list.show_spinner()); // spinner is hidden by default
1394 /// ```
1395 pub fn show_spinner(&self) -> bool {
1396 self.show_spinner
1397 }
1398
1399 /// Sets whether the spinner should be displayed.
1400 ///
1401 /// When enabled, the spinner will be rendered as part of the list view,
1402 /// typically to indicate loading state.
1403 ///
1404 /// # Arguments
1405 ///
1406 /// * `show` - `true` to show the spinner, `false` to hide it
1407 ///
1408 /// # Examples
1409 ///
1410 /// ```
1411 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1412 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1413 /// list.set_show_spinner(true);
1414 /// assert!(list.show_spinner());
1415 /// ```
1416 pub fn set_show_spinner(&mut self, show: bool) {
1417 self.show_spinner = show;
1418 }
1419
1420 /// Toggles spinner display on/off.
1421 ///
1422 /// This is a convenience method that flips the current spinner display state.
1423 ///
1424 /// # Returns
1425 ///
1426 /// The new spinner display state after toggling.
1427 ///
1428 /// # Examples
1429 ///
1430 /// ```
1431 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1432 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1433 /// let new_state = list.toggle_spinner();
1434 /// assert_eq!(new_state, list.show_spinner());
1435 /// ```
1436 pub fn toggle_spinner(&mut self) -> bool {
1437 self.show_spinner = !self.show_spinner;
1438 self.show_spinner
1439 }
1440
1441 /// Returns a reference to the spinner model.
1442 ///
1443 /// This allows access to the underlying spinner for customization
1444 /// and state management.
1445 ///
1446 /// # Returns
1447 ///
1448 /// A reference to the spinner model.
1449 ///
1450 /// # Examples
1451 ///
1452 /// ```
1453 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1454 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1455 /// let spinner = list.spinner();
1456 /// ```
1457 pub fn spinner(&self) -> &spinner::Model {
1458 &self.spinner
1459 }
1460
1461 /// Returns a mutable reference to the spinner model.
1462 ///
1463 /// This allows modification of the underlying spinner for customization
1464 /// and state management.
1465 ///
1466 /// # Returns
1467 ///
1468 /// A mutable reference to the spinner model.
1469 ///
1470 /// # Examples
1471 ///
1472 /// ```
1473 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1474 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1475 /// let spinner = list.spinner_mut();
1476 /// ```
1477 pub fn spinner_mut(&mut self) -> &mut spinner::Model {
1478 &mut self.spinner
1479 }
1480
1481 /// Returns whether the help is currently shown.
1482 ///
1483 /// # Returns
1484 ///
1485 /// `true` if help is displayed, `false` otherwise.
1486 ///
1487 /// # Examples
1488 ///
1489 /// ```
1490 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1491 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1492 /// assert!(list.show_help()); // help is shown by default
1493 /// ```
1494 pub fn show_help(&self) -> bool {
1495 self.show_help
1496 }
1497
1498 /// Sets whether help should be displayed.
1499 ///
1500 /// When enabled, help text will be rendered as part of the list view,
1501 /// showing available key bindings and controls.
1502 ///
1503 /// # Arguments
1504 ///
1505 /// * `show` - `true` to show help, `false` to hide it
1506 ///
1507 /// # Examples
1508 ///
1509 /// ```
1510 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1511 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1512 /// list.set_show_help(true);
1513 /// assert!(list.show_help());
1514 /// ```
1515 pub fn set_show_help(&mut self, show: bool) {
1516 self.show_help = show;
1517 }
1518
1519 /// Toggles help display on/off.
1520 ///
1521 /// This is a convenience method that flips the current help display state.
1522 ///
1523 /// # Returns
1524 ///
1525 /// The new help display state after toggling.
1526 ///
1527 /// # Examples
1528 ///
1529 /// ```
1530 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1531 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1532 /// let new_state = list.toggle_help();
1533 /// assert_eq!(new_state, list.show_help());
1534 /// ```
1535 pub fn toggle_help(&mut self) -> bool {
1536 self.show_help = !self.show_help;
1537 self.show_help
1538 }
1539
1540 /// Returns a reference to the help model.
1541 ///
1542 /// This allows access to the underlying help system for customization
1543 /// and state management.
1544 ///
1545 /// # Returns
1546 ///
1547 /// A reference to the help model.
1548 ///
1549 /// # Examples
1550 ///
1551 /// ```
1552 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1553 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1554 /// let help = list.help();
1555 /// ```
1556 pub fn help(&self) -> &help::Model {
1557 &self.help
1558 }
1559
1560 /// Returns a mutable reference to the help model.
1561 ///
1562 /// This allows modification of the underlying help system for customization
1563 /// and state management.
1564 ///
1565 /// # Returns
1566 ///
1567 /// A mutable reference to the help model.
1568 ///
1569 /// # Examples
1570 ///
1571 /// ```
1572 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1573 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1574 /// let help = list.help_mut();
1575 /// ```
1576 pub fn help_mut(&mut self) -> &mut help::Model {
1577 &mut self.help
1578 }
1579
1580 /// Returns a reference to the list's styling configuration.
1581 ///
1582 /// This provides read-only access to all visual styles used by the list,
1583 /// including title, item, status bar, pagination, and help styles.
1584 ///
1585 /// # Returns
1586 ///
1587 /// A reference to the `ListStyles` configuration.
1588 ///
1589 /// # Examples
1590 ///
1591 /// ```
1592 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1593 /// let list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1594 /// let styles = list.styles();
1595 /// // Access specific styles, e.g., styles.title, styles.pagination_style
1596 /// ```
1597 pub fn styles(&self) -> &ListStyles {
1598 &self.styles
1599 }
1600
1601 /// Returns a mutable reference to the list's styling configuration.
1602 ///
1603 /// This provides direct mutable access to all visual styles used by the list.
1604 /// Changes to styles take effect immediately on the next render.
1605 ///
1606 /// # Returns
1607 ///
1608 /// A mutable reference to the `ListStyles` configuration.
1609 ///
1610 /// # Examples
1611 ///
1612 /// ```
1613 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1614 /// # use lipgloss_extras::prelude::*;
1615 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1616 /// let styles = list.styles_mut();
1617 /// styles.title = Style::new().foreground("#FF0000"); // Red title
1618 /// ```
1619 pub fn styles_mut(&mut self) -> &mut ListStyles {
1620 &mut self.styles
1621 }
1622
1623 /// Sets the list's styling configuration.
1624 ///
1625 /// This replaces all current styles with the provided configuration.
1626 /// Changes take effect immediately on the next render.
1627 ///
1628 /// # Arguments
1629 ///
1630 /// * `styles` - The new styling configuration to apply
1631 ///
1632 /// # Examples
1633 ///
1634 /// ```
1635 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1636 /// # use bubbletea_widgets::list::style::ListStyles;
1637 /// let mut list: Model<DefaultItem> = Model::new(vec![], DefaultDelegate::new(), 80, 24);
1638 /// let custom_styles = ListStyles::default();
1639 /// list.set_styles(custom_styles);
1640 /// ```
1641 pub fn set_styles(&mut self, styles: ListStyles) {
1642 // Update paginator dots from styled strings
1643 self.paginator.active_dot = styles.active_pagination_dot.render("");
1644 self.paginator.inactive_dot = styles.inactive_pagination_dot.render("");
1645 self.styles = styles;
1646 }
1647
1648 /// Renders the status bar as a formatted string.
1649 ///
1650 /// The status bar shows the current selection position and total item count,
1651 /// using the custom item names if set. The format is "X/Y items" where X is
1652 /// the current cursor position + 1, and Y is the total item count.
1653 ///
1654 /// # Returns
1655 ///
1656 /// A formatted status string, or empty string if status bar is disabled.
1657 ///
1658 /// # Examples
1659 ///
1660 /// ```
1661 /// # use bubbletea_widgets::list::{Model, DefaultDelegate, DefaultItem};
1662 /// let items = vec![
1663 /// DefaultItem::new("First", ""),
1664 /// DefaultItem::new("Second", ""),
1665 /// ];
1666 /// let list = Model::new(items, DefaultDelegate::new(), 80, 24);
1667 /// let status = list.status_view();
1668 /// assert!(status.contains("1/2"));
1669 /// ```
1670 pub fn status_view(&self) -> String {
1671 if !self.show_status_bar {
1672 return String::new();
1673 }
1674
1675 let mut footer = String::new();
1676 if !self.is_empty() {
1677 let singular = self.status_item_singular.as_deref().unwrap_or("item");
1678 let plural = self.status_item_plural.as_deref().unwrap_or("items");
1679 let noun = if self.len() == 1 { singular } else { plural };
1680 footer.push_str(&format!("{}/{} {}", self.cursor + 1, self.len(), noun));
1681 }
1682 let help_view = self.help.view(self);
1683 if !help_view.is_empty() {
1684 footer.push('\n');
1685 footer.push_str(&help_view);
1686 }
1687 footer
1688 }
1689
1690 // === Advanced Filtering API ===
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695 use super::*;
1696 use crate::list::{DefaultDelegate, DefaultItem};
1697
1698 #[test]
1699 fn test_pagination_calculation_fix() {
1700 // Test that our pagination calculation fix works correctly
1701 let items: Vec<DefaultItem> = (0..23)
1702 .map(|i| DefaultItem::new(&format!("Item {}", i), "Description"))
1703 .collect();
1704 let delegate = DefaultDelegate::new();
1705
1706 // Test different terminal heights like the user mentioned
1707 for terminal_height in [24, 30, 34] {
1708 let doc_margin = 2; // doc_style margin from the example
1709 let list_height = terminal_height - doc_margin;
1710
1711 let list = Model::new(items.clone(), delegate.clone(), 80, list_height)
1712 .with_title("Test List");
1713
1714 // Calculate expected values
1715 let title_height = list.calculate_element_height("title"); // 2
1716 let status_height = list.calculate_element_height("status_bar"); // 2
1717 let pagination_height = list.calculate_element_height("pagination"); // 1
1718 let help_height = list.calculate_element_height("help"); // 2
1719
1720 let header_height = title_height + status_height; // 4
1721 let footer_height = pagination_height + help_height; // 3
1722 let total_reserved = header_height + footer_height; // 7
1723 let available_height = list_height - total_reserved;
1724
1725 // DefaultDelegate uses 3 lines per item (2 content + 1 spacing)
1726 let delegate_item_height = 3;
1727 let expected_per_page = available_height / delegate_item_height;
1728 let expected_total_pages =
1729 (items.len() as f32 / expected_per_page as f32).ceil() as usize;
1730
1731 println!("Terminal height {}: list_height={}, available={}, expected_per_page={}, expected_pages={}, actual_per_page={}, actual_pages={}",
1732 terminal_height, list_height, available_height, expected_per_page, expected_total_pages, list.per_page(), list.total_pages());
1733
1734 assert_eq!(
1735 list.per_page(),
1736 expected_per_page,
1737 "Items per page mismatch for terminal height {}",
1738 terminal_height
1739 );
1740 assert_eq!(
1741 list.total_pages(),
1742 expected_total_pages,
1743 "Total pages mismatch for terminal height {}",
1744 terminal_height
1745 );
1746 }
1747 }
1748}