bubbletea_widgets/
viewport.rs

1//! Scrollable viewport component for displaying large content in terminal applications.
2//!
3//! This module provides a sophisticated viewport component that enables smooth scrolling
4//! through content that exceeds the available display area. It supports both vertical
5//! and horizontal scrolling, efficient rendering of large datasets, and comprehensive
6//! keyboard navigation with customizable key bindings.
7//!
8//! # Core Features
9//!
10//! - **Bidirectional Scrolling**: Smooth vertical and horizontal content navigation
11//! - **Efficient Rendering**: Only visible content is processed for optimal performance
12//! - **Vim-Style Navigation**: Familiar keyboard shortcuts with arrow key alternatives
13//! - **Content Management**: Support for both string and line-based content
14//! - **Styling Integration**: Full lipgloss styling support with frame calculations
15//! - **Mouse Support**: Configurable mouse wheel scrolling (when available)
16//! - **Position Tracking**: Precise scroll percentage and boundary detection
17//!
18//! # Quick Start
19//!
20//! ```rust
21//! use bubbletea_widgets::viewport::{new, Model};
22//!
23//! // Create a viewport with specific dimensions
24//! let mut viewport = new(80, 24);
25//!
26//! // Set content to display
27//! viewport.set_content("Line 1\nLine 2\nLine 3\nLine 4\nLine 5");
28//!
29//! // Navigate through content
30//! viewport.scroll_down(1);     // Scroll down one line
31//! viewport.page_down();        // Scroll down one page
32//! viewport.goto_bottom();      // Jump to end
33//!
34//! // Check current state
35//! let visible = viewport.visible_lines();
36//! let progress = viewport.scroll_percent();
37//! ```
38//!
39//! # Integration with Bubble Tea
40//!
41//! ```rust
42//! use bubbletea_widgets::viewport::{Model as ViewportModel, ViewportKeyMap};
43//! use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg};
44//! use lipgloss_extras::prelude::*;
45//!
46//! struct DocumentViewer {
47//!     viewport: ViewportModel,
48//!     content: String,
49//! }
50//!
51//! impl BubbleTeaModel for DocumentViewer {
52//!     fn init() -> (Self, Option<Cmd>) {
53//!         let mut viewer = DocumentViewer {
54//!             viewport: ViewportModel::new(80, 20),
55//!             content: "Large document content...".to_string(),
56//!         };
57//!         viewer.viewport.set_content(&viewer.content);
58//!         (viewer, None)
59//!     }
60//!
61//!     fn update(&mut self, msg: Msg) -> Option<Cmd> {
62//!         // Forward navigation messages to viewport
63//!         self.viewport.update(msg)
64//!     }
65//!
66//!     fn view(&self) -> String {
67//!         format!(
68//!             "Document Viewer\n\n{}\n\nScroll: {:.1}%",
69//!             self.viewport.view(),
70//!             self.viewport.scroll_percent() * 100.0
71//!         )
72//!     }
73//! }
74//! ```
75//!
76//! # Advanced Usage
77//!
78//! ```rust
79//! use bubbletea_widgets::viewport::{Model, ViewportKeyMap};
80//! use lipgloss_extras::prelude::*;
81//!
82//! // Create viewport with custom styling
83//! let mut viewport = Model::new(60, 15)
84//!     .with_style(
85//!         Style::new()
86//!             .border_style(lipgloss::normal_border())
87//!             .border_foreground(Color::from("#874BFD"))
88//!             .padding(1, 2, 1, 2)
89//!     );
90//!
91//! // Content from multiple sources
92//! let lines: Vec<String> = vec![
93//!     "Header information".to_string(),
94//!     "Content line 1".to_string(),
95//!     "Content line 2".to_string(),
96//! ];
97//! viewport.set_content_lines(lines);
98//!
99//! // Configure horizontal scrolling
100//! viewport.set_horizontal_step(5); // Scroll 5 columns at a time
101//! viewport.scroll_right();         // Move right
102//! ```
103//!
104//! # Navigation Controls
105//!
106//! | Keys | Action | Description |
107//! |------|--------| ----------- |
108//! | `↑`, `k` | Line Up | Scroll up one line |
109//! | `↓`, `j` | Line Down | Scroll down one line |
110//! | `←`, `h` | Left | Scroll left horizontally |
111//! | `→`, `l` | Right | Scroll right horizontally |
112//! | `PgUp`, `b` | Page Up | Scroll up one page |
113//! | `PgDn`, `f`, `Space` | Page Down | Scroll down one page |
114//! | `u` | Half Page Up | Scroll up half a page |
115//! | `d` | Half Page Down | Scroll down half a page |
116//!
117//! # Performance Optimization
118//!
119//! The viewport is designed for efficient handling of large content:
120//!
121//! - Only visible lines are rendered, regardless of total content size
122//! - Scrolling operations return affected lines for incremental updates
123//! - String operations are optimized for Unicode content
124//! - Frame size calculations account for lipgloss styling overhead
125//!
126//! # Content Types
127//!
128//! The viewport supports various content formats:
129//!
130//! - **Plain text**: Simple string content with automatic line splitting
131//! - **Pre-formatted lines**: Vector of strings for precise line control
132//! - **Unicode content**: Full support for wide characters and emojis
133//! - **Styled content**: Integration with lipgloss for rich formatting
134//!
135//! # State Management
136//!
137//! Track viewport state with built-in methods:
138//!
139//! - `at_top()` / `at_bottom()`: Boundary detection
140//! - `scroll_percent()`: Vertical scroll progress (0.0 to 1.0)
141//! - `horizontal_scroll_percent()`: Horizontal scroll progress
142//! - `line_count()`: Total content lines
143//! - `visible_lines()`: Currently displayed content
144
145use crate::key::{self, KeyMap as KeyMapTrait};
146use bubbletea_rs::{Cmd, KeyMsg, Model as BubbleTeaModel, Msg};
147use crossterm::event::KeyCode;
148use lipgloss_extras::lipgloss::width as lg_width;
149use lipgloss_extras::prelude::*;
150use unicode_width::UnicodeWidthChar;
151
152const SPACEBAR: char = ' ';
153
154/// Keyboard binding configuration for viewport navigation.
155///
156/// This struct defines all key combinations that control viewport scrolling,
157/// including line-by-line movement, page scrolling, and horizontal navigation.
158/// Each binding supports multiple key combinations and includes help text for
159/// documentation generation.
160///
161/// # Default Key Bindings
162///
163/// The default configuration provides both traditional navigation keys and
164/// Vim-style alternatives for maximum compatibility:
165///
166/// - **Line Movement**: Arrow keys (`↑↓`) and Vim keys (`kj`)
167/// - **Page Movement**: Page Up/Down and Vim keys (`bf`)
168/// - **Half Page**: Vim-style `u` (up) and `d` (down)
169/// - **Horizontal**: Arrow keys (`←→`) and Vim keys (`hl`)
170///
171/// # Examples
172///
173/// ```rust
174/// use bubbletea_widgets::viewport::{ViewportKeyMap, Model};
175/// use bubbletea_widgets::key;
176/// use crossterm::event::KeyCode;
177///
178/// // Use default key bindings
179/// let mut viewport = Model::new(80, 24);
180/// let keymap = viewport.keymap.clone(); // Uses ViewportKeyMap::default()
181///
182/// // Customize key bindings
183/// let mut custom_keymap = ViewportKeyMap::default();
184/// custom_keymap.page_down = key::Binding::new(vec![KeyCode::Char('n')])
185///     .with_help("n", "next page");
186/// custom_keymap.page_up = key::Binding::new(vec![KeyCode::Char('p')])
187///     .with_help("p", "previous page");
188///
189/// viewport.keymap = custom_keymap;
190/// ```
191///
192/// Integration with help system:
193/// ```rust
194/// use bubbletea_widgets::viewport::ViewportKeyMap;
195/// use bubbletea_widgets::key::KeyMap as KeyMapTrait;
196///
197/// let keymap = ViewportKeyMap::default();
198///
199/// // Get essential bindings for compact help
200/// let short_help = keymap.short_help();
201/// assert_eq!(short_help.len(), 4); // up, down, page_up, page_down
202///
203/// // Get all bindings organized by category
204/// let full_help = keymap.full_help();
205/// assert_eq!(full_help.len(), 4); // 4 categories of bindings
206/// ```
207///
208/// # Customization Patterns
209///
210/// Common customization scenarios:
211///
212/// ```rust
213/// use bubbletea_widgets::viewport::ViewportKeyMap;
214/// use bubbletea_widgets::key;
215/// use crossterm::event::KeyCode;
216///
217/// let mut keymap = ViewportKeyMap::default();
218///
219/// // Add additional keys for page navigation
220/// keymap.page_down = key::Binding::new(vec![
221///     KeyCode::PageDown,
222///     KeyCode::Char(' '),    // Space bar (default)
223///     KeyCode::Char('f'),    // Vim style (default)
224///     KeyCode::Enter,        // Custom addition
225/// ]).with_help("space/f/enter", "next page");
226///
227/// // Game-style WASD navigation
228/// keymap.up = key::Binding::new(vec![KeyCode::Char('w')])
229///     .with_help("w", "move up");
230/// keymap.down = key::Binding::new(vec![KeyCode::Char('s')])
231///     .with_help("s", "move down");
232/// keymap.left = key::Binding::new(vec![KeyCode::Char('a')])
233///     .with_help("a", "move left");
234/// keymap.right = key::Binding::new(vec![KeyCode::Char('d')])
235///     .with_help("d", "move right");
236/// ```
237#[derive(Debug, Clone)]
238pub struct ViewportKeyMap {
239    /// Key binding for scrolling down one full page.
240    ///
241    /// Default keys: Page Down, Space, `f` (Vim-style "forward")
242    pub page_down: key::Binding,
243    /// Key binding for scrolling up one full page.
244    ///
245    /// Default keys: Page Up, `b` (Vim-style "backward")
246    pub page_up: key::Binding,
247    /// Key binding for scrolling up half a page.
248    ///
249    /// Default key: `u` (Vim-style "up half page")
250    pub half_page_up: key::Binding,
251    /// Key binding for scrolling down half a page.
252    ///
253    /// Default key: `d` (Vim-style "down half page")
254    pub half_page_down: key::Binding,
255    /// Key binding for scrolling down one line.
256    ///
257    /// Default keys: Down arrow (`↓`), `j` (Vim-style)
258    pub down: key::Binding,
259    /// Key binding for scrolling up one line.
260    ///
261    /// Default keys: Up arrow (`↑`), `k` (Vim-style)
262    pub up: key::Binding,
263    /// Key binding for horizontal scrolling to the left.
264    ///
265    /// Default keys: Left arrow (`←`), `h` (Vim-style)
266    pub left: key::Binding,
267    /// Key binding for horizontal scrolling to the right.
268    ///
269    /// Default keys: Right arrow (`→`), `l` (Vim-style)
270    pub right: key::Binding,
271}
272
273impl Default for ViewportKeyMap {
274    /// Creates default viewport key bindings with Vim-style alternatives.
275    ///
276    /// The default configuration provides comprehensive navigation options
277    /// that accommodate both traditional arrow key users and Vim enthusiasts.
278    /// Each binding includes multiple key combinations for flexibility.
279    ///
280    /// # Default Key Mappings
281    ///
282    /// | Binding | Keys | Description |
283    /// |---------|------|-------------|
284    /// | `page_down` | `PgDn`, `Space`, `f` | Scroll down one page |
285    /// | `page_up` | `PgUp`, `b` | Scroll up one page |
286    /// | `half_page_down` | `d` | Scroll down half page |
287    /// | `half_page_up` | `u` | Scroll up half page |
288    /// | `down` | `↓`, `j` | Scroll down one line |
289    /// | `up` | `↑`, `k` | Scroll up one line |
290    /// | `left` | `←`, `h` | Scroll left horizontally |
291    /// | `right` | `→`, `l` | Scroll right horizontally |
292    ///
293    /// # Examples
294    ///
295    /// ```rust
296    /// use bubbletea_widgets::viewport::ViewportKeyMap;
297    ///
298    /// // Create with default bindings
299    /// let keymap = ViewportKeyMap::default();
300    ///
301    /// // Verify some default key combinations
302    /// assert!(!keymap.page_down.keys().is_empty());
303    /// assert!(!keymap.up.keys().is_empty());
304    /// ```
305    ///
306    /// # Design Philosophy
307    ///
308    /// - **Accessibility**: Arrow keys work for all users
309    /// - **Efficiency**: Vim keys provide rapid navigation for power users
310    /// - **Consistency**: Key choices match common terminal application patterns
311    /// - **Discoverability**: Help text explains each binding clearly
312    fn default() -> Self {
313        Self {
314            page_down: key::Binding::new(vec![
315                KeyCode::PageDown,
316                KeyCode::Char(SPACEBAR),
317                KeyCode::Char('f'),
318            ])
319            .with_help("f/pgdn", "page down"),
320            page_up: key::Binding::new(vec![KeyCode::PageUp, KeyCode::Char('b')])
321                .with_help("b/pgup", "page up"),
322            half_page_up: key::Binding::new(vec!["u", "ctrl+u"]).with_help("u/ctrl+u", "½ page up"),
323            half_page_down: key::Binding::new(vec!["d", "ctrl+d"])
324                .with_help("d/ctrl+d", "½ page down"),
325            up: key::Binding::new(vec![KeyCode::Up, KeyCode::Char('k')]).with_help("↑/k", "up"),
326            down: key::Binding::new(vec![KeyCode::Down, KeyCode::Char('j')])
327                .with_help("↓/j", "down"),
328            left: key::Binding::new(vec![KeyCode::Left, KeyCode::Char('h')])
329                .with_help("←/h", "move left"),
330            right: key::Binding::new(vec![KeyCode::Right, KeyCode::Char('l')])
331                .with_help("→/l", "move right"),
332        }
333    }
334}
335
336impl KeyMapTrait for ViewportKeyMap {
337    /// Returns the most essential key bindings for compact help display.
338    ///
339    /// This method provides a concise list of the most frequently used
340    /// navigation keys, suitable for brief help displays or status bars.
341    ///
342    /// # Returns
343    ///
344    /// A vector containing bindings for: up, down, page up, page down
345    ///
346    /// # Examples
347    ///
348    /// ```rust
349    /// use bubbletea_widgets::viewport::ViewportKeyMap;
350    /// use bubbletea_widgets::key::KeyMap as KeyMapTrait;
351    ///
352    /// let keymap = ViewportKeyMap::default();
353    /// let essential_keys = keymap.short_help();
354    ///
355    /// assert_eq!(essential_keys.len(), 4);
356    /// // Contains: up, down, page_up, page_down
357    /// ```
358    fn short_help(&self) -> Vec<&key::Binding> {
359        vec![&self.up, &self.down, &self.page_up, &self.page_down]
360    }
361
362    /// Returns all key bindings organized by navigation category.
363    ///
364    /// This method groups related navigation keys together for comprehensive
365    /// help displays. Each group represents a logical category of movement.
366    ///
367    /// # Returns
368    ///
369    /// A vector of binding groups:
370    /// 1. **Line navigation**: up, down
371    /// 2. **Horizontal navigation**: left, right  
372    /// 3. **Page navigation**: page up, page down
373    /// 4. **Half-page navigation**: half page up, half page down
374    ///
375    /// # Examples
376    ///
377    /// ```rust
378    /// use bubbletea_widgets::viewport::ViewportKeyMap;
379    /// use bubbletea_widgets::key::KeyMap as KeyMapTrait;
380    ///
381    /// let keymap = ViewportKeyMap::default();
382    /// let all_keys = keymap.full_help();
383    ///
384    /// assert_eq!(all_keys.len(), 4); // 4 categories
385    /// assert_eq!(all_keys[0].len(), 2); // Line navigation: up, down
386    /// assert_eq!(all_keys[1].len(), 2); // Horizontal: left, right
387    /// assert_eq!(all_keys[2].len(), 2); // Page: page_up, page_down
388    /// assert_eq!(all_keys[3].len(), 2); // Half-page: half_page_up, half_page_down
389    /// ```
390    ///
391    /// # Help Display Integration
392    ///
393    /// This organization enables structured help displays:
394    /// ```text
395    /// Navigation:
396    ///   ↑/k, ↓/j     line up, line down
397    ///   ←/h, →/l     scroll left, scroll right
398    ///   
399    ///   b/pgup, f/pgdn/space    page up, page down
400    ///   u, d                     half page up, half page down
401    /// ```
402    fn full_help(&self) -> Vec<Vec<&key::Binding>> {
403        vec![
404            vec![&self.up, &self.down],
405            vec![&self.left, &self.right],
406            vec![&self.page_up, &self.page_down],
407            vec![&self.half_page_up, &self.half_page_down],
408        ]
409    }
410}
411
412/// High-performance scrollable viewport for displaying large content efficiently.
413///
414/// This struct represents a complete viewport implementation that can handle content
415/// larger than the available display area. It provides smooth scrolling in both
416/// vertical and horizontal directions, efficient rendering of only visible content,
417/// and comprehensive keyboard navigation.
418///
419/// # Core Features
420///
421/// - **Efficient Rendering**: Only visible content is processed, enabling smooth performance with large datasets
422/// - **Bidirectional Scrolling**: Full support for both vertical and horizontal content navigation
423/// - **Content Management**: Flexible content input via strings or line vectors
424/// - **Styling Integration**: Full lipgloss styling support with automatic frame calculations
425/// - **Position Tracking**: Precise scroll percentages and boundary detection
426/// - **Keyboard Navigation**: Comprehensive key bindings with Vim-style alternatives
427///
428/// # Examples
429///
430/// Basic viewport setup:
431/// ```rust
432/// use bubbletea_widgets::viewport::Model;
433///
434/// // Create a viewport with specific dimensions
435/// let mut viewport = Model::new(80, 24);
436///
437/// // Add content to display
438/// let content = "Line 1\nLine 2\nLine 3\nVery long line that extends beyond viewport width\nLine 5";
439/// viewport.set_content(content);
440///
441/// // Navigate through content
442/// viewport.scroll_down(2);  // Move down 2 lines
443/// viewport.page_down();     // Move down one page
444///
445/// // Check current state
446/// println!("At bottom: {}", viewport.at_bottom());
447/// println!("Scroll progress: {:.1}%", viewport.scroll_percent() * 100.0);
448/// ```
449///
450/// Integration with styling:
451/// ```rust
452/// use bubbletea_widgets::viewport::Model;
453/// use lipgloss_extras::prelude::*;
454///
455/// let viewport = Model::new(60, 20)
456///     .with_style(
457///         Style::new()
458///             .border_style(lipgloss::normal_border())
459///             .border_foreground(Color::from("#874BFD"))
460///             .padding(1, 2, 1, 2)
461///     );
462/// ```
463///
464/// Working with line-based content:
465/// ```rust
466/// use bubbletea_widgets::viewport::Model;
467///
468/// let mut viewport = Model::new(50, 15);
469///
470/// // Set content from individual lines
471/// let lines = vec![
472///     "Header Line".to_string(),
473///     "Content Line 1".to_string(),
474///     "Content Line 2".to_string(),
475/// ];
476/// viewport.set_content_lines(lines);
477///
478/// // Get currently visible content
479/// let visible = viewport.visible_lines();
480/// println!("Displaying {} lines", visible.len());
481/// ```
482///
483/// # Performance Characteristics
484///
485/// - **Memory**: Only stores content lines, not rendered output
486/// - **CPU**: Rendering scales with viewport size, not content size
487/// - **Scrolling**: Incremental updates return only affected lines
488/// - **Unicode**: Proper width calculation for international content
489///
490/// # Thread Safety
491///
492/// The Model struct is `Clone` and can be safely used across threads.
493/// All internal state is self-contained and doesn't rely on external resources.
494///
495/// # State Management
496///
497/// The viewport maintains several key pieces of state:
498/// - **Content**: Lines of text stored internally
499/// - **Position**: Current scroll offsets for both axes
500/// - **Dimensions**: Viewport size and styling frame calculations
501/// - **Configuration**: Mouse settings, scroll steps, and key bindings
502#[derive(Debug, Clone)]
503pub struct Model {
504    /// Display width of the viewport in characters.
505    ///
506    /// This determines how many characters are visible horizontally.
507    /// Content wider than this will require horizontal scrolling to view.
508    pub width: usize,
509    /// Display height of the viewport in lines.
510    ///
511    /// This determines how many lines of content are visible at once.
512    /// Content with more lines will require vertical scrolling to view.
513    pub height: usize,
514    /// Lipgloss style applied to the viewport content.
515    ///
516    /// This style affects the entire viewport area and can include borders,
517    /// padding, margins, and background colors. Frame sizes are automatically
518    /// calculated and subtracted from the available content area.
519    pub style: Style,
520    /// Whether mouse wheel scrolling is enabled.
521    ///
522    /// When `true`, mouse wheel events will scroll the viewport content.
523    /// Note: Actual mouse wheel support depends on the terminal and
524    /// bubbletea-rs mouse event capabilities.
525    pub mouse_wheel_enabled: bool,
526    /// Number of lines to scroll per mouse wheel event.
527    ///
528    /// Default is 3 lines per wheel "click", which provides smooth scrolling
529    /// without being too sensitive. Adjust based on content density.
530    pub mouse_wheel_delta: usize,
531    /// Current vertical scroll position (lines from top).
532    ///
533    /// This value indicates how many lines have been scrolled down from
534    /// the beginning of the content. 0 means showing from the first line.
535    pub y_offset: usize,
536    /// Current horizontal scroll position (characters from left).
537    ///
538    /// This value indicates how many characters have been scrolled right
539    /// from the beginning of each line. 0 means showing from column 0.
540    pub x_offset: usize,
541    /// Number of characters to scroll horizontally per step.
542    ///
543    /// Controls the granularity of horizontal scrolling. Smaller values
544    /// provide finer control, larger values enable faster navigation.
545    pub horizontal_step: usize,
546    /// Vertical position of viewport in terminal for performance rendering.
547    ///
548    /// Used for optimized rendering in some terminal applications.
549    /// Generally can be left at default (0) unless implementing
550    /// advanced rendering optimizations.
551    pub y_position: usize,
552    /// Keyboard binding configuration for navigation.
553    ///
554    /// Defines which keys control scrolling behavior. Can be customized
555    /// to match application-specific navigation patterns or user preferences.
556    pub keymap: ViewportKeyMap,
557
558    // Internal state
559    /// Content lines stored for display.
560    ///
561    /// Internal storage for the content being displayed. Managed automatically
562    /// when content is set via `set_content()` or `set_content_lines()`.
563    lines: Vec<String>,
564    /// Width of the longest content line in characters.
565    ///
566    /// Cached value used for horizontal scrolling calculations and
567    /// scroll percentage computations. Updated automatically when content changes.
568    longest_line_width: usize,
569    /// Whether the viewport has been properly initialized.
570    ///
571    /// Tracks initialization state to ensure proper configuration.
572    /// Set automatically during construction and configuration.
573    initialized: bool,
574}
575
576impl Model {
577    /// Creates a new viewport with the specified dimensions.
578    ///
579    /// This constructor initializes a viewport with the given width and height,
580    /// along with sensible defaults for all configuration options. The viewport
581    /// starts with no content and is ready to receive text via `set_content()`
582    /// or `set_content_lines()`.
583    ///
584    /// # Arguments
585    ///
586    /// * `width` - Display width in characters (horizontal viewport size)
587    /// * `height` - Display height in lines (vertical viewport size)
588    ///
589    /// # Returns
590    ///
591    /// A new `Model` instance with default configuration
592    ///
593    /// # Examples
594    ///
595    /// ```rust
596    /// use bubbletea_widgets::viewport::Model;
597    ///
598    /// // Create a standard terminal-sized viewport
599    /// let viewport = Model::new(80, 24);
600    /// assert_eq!(viewport.width, 80);
601    /// assert_eq!(viewport.height, 24);
602    /// assert!(viewport.mouse_wheel_enabled);
603    /// ```
604    ///
605    /// Different viewport sizes for various use cases:
606    /// ```rust
607    /// use bubbletea_widgets::viewport::Model;
608    ///
609    /// // Compact viewport for sidebar content
610    /// let sidebar = Model::new(30, 20);
611    ///
612    /// // Wide viewport for code display
613    /// let code_view = Model::new(120, 40);
614    ///
615    /// // Small preview viewport
616    /// let preview = Model::new(40, 10);
617    /// ```
618    ///
619    /// # Default Configuration
620    ///
621    /// - **Mouse wheel**: Enabled with 3-line scroll delta
622    /// - **Scroll position**: At top-left (0, 0)
623    /// - **Horizontal step**: 1 character per scroll
624    /// - **Style**: No styling applied
625    /// - **Key bindings**: Vim-style with arrow key alternatives
626    ///
627    /// # Performance
628    ///
629    /// Viewport creation is very fast as no content processing occurs during
630    /// construction. Memory usage scales with content size, not viewport dimensions.
631    pub fn new(width: usize, height: usize) -> Self {
632        let mut model = Self {
633            width,
634            height,
635            style: Style::new(),
636            mouse_wheel_enabled: true,
637            mouse_wheel_delta: 3,
638            y_offset: 0,
639            x_offset: 0,
640            horizontal_step: 1,
641            y_position: 0,
642            keymap: ViewportKeyMap::default(),
643            lines: Vec::new(),
644            longest_line_width: 0,
645            initialized: false,
646        };
647        model.set_initial_values();
648        model
649    }
650
651    /// Set initial values for the viewport
652    fn set_initial_values(&mut self) {
653        self.mouse_wheel_enabled = true;
654        self.mouse_wheel_delta = 3;
655        self.initialized = true;
656    }
657
658    /// Builder method to set viewport dimensions during construction.
659    ///
660    /// This method allows for fluent construction by updating the viewport
661    /// dimensions after creation. Useful when dimensions are computed or
662    /// provided by external sources.
663    ///
664    /// # Arguments
665    ///
666    /// * `width` - New width in characters
667    /// * `height` - New height in lines
668    ///
669    /// # Returns
670    ///
671    /// The modified viewport for method chaining
672    ///
673    /// # Examples
674    ///
675    /// ```rust
676    /// use bubbletea_widgets::viewport::Model;
677    /// use lipgloss_extras::prelude::*;
678    ///
679    /// // Fluent construction with dimensions
680    /// let viewport = Model::new(40, 10)
681    ///     .with_dimensions(80, 24)
682    ///     .with_style(Style::new().padding(1, 2, 1, 2));
683    ///
684    /// assert_eq!(viewport.width, 80);
685    /// assert_eq!(viewport.height, 24);
686    /// ```
687    ///
688    /// Dynamic viewport sizing:
689    /// ```rust
690    /// use bubbletea_widgets::viewport::Model;
691    ///
692    /// fn create_responsive_viewport(terminal_width: usize, terminal_height: usize) -> Model {
693    ///     Model::new(20, 10) // Default size
694    ///         .with_dimensions(
695    ///             (terminal_width * 80) / 100,  // 80% of terminal width
696    ///             (terminal_height * 60) / 100  // 60% of terminal height
697    ///         )
698    /// }
699    /// ```
700    pub fn with_dimensions(mut self, width: usize, height: usize) -> Self {
701        self.width = width;
702        self.height = height;
703        self
704    }
705
706    /// Builder method to apply lipgloss styling to the viewport.
707    ///
708    /// This method sets the visual styling for the entire viewport area.
709    /// The style can include borders, padding, margins, colors, and other
710    /// lipgloss formatting. Frame sizes are automatically calculated and
711    /// subtracted from the content display area.
712    ///
713    /// # Arguments
714    ///
715    /// * `style` - Lipgloss style to apply to the viewport
716    ///
717    /// # Returns
718    ///
719    /// The styled viewport for method chaining
720    ///
721    /// # Examples
722    ///
723    /// ```rust
724    /// use bubbletea_widgets::viewport::Model;
725    /// use lipgloss_extras::prelude::*;
726    ///
727    /// // Create viewport with border and padding
728    /// let viewport = Model::new(60, 20)
729    ///     .with_style(
730    ///         Style::new()
731    ///             .border_style(lipgloss::normal_border())
732    ///             .border_foreground(Color::from("#874BFD"))
733    ///             .padding(1, 2, 1, 2)
734    ///     );
735    /// ```
736    ///
737    /// Themed viewport styling:
738    /// ```rust
739    /// use bubbletea_widgets::viewport::Model;
740    /// use lipgloss_extras::prelude::*;
741    ///
742    /// // Dark theme viewport
743    /// let dark_viewport = Model::new(80, 24)
744    ///     .with_style(
745    ///         Style::new()
746    ///             .background(Color::from("#1a1a1a"))
747    ///             .foreground(Color::from("#ffffff"))
748    ///             .border_style(lipgloss::normal_border())
749    ///             .border_foreground(Color::from("#444444"))
750    ///     );
751    ///
752    /// // Light theme viewport
753    /// let light_viewport = Model::new(80, 24)
754    ///     .with_style(
755    ///         Style::new()
756    ///             .background(Color::from("#ffffff"))
757    ///             .foreground(Color::from("#000000"))
758    ///             .border_style(lipgloss::normal_border())
759    ///             .border_foreground(Color::from("#cccccc"))
760    ///     );
761    /// ```
762    ///
763    /// # Frame Size Impact
764    ///
765    /// Styling with borders and padding reduces the available content area:
766    /// ```rust
767    /// use bubbletea_widgets::viewport::Model;
768    /// use lipgloss_extras::prelude::*;
769    ///
770    /// // 80x24 viewport with 2-character padding
771    /// let viewport = Model::new(80, 24)
772    ///     .with_style(
773    ///         Style::new().padding(1, 2, 1, 2) // top, right, bottom, left
774    ///     );
775    ///
776    /// // Effective content area is now ~76x22 due to padding
777    /// ```
778    pub fn with_style(mut self, style: Style) -> Self {
779        self.style = style;
780        self
781    }
782
783    /// Returns whether the viewport is scrolled to the very top of the content.
784    ///
785    /// This method checks if the vertical scroll position is at the beginning,
786    /// meaning no content is hidden above the current view. Useful for
787    /// determining when scroll-up operations should be disabled or when
788    /// displaying scroll indicators.
789    ///
790    /// # Returns
791    ///
792    /// `true` if at the top (y_offset == 0), `false` if content is scrolled
793    ///
794    /// # Examples
795    ///
796    /// ```rust
797    /// use bubbletea_widgets::viewport::Model;
798    ///
799    /// let mut viewport = Model::new(40, 5);
800    /// let content = (1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n");
801    /// viewport.set_content(&content);
802    ///
803    /// // Initially at top
804    /// assert!(viewport.at_top());
805    ///
806    /// // After scrolling down
807    /// viewport.scroll_down(1);
808    /// assert!(!viewport.at_top());
809    ///
810    /// // After returning to top
811    /// viewport.goto_top();
812    /// assert!(viewport.at_top());
813    /// ```
814    ///
815    /// UI integration example:
816    /// ```rust
817    /// use bubbletea_widgets::viewport::Model;
818    ///
819    /// fn render_scroll_indicator(viewport: &Model) -> String {
820    ///     let up_arrow = if viewport.at_top() { " " } else { "↑" };
821    ///     let down_arrow = if viewport.at_bottom() { " " } else { "↓" };
822    ///     format!("{} Content {} ", up_arrow, down_arrow)
823    /// }
824    /// ```
825    pub fn at_top(&self) -> bool {
826        self.y_offset == 0
827    }
828
829    /// Returns whether the viewport is scrolled to or past the bottom of the content.
830    ///
831    /// This method checks if the vertical scroll position has reached the end,
832    /// meaning no more content is available below the current view. Useful for
833    /// determining when scroll-down operations should be disabled or when
834    /// implementing infinite scroll detection.
835    ///
836    /// # Returns
837    ///
838    /// `true` if at or past the bottom, `false` if more content is available below
839    ///
840    /// # Examples
841    ///
842    /// ```rust
843    /// use bubbletea_widgets::viewport::Model;
844    ///
845    /// let mut viewport = Model::new(40, 3); // Small viewport
846    /// viewport.set_content("Line 1\nLine 2\nLine 3\nLine 4\nLine 5");
847    ///
848    /// // Initially not at bottom (more content below)
849    /// assert!(!viewport.at_bottom());
850    ///
851    /// // Scroll to bottom
852    /// viewport.goto_bottom();
853    /// assert!(viewport.at_bottom());
854    /// ```
855    ///
856    /// Scroll control logic:
857    /// ```rust
858    /// use bubbletea_widgets::viewport::Model;
859    ///
860    /// fn handle_scroll_down(viewport: &mut Model) -> bool {
861    ///     if viewport.at_bottom() {
862    ///         // Can't scroll further down
863    ///         false
864    ///     } else {
865    ///         viewport.scroll_down(1);
866    ///         true
867    ///     }
868    /// }
869    /// ```
870    ///
871    /// # Difference from `past_bottom()`
872    ///
873    /// - `at_bottom()`: Returns `true` when at the maximum valid scroll position
874    /// - `past_bottom()`: Returns `true` only when scrolled beyond valid content
875    pub fn at_bottom(&self) -> bool {
876        self.y_offset >= self.max_y_offset()
877    }
878
879    /// Returns whether the viewport has been scrolled beyond valid content.
880    ///
881    /// This method detects an invalid scroll state where the y_offset exceeds
882    /// the maximum valid position. This can occur during content changes or
883    /// viewport resizing. Generally indicates a need for scroll position correction.
884    ///
885    /// # Returns
886    ///
887    /// `true` if scrolled past valid content, `false` if within valid range
888    ///
889    /// # Examples
890    ///
891    /// ```rust
892    /// use bubbletea_widgets::viewport::Model;
893    ///
894    /// let mut viewport = Model::new(40, 10);
895    /// viewport.set_content("Line 1\nLine 2\nLine 3");
896    ///
897    /// // Normal scroll position
898    /// assert!(!viewport.past_bottom());
899    ///
900    /// // This would typically be prevented by normal scroll methods,
901    /// // but could occur during content changes
902    /// ```
903    ///
904    /// Auto-correction usage:
905    /// ```rust
906    /// use bubbletea_widgets::viewport::Model;
907    ///
908    /// fn ensure_valid_scroll(viewport: &mut Model) {
909    ///     if viewport.past_bottom() {
910    ///         viewport.goto_bottom(); // Correct invalid position
911    ///     }
912    /// }
913    /// ```
914    ///
915    /// # Use Cases
916    ///
917    /// - Detecting invalid state after content changes
918    /// - Validation in custom scroll implementations
919    /// - Debug assertion checks
920    /// - Auto-correction logic
921    pub fn past_bottom(&self) -> bool {
922        self.y_offset > self.max_y_offset()
923    }
924
925    /// Returns the vertical scroll progress as a percentage from 0.0 to 1.0.
926    ///
927    /// This method calculates how far through the content the viewport has
928    /// scrolled vertically. 0.0 indicates the top, 1.0 indicates the bottom.
929    /// Useful for implementing scroll indicators, progress bars, or proportional
930    /// navigation controls.
931    ///
932    /// # Returns
933    ///
934    /// A float between 0.0 and 1.0 representing scroll progress:
935    /// - `0.0`: At the very top of content
936    /// - `0.5`: Halfway through content
937    /// - `1.0`: At or past the bottom of content
938    ///
939    /// # Examples
940    ///
941    /// ```rust
942    /// use bubbletea_widgets::viewport::Model;
943    ///
944    /// let mut viewport = Model::new(40, 5);
945    /// viewport.set_content("Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10");
946    ///
947    /// // At top
948    /// assert_eq!(viewport.scroll_percent(), 0.0);
949    ///
950    /// // Scroll partway down
951    /// viewport.scroll_down(2);
952    /// let progress = viewport.scroll_percent();
953    /// assert!(progress > 0.0 && progress < 1.0);
954    ///
955    /// // At bottom
956    /// viewport.goto_bottom();
957    /// assert_eq!(viewport.scroll_percent(), 1.0);
958    /// ```
959    ///
960    /// Progress bar implementation:
961    /// ```rust
962    /// use bubbletea_widgets::viewport::Model;
963    ///
964    /// fn render_progress_bar(viewport: &Model, width: usize) -> String {
965    ///     let progress = viewport.scroll_percent();
966    ///     let filled_chars = (progress * width as f64) as usize;
967    ///     let empty_chars = width - filled_chars;
968    ///     
969    ///     format!(
970    ///         "[{}{}] {:.1}%",
971    ///         "█".repeat(filled_chars),
972    ///         "░".repeat(empty_chars),
973    ///         progress * 100.0
974    ///     )
975    /// }
976    /// ```
977    ///
978    /// # Special Cases
979    ///
980    /// - If viewport height >= content lines, returns 1.0 (all content visible)
981    /// - Result is clamped to [0.0, 1.0] range for safety
982    /// - Calculation accounts for viewport height in determining valid scroll range
983    pub fn scroll_percent(&self) -> f64 {
984        if self.height >= self.lines.len() {
985            return 1.0;
986        }
987        let y = self.y_offset as f64;
988        let h = self.height as f64;
989        let t = self.lines.len() as f64;
990        let v = y / (t - h);
991        v.clamp(0.0, 1.0)
992    }
993
994    /// Returns the horizontal scroll progress as a percentage from 0.0 to 1.0.
995    ///
996    /// This method calculates how far through the content width the viewport has
997    /// scrolled horizontally. 0.0 indicates the leftmost position, 1.0 indicates
998    /// the rightmost position. Useful for implementing horizontal scroll indicators
999    /// or proportional navigation controls for wide content.
1000    ///
1001    /// # Returns
1002    ///
1003    /// A float between 0.0 and 1.0 representing horizontal scroll progress:
1004    /// - `0.0`: At the leftmost edge of content
1005    /// - `0.5`: Halfway through the content width
1006    /// - `1.0`: At or past the rightmost edge of content
1007    ///
1008    /// # Examples
1009    ///
1010    /// ```rust
1011    /// use bubbletea_widgets::viewport::Model;
1012    ///
1013    /// let mut viewport = Model::new(20, 5);
1014    /// viewport.set_content("Short line\nThis is a very long line that extends beyond viewport width\nAnother line");
1015    ///
1016    /// // At left edge
1017    /// assert_eq!(viewport.horizontal_scroll_percent(), 0.0);
1018    ///
1019    /// // Scroll horizontally
1020    /// // Scroll right 10 times
1021    /// for _ in 0..10 {
1022    ///     viewport.scroll_right();
1023    /// }
1024    /// let h_progress = viewport.horizontal_scroll_percent();
1025    /// assert!(h_progress > 0.0 && h_progress <= 1.0);
1026    ///
1027    /// // At right edge
1028    /// // Scroll far to ensure we reach the end
1029    /// for _ in 0..1000 {
1030    ///     viewport.scroll_right();
1031    /// }
1032    /// assert_eq!(viewport.horizontal_scroll_percent(), 1.0);
1033    /// ```
1034    ///
1035    /// Horizontal progress indicator:
1036    /// ```rust
1037    /// use bubbletea_widgets::viewport::Model;
1038    ///
1039    /// fn render_horizontal_indicator(viewport: &Model, width: usize) -> String {
1040    ///     let h_progress = viewport.horizontal_scroll_percent();
1041    ///     let position = (h_progress * width as f64) as usize;
1042    ///     
1043    ///     let mut indicator = vec!['-'; width];
1044    ///     if position < width {
1045    ///         indicator[position] = '|';
1046    ///     }
1047    ///     indicator.into_iter().collect()
1048    /// }
1049    /// ```
1050    ///
1051    /// # Special Cases
1052    ///
1053    /// - If viewport width >= longest line width, returns 1.0 (all content visible)
1054    /// - Result is clamped to [0.0, 1.0] range for safety
1055    /// - Based on the longest line in the content, not current visible lines
1056    pub fn horizontal_scroll_percent(&self) -> f64 {
1057        if self.x_offset >= self.longest_line_width.saturating_sub(self.width) {
1058            return 1.0;
1059        }
1060        let y = self.x_offset as f64;
1061        let h = self.width as f64;
1062        let t = self.longest_line_width as f64;
1063        let v = y / (t - h);
1064        v.clamp(0.0, 1.0)
1065    }
1066
1067    /// Sets the viewport's text content from a multi-line string.
1068    ///
1069    /// This method processes the provided string by splitting it into individual lines
1070    /// and storing them internally for display. Line endings are normalized to Unix-style
1071    /// (`\n`), and the longest line width is calculated for horizontal scrolling support.
1072    ///
1073    /// # Arguments
1074    ///
1075    /// * `content` - The text content as a multi-line string
1076    ///
1077    /// # Examples
1078    ///
1079    /// ```rust
1080    /// use bubbletea_widgets::viewport::Model;
1081    ///
1082    /// let mut viewport = Model::new(40, 10);
1083    /// viewport.set_content("Line 1\nLine 2\nVery long line that extends beyond viewport width\nLine 4");
1084    ///
1085    /// // Content is now available for display
1086    /// let visible = viewport.visible_lines();
1087    /// assert!(!visible.is_empty());
1088    /// ```
1089    ///
1090    /// Loading file content:
1091    /// ```rust
1092    /// use bubbletea_widgets::viewport::Model;
1093    /// use std::fs;
1094    ///
1095    /// let mut viewport = Model::new(80, 24);
1096    ///
1097    /// // Load file content into viewport
1098    /// let file_content = fs::read_to_string("example.txt").unwrap_or_default();
1099    /// viewport.set_content(&file_content);
1100    /// ```
1101    ///
1102    /// Dynamic content updates:
1103    /// ```rust
1104    /// use bubbletea_widgets::viewport::Model;
1105    ///
1106    /// let mut viewport = Model::new(50, 15);
1107    ///
1108    /// // Initial content
1109    /// viewport.set_content("Initial content\nLine 2");
1110    ///
1111    /// // Update with new content
1112    /// let new_content = (1..=100)
1113    ///     .map(|i| format!("Generated line {}", i))
1114    ///     .collect::<Vec<_>>()
1115    ///     .join("\n");
1116    /// viewport.set_content(&new_content);
1117    /// ```
1118    ///
1119    /// # Behavior
1120    ///
1121    /// - **Line Ending Normalization**: Converts `\r\n` to `\n` for consistency
1122    /// - **Width Calculation**: Automatically computes the longest line for horizontal scrolling
1123    /// - **Scroll Position**: If the current scroll position becomes invalid, scrolls to bottom
1124    /// - **Performance**: Content processing occurs immediately; consider using `set_content_lines()` for pre-split content
1125    ///
1126    /// # Cross-Platform Compatibility
1127    ///
1128    /// Content from Windows systems with `\r\n` line endings is automatically normalized,
1129    /// ensuring consistent behavior across all platforms.
1130    pub fn set_content(&mut self, content: &str) {
1131        let content = content.replace("\r\n", "\n"); // normalize line endings
1132        self.lines = content.split('\n').map(|s| s.to_string()).collect();
1133        self.longest_line_width = find_longest_line_width(&self.lines);
1134
1135        if self.y_offset > self.lines.len().saturating_sub(1) {
1136            self.goto_bottom();
1137        }
1138    }
1139
1140    /// Sets the viewport content from a pre-split vector of lines.
1141    ///
1142    /// This method directly sets the viewport content from an existing vector of
1143    /// strings, avoiding the string splitting overhead of `set_content()`. Each
1144    /// string represents one line of content. This is more efficient when content
1145    /// is already available as individual lines.
1146    ///
1147    /// # Arguments
1148    ///
1149    /// * `lines` - Vector of strings where each string is a content line
1150    ///
1151    /// # Examples
1152    ///
1153    /// ```rust
1154    /// use bubbletea_widgets::viewport::Model;
1155    ///
1156    /// let mut viewport = Model::new(40, 10);
1157    ///
1158    /// let lines = vec![
1159    ///     "Header Line".to_string(),
1160    ///     "Content Line 1".to_string(),
1161    ///     "Content Line 2".to_string(),
1162    ///     "A very long line that extends beyond the viewport width".to_string(),
1163    /// ];
1164    ///
1165    /// viewport.set_content_lines(lines);
1166    ///
1167    /// let visible = viewport.visible_lines();
1168    /// assert_eq!(visible.len(), 4); // All lines fit in viewport height
1169    /// ```
1170    ///
1171    /// Processing structured data:
1172    /// ```rust
1173    /// use bubbletea_widgets::viewport::Model;
1174    ///
1175    /// #[derive(Debug)]
1176    /// struct LogEntry {
1177    ///     timestamp: String,
1178    ///     level: String,
1179    ///     message: String,
1180    /// }
1181    ///
1182    /// let mut viewport = Model::new(80, 20);
1183    /// let log_entries = vec![
1184    ///     LogEntry { timestamp: "2024-01-01T10:00:00".to_string(), level: "INFO".to_string(), message: "Application started".to_string() },
1185    ///     LogEntry { timestamp: "2024-01-01T10:01:00".to_string(), level: "ERROR".to_string(), message: "Connection failed".to_string() },
1186    /// ];
1187    ///
1188    /// let formatted_lines: Vec<String> = log_entries
1189    ///     .iter()
1190    ///     .map(|entry| format!("[{}] {}: {}", entry.timestamp, entry.level, entry.message))
1191    ///     .collect();
1192    ///
1193    /// viewport.set_content_lines(formatted_lines);
1194    /// ```
1195    ///
1196    /// Reading from various sources:
1197    /// ```rust
1198    /// use bubbletea_widgets::viewport::Model;
1199    ///
1200    /// let mut viewport = Model::new(60, 15);
1201    ///
1202    /// // Use pre-split lines for better performance
1203    /// let lines: Vec<String> = vec![
1204    ///     "Line 1".to_string(),
1205    ///     "Line 2".to_string(),
1206    ///     "Line 3".to_string(),
1207    /// ];
1208    ///
1209    /// viewport.set_content_lines(lines);
1210    /// assert_eq!(viewport.line_count(), 3);
1211    /// ```
1212    ///
1213    /// # Performance Benefits
1214    ///
1215    /// - **No String Processing**: Avoids the overhead of splitting a large string
1216    /// - **Memory Efficient**: Directly moves the vector into internal storage
1217    /// - **Ideal for Streaming**: Perfect for incrementally building content
1218    /// - **Pre-formatted Content**: Useful when lines are already processed/formatted
1219    ///
1220    /// # Behavior
1221    ///
1222    /// - **Width Calculation**: Automatically computes the longest line for horizontal scrolling
1223    /// - **Scroll Position**: If current scroll position becomes invalid, scrolls to bottom
1224    /// - **Ownership**: Takes ownership of the provided vector
1225    /// - **No Normalization**: Lines are used as-is without line ending processing
1226    pub fn set_content_lines(&mut self, lines: Vec<String>) {
1227        self.lines = lines;
1228        self.longest_line_width = find_longest_line_width(&self.lines);
1229
1230        if self.y_offset > self.lines.len().saturating_sub(1) {
1231            self.goto_bottom();
1232        }
1233    }
1234
1235    /// Returns the lines currently visible in the viewport.
1236    ///
1237    /// This method calculates which lines should be displayed based on the current
1238    /// scroll position, viewport dimensions, and applied styling. It handles both
1239    /// vertical scrolling (which lines to show) and horizontal scrolling (which
1240    /// portion of each line to show). The result accounts for frame sizes from
1241    /// lipgloss styling like borders and padding.
1242    ///
1243    /// # Returns
1244    ///
1245    /// A vector of strings representing the currently visible content lines.
1246    /// Each string is horizontally clipped to fit within the viewport width.
1247    ///
1248    /// # Examples
1249    ///
1250    /// ```rust
1251    /// use bubbletea_widgets::viewport::Model;
1252    ///
1253    /// let mut viewport = Model::new(20, 5);
1254    /// viewport.set_content("Line 1\nLine 2\nLine 3\nLine 4\nLine 5");
1255    ///
1256    /// // Get initial visible lines (height 5 minus 2 frame = 3 effective)
1257    /// let visible = viewport.visible_lines();
1258    /// assert_eq!(visible.len(), 3);
1259    /// assert_eq!(visible[0], "Line 1");
1260    /// assert_eq!(visible[1], "Line 2");
1261    /// assert_eq!(visible[2], "Line 3");
1262    ///
1263    /// // After scrolling down
1264    /// viewport.scroll_down(2);
1265    /// let visible = viewport.visible_lines();
1266    /// assert_eq!(visible[0], "Line 3");
1267    /// assert_eq!(visible[1], "Line 4");
1268    /// assert_eq!(visible[2], "Line 5");
1269    /// ```
1270    ///
1271    /// Horizontal scrolling example:
1272    /// ```rust
1273    /// use bubbletea_widgets::viewport::Model;
1274    ///
1275    /// let mut viewport = Model::new(10, 4); // Narrow viewport (4 height minus 2 frame = 2 effective)
1276    /// viewport.set_content("Short\nThis is a very long line that gets clipped");
1277    ///
1278    /// // Initial view shows left portion
1279    /// let visible = viewport.visible_lines();
1280    /// assert_eq!(visible[1], "This is ");
1281    ///
1282    /// // After horizontal scrolling
1283    /// // Scroll right 5 times
1284    /// for _ in 0..5 {
1285    ///     viewport.scroll_right();
1286    /// }
1287    /// let visible = viewport.visible_lines();
1288    /// assert_eq!(visible[1], "is a ver"); // Shifted right (8 chars max)
1289    /// ```
1290    ///
1291    /// Working with styled viewport:
1292    /// ```rust
1293    /// use bubbletea_widgets::viewport::Model;
1294    /// use lipgloss_extras::prelude::*;
1295    ///
1296    /// let mut viewport = Model::new(20, 5)
1297    ///     .with_style(
1298    ///         Style::new().padding(1, 2, 1, 2) // Reduces effective size
1299    ///     );
1300    ///
1301    /// viewport.set_content("Line 1\nLine 2\nLine 3");
1302    /// let visible = viewport.visible_lines();
1303    ///
1304    /// // Available content area is reduced by padding
1305    /// // Each visible line is also clipped to account for horizontal padding
1306    /// ```
1307    ///
1308    /// # Performance Considerations
1309    ///
1310    /// - **Efficient Rendering**: Only processes lines within the visible area
1311    /// - **Frame Calculation**: Style frame sizes are computed once per call
1312    /// - **Clipping**: Horizontal clipping is applied only when needed
1313    /// - **Memory**: Returns a new vector; consider caching for frequent calls
1314    ///
1315    /// # Integration Patterns
1316    ///
1317    /// This method is typically used in the view/render phase:
1318    /// ```rust
1319    /// use bubbletea_widgets::viewport::Model;
1320    /// use lipgloss_extras::prelude::*;
1321    ///
1322    /// fn render_viewport_content(viewport: &Model) -> String {
1323    ///     let visible_lines = viewport.visible_lines();
1324    ///     
1325    ///     if visible_lines.is_empty() {
1326    ///         return "No content to display".to_string();
1327    ///     }
1328    ///     
1329    ///     visible_lines.join("\n")
1330    /// }
1331    /// ```
1332    pub fn visible_lines(&self) -> Vec<String> {
1333        let frame_height = self.style.get_vertical_frame_size();
1334        let frame_width = self.style.get_horizontal_frame_size();
1335        let h = self.height.saturating_sub(frame_height as usize);
1336        let w = self.width.saturating_sub(frame_width as usize);
1337
1338        let mut lines = Vec::new();
1339        if !self.lines.is_empty() {
1340            let top = self.y_offset;
1341            let bottom = (self.y_offset + h).min(self.lines.len());
1342            lines = self.lines[top..bottom].to_vec();
1343        }
1344
1345        // Handle horizontal scrolling
1346        if self.x_offset == 0 && self.longest_line_width <= w || w == 0 {
1347            return lines;
1348        }
1349
1350        let mut cut_lines = Vec::new();
1351        for line in lines {
1352            let cut_line = cut_string(&line, self.x_offset, self.x_offset + w);
1353            cut_lines.push(cut_line);
1354        }
1355        cut_lines
1356    }
1357
1358    /// Sets the vertical scroll position to a specific line offset.
1359    ///
1360    /// This method directly positions the viewport at the specified line offset
1361    /// from the beginning of the content. The offset is automatically clamped
1362    /// to ensure it remains within valid bounds (0 to maximum valid offset).
1363    ///
1364    /// # Arguments
1365    ///
1366    /// * `n` - The line number to scroll to (0-based indexing)
1367    ///
1368    /// # Examples
1369    ///
1370    /// ```rust
1371    /// use bubbletea_widgets::viewport::Model;
1372    ///
1373    /// let mut viewport = Model::new(40, 5);
1374    /// viewport.set_content(&(1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1375    ///
1376    /// // Jump to line 10 (0-based, so content shows "Line 11")
1377    /// viewport.set_y_offset(10);
1378    /// let visible = viewport.visible_lines();
1379    /// assert_eq!(visible[0], "Line 11");
1380    ///
1381    /// // Attempt to scroll beyond content (gets clamped)
1382    /// viewport.set_y_offset(1000);
1383    /// assert!(viewport.at_bottom());
1384    /// ```
1385    ///
1386    /// Direct positioning for navigation:
1387    /// ```rust
1388    /// use bubbletea_widgets::viewport::Model;
1389    ///
1390    /// let mut viewport = Model::new(80, 20);
1391    /// viewport.set_content("Line content...");
1392    ///
1393    /// // Jump to 25% through the content
1394    /// let total_lines = 100; // Assume we know content size
1395    /// let quarter_position = total_lines / 4;
1396    /// viewport.set_y_offset(quarter_position);
1397    /// ```
1398    ///
1399    /// # Clamping Behavior
1400    ///
1401    /// - Values less than 0: Set to 0 (top of content)
1402    /// - Values greater than maximum valid offset: Set to maximum (bottom view)
1403    /// - Maximum offset ensures at least one line is visible when possible
1404    ///
1405    /// # Use Cases
1406    ///
1407    /// - **Direct Navigation**: Jump to specific locations
1408    /// - **Proportional Scrolling**: Navigate based on percentages
1409    /// - **Search Results**: Position at specific line numbers
1410    /// - **Bookmarks**: Return to saved positions
1411    pub fn set_y_offset(&mut self, n: usize) {
1412        self.y_offset = n.min(self.max_y_offset());
1413    }
1414
1415    /// Scrolls down by one full page (viewport height).
1416    ///
1417    /// This method moves the viewport down by exactly the viewport height,
1418    /// effectively showing the next "page" of content. This is the standard
1419    /// page-down operation found in most text viewers and editors.
1420    ///
1421    /// # Returns
1422    ///
1423    /// A vector of strings representing the newly visible lines that scrolled into view.
1424    /// Returns an empty vector if already at the bottom or no scrolling occurred.
1425    ///
1426    /// # Examples
1427    ///
1428    /// ```rust
1429    /// use bubbletea_widgets::viewport::Model;
1430    ///
1431    /// let mut viewport = Model::new(40, 5);
1432    /// viewport.set_content(&(1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1433    ///
1434    /// // Initially shows lines 1-5
1435    /// let visible = viewport.visible_lines();
1436    /// assert_eq!(visible[0], "Line 1");
1437    ///
1438    /// // Page down shows lines 6-10
1439    /// let new_lines = viewport.page_down();
1440    /// let visible = viewport.visible_lines();
1441    /// assert_eq!(visible[0], "Line 6");
1442    /// assert!(!new_lines.is_empty());
1443    /// ```
1444    ///
1445    /// Handling bottom boundary:
1446    /// ```rust
1447    /// use bubbletea_widgets::viewport::Model;
1448    ///
1449    /// let mut viewport = Model::new(40, 5);
1450    /// viewport.set_content("Line 1\nLine 2\nLine 3"); // Only 3 lines
1451    ///
1452    /// // At bottom already, page_down returns empty
1453    /// viewport.goto_bottom();
1454    /// let result = viewport.page_down();
1455    /// assert!(result.is_empty());
1456    /// ```
1457    ///
1458    /// # Performance Optimization
1459    ///
1460    /// The returned vector contains only the newly visible lines for efficient
1461    /// rendering updates. Applications can use this for incremental display updates.
1462    pub fn page_down(&mut self) -> Vec<String> {
1463        if self.at_bottom() {
1464            return Vec::new();
1465        }
1466        self.scroll_down(self.height)
1467    }
1468
1469    /// Scrolls up by one full page (viewport height).
1470    ///
1471    /// This method moves the viewport up by exactly the viewport height,
1472    /// effectively showing the previous "page" of content. This is the standard
1473    /// page-up operation found in most text viewers and editors.
1474    ///
1475    /// # Returns
1476    ///
1477    /// A vector of strings representing the newly visible lines that scrolled into view.
1478    /// Returns an empty vector if already at the top or no scrolling occurred.
1479    ///
1480    /// # Examples
1481    ///
1482    /// ```rust
1483    /// use bubbletea_widgets::viewport::Model;
1484    ///
1485    /// let mut viewport = Model::new(40, 5);
1486    /// viewport.set_content(&(1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1487    ///
1488    /// // Scroll to middle, then page up
1489    /// viewport.set_y_offset(10);
1490    /// let new_lines = viewport.page_up();
1491    /// let visible = viewport.visible_lines();
1492    /// assert_eq!(visible[0], "Line 6"); // Moved up by 5 lines
1493    /// ```
1494    ///
1495    /// Handling top boundary:
1496    /// ```rust
1497    /// use bubbletea_widgets::viewport::Model;
1498    ///
1499    /// let mut viewport = Model::new(40, 5);
1500    /// viewport.set_content("Line 1\nLine 2\nLine 3");
1501    ///
1502    /// // Already at top, page_up returns empty
1503    /// let result = viewport.page_up();
1504    /// assert!(result.is_empty());
1505    /// assert!(viewport.at_top());
1506    /// ```
1507    pub fn page_up(&mut self) -> Vec<String> {
1508        if self.at_top() {
1509            return Vec::new();
1510        }
1511        self.scroll_up(self.height)
1512    }
1513
1514    /// Scrolls down by half the viewport height.
1515    ///
1516    /// This method provides a more granular scrolling option than full page scrolling,
1517    /// moving the viewport down by half its height. This is commonly mapped to
1518    /// Ctrl+D in Vim-style navigation.
1519    ///
1520    /// # Returns
1521    ///
1522    /// A vector of strings representing the newly visible lines that scrolled into view.
1523    /// Returns an empty vector if already at the bottom or no scrolling occurred.
1524    ///
1525    /// # Examples
1526    ///
1527    /// ```rust
1528    /// use bubbletea_widgets::viewport::Model;
1529    ///
1530    /// let mut viewport = Model::new(40, 10); // Height of 10
1531    /// viewport.set_content(&(1..=30).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1532    ///
1533    /// // Half page down moves by 5 lines (10/2)
1534    /// viewport.half_page_down();
1535    /// let visible = viewport.visible_lines();
1536    /// assert_eq!(visible[0], "Line 6"); // Moved down 5 lines
1537    /// ```
1538    ///
1539    /// # Use Cases
1540    ///
1541    /// - **Gradual Navigation**: More controlled scrolling than full pages
1542    /// - **Vim Compatibility**: Matches Ctrl+D behavior
1543    /// - **Reading Flow**: Maintains better context when scrolling through text
1544    pub fn half_page_down(&mut self) -> Vec<String> {
1545        if self.at_bottom() {
1546            return Vec::new();
1547        }
1548        self.scroll_down(self.height / 2)
1549    }
1550
1551    /// Scrolls up by half the viewport height.
1552    ///
1553    /// This method provides a more granular scrolling option than full page scrolling,
1554    /// moving the viewport up by half its height. This is commonly mapped to
1555    /// Ctrl+U in Vim-style navigation.
1556    ///
1557    /// # Returns
1558    ///
1559    /// A vector of strings representing the newly visible lines that scrolled into view.
1560    /// Returns an empty vector if already at the top or no scrolling occurred.
1561    ///
1562    /// # Examples
1563    ///
1564    /// ```rust
1565    /// use bubbletea_widgets::viewport::Model;
1566    ///
1567    /// let mut viewport = Model::new(40, 10); // Height of 10
1568    /// viewport.set_content(&(1..=30).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1569    ///
1570    /// // Move to middle, then half page up
1571    /// viewport.set_y_offset(15);
1572    /// viewport.half_page_up();
1573    /// let visible = viewport.visible_lines();
1574    /// assert_eq!(visible[0], "Line 11"); // Moved up 5 lines (15-5+1)
1575    /// ```
1576    ///
1577    /// # Use Cases
1578    ///
1579    /// - **Gradual Navigation**: More controlled scrolling than full pages
1580    /// - **Vim Compatibility**: Matches Ctrl+U behavior  
1581    /// - **Reading Flow**: Maintains better context when scrolling through text
1582    pub fn half_page_up(&mut self) -> Vec<String> {
1583        if self.at_top() {
1584            return Vec::new();
1585        }
1586        self.scroll_up(self.height / 2)
1587    }
1588
1589    /// Scrolls down by the specified number of lines.
1590    ///
1591    /// This is the fundamental vertical scrolling method that moves the viewport
1592    /// down by the specified number of lines. All other downward scrolling methods
1593    /// (page_down, half_page_down) ultimately delegate to this method.
1594    ///
1595    /// # Arguments
1596    ///
1597    /// * `n` - Number of lines to scroll down
1598    ///
1599    /// # Returns
1600    ///
1601    /// A vector containing the newly visible lines for performance rendering.
1602    /// Returns empty vector if no scrolling occurred.
1603    ///
1604    /// # Examples
1605    ///
1606    /// ```rust
1607    /// use bubbletea_widgets::viewport::Model;
1608    ///
1609    /// let mut viewport = Model::new(40, 5);
1610    /// viewport.set_content(&(1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1611    ///
1612    /// // Scroll down 3 lines
1613    /// let new_lines = viewport.scroll_down(3);
1614    /// let visible = viewport.visible_lines();
1615    /// assert_eq!(visible[0], "Line 4"); // Now starting from line 4
1616    /// assert_eq!(new_lines.len(), 3); // 3 new lines scrolled in
1617    /// ```
1618    ///
1619    /// Edge case handling:
1620    /// ```rust
1621    /// use bubbletea_widgets::viewport::Model;
1622    ///
1623    /// let mut viewport = Model::new(40, 5);
1624    /// viewport.set_content("Line 1\nLine 2");
1625    ///
1626    /// // No scrolling at bottom
1627    /// viewport.goto_bottom();
1628    /// let result = viewport.scroll_down(5);
1629    /// assert!(result.is_empty());
1630    ///
1631    /// // No scrolling with n=0
1632    /// viewport.goto_top();
1633    /// let result = viewport.scroll_down(0);
1634    /// assert!(result.is_empty());
1635    /// ```
1636    ///
1637    /// # Performance Optimization
1638    ///
1639    /// The returned vector contains only the lines that scrolled into view,
1640    /// enabling efficient incremental rendering in terminal applications.
1641    /// This avoids re-rendering the entire viewport when only a few lines changed.
1642    ///
1643    /// # Boundary Behavior
1644    ///
1645    /// - Automatically stops at the bottom of content
1646    /// - Returns empty vector if already at bottom
1647    /// - Handles viewport larger than content gracefully
1648    pub fn scroll_down(&mut self, n: usize) -> Vec<String> {
1649        if self.at_bottom() || n == 0 || self.lines.is_empty() {
1650            return Vec::new();
1651        }
1652
1653        self.set_y_offset(self.y_offset + n);
1654
1655        // Gather lines for performance scrolling
1656        let bottom = (self.y_offset + self.height).min(self.lines.len());
1657        let top = (self.y_offset + self.height).saturating_sub(n).min(bottom);
1658        self.lines[top..bottom].to_vec()
1659    }
1660
1661    /// Scrolls up by the specified number of lines.
1662    ///
1663    /// This is the fundamental vertical scrolling method that moves the viewport
1664    /// up by the specified number of lines. All other upward scrolling methods
1665    /// (page_up, half_page_up) ultimately delegate to this method.
1666    ///
1667    /// # Arguments
1668    ///
1669    /// * `n` - Number of lines to scroll up
1670    ///
1671    /// # Returns
1672    ///
1673    /// A vector containing the newly visible lines for performance rendering.
1674    /// Returns empty vector if no scrolling occurred.
1675    ///
1676    /// # Examples
1677    ///
1678    /// ```rust
1679    /// use bubbletea_widgets::viewport::Model;
1680    ///
1681    /// let mut viewport = Model::new(40, 5);
1682    /// viewport.set_content(&(1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1683    ///
1684    /// // Start from middle
1685    /// viewport.set_y_offset(10);
1686    ///
1687    /// // Scroll up 3 lines
1688    /// let new_lines = viewport.scroll_up(3);
1689    /// let visible = viewport.visible_lines();
1690    /// assert_eq!(visible[0], "Line 8"); // Now starting from line 8 (10-3+1)
1691    /// assert_eq!(new_lines.len(), 3); // 3 new lines scrolled in
1692    /// ```
1693    ///
1694    /// Edge case handling:
1695    /// ```rust
1696    /// use bubbletea_widgets::viewport::Model;
1697    ///
1698    /// let mut viewport = Model::new(40, 5);
1699    /// viewport.set_content("Line 1\nLine 2");
1700    ///
1701    /// // No scrolling at top
1702    /// let result = viewport.scroll_up(5);
1703    /// assert!(result.is_empty());
1704    /// assert!(viewport.at_top());
1705    ///
1706    /// // No scrolling with n=0
1707    /// let result = viewport.scroll_up(0);
1708    /// assert!(result.is_empty());
1709    /// ```
1710    ///
1711    /// # Performance Optimization
1712    ///
1713    /// The returned vector contains only the lines that scrolled into view,
1714    /// enabling efficient incremental rendering. Applications can update only
1715    /// the changed portions of the display.
1716    ///
1717    /// # Boundary Behavior
1718    ///
1719    /// - Automatically stops at the top of content
1720    /// - Returns empty vector if already at top
1721    /// - Uses saturating subtraction to prevent underflow
1722    pub fn scroll_up(&mut self, n: usize) -> Vec<String> {
1723        if self.at_top() || n == 0 || self.lines.is_empty() {
1724            return Vec::new();
1725        }
1726
1727        self.set_y_offset(self.y_offset.saturating_sub(n));
1728
1729        // Gather lines for performance scrolling
1730        let top = self.y_offset;
1731        let bottom = (self.y_offset + n).min(self.max_y_offset());
1732        self.lines[top..bottom].to_vec()
1733    }
1734
1735    /// Jumps directly to the beginning of the content.
1736    ///
1737    /// This method immediately positions the viewport at the very top of the
1738    /// content, setting the vertical offset to 0. This is equivalent to pressing
1739    /// the "Home" key in most text viewers.
1740    ///
1741    /// # Examples
1742    ///
1743    /// ```rust
1744    /// use bubbletea_widgets::viewport::Model;
1745    ///
1746    /// let mut viewport = Model::new(40, 5);
1747    /// viewport.set_content(&(1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1748    ///
1749    /// // Scroll to middle first
1750    /// viewport.set_y_offset(10);
1751    /// assert!(!viewport.at_top());
1752    ///
1753    /// // Jump to top
1754    /// viewport.goto_top();
1755    /// assert!(viewport.at_top());
1756    ///
1757    /// let visible = viewport.visible_lines();
1758    /// assert_eq!(visible[0], "Line 1");
1759    /// ```
1760    ///
1761    /// # Use Cases
1762    ///
1763    /// - **Navigation shortcuts**: Quick return to document start
1764    /// - **Reset position**: Return to initial state after scrolling
1765    /// - **Search results**: Jump to first occurrence
1766    /// - **Content refresh**: Start from beginning after content changes
1767    pub fn goto_top(&mut self) {
1768        self.y_offset = 0;
1769    }
1770
1771    /// Jumps directly to the end of the content.
1772    ///
1773    /// This method immediately positions the viewport at the bottom of the
1774    /// content, showing the last possible page. This is equivalent to pressing
1775    /// the "End" key in most text viewers.
1776    ///
1777    /// # Examples
1778    ///
1779    /// ```rust
1780    /// use bubbletea_widgets::viewport::Model;
1781    ///
1782    /// let mut viewport = Model::new(40, 5);
1783    /// viewport.set_content(&(1..=20).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1784    ///
1785    /// // Jump to bottom
1786    /// viewport.goto_bottom();
1787    /// assert!(viewport.at_bottom());
1788    ///
1789    /// let visible = viewport.visible_lines();
1790    /// // With 20 lines total and height 5 (minus 2 for frame), bottom shows last 3 lines
1791    /// assert_eq!(visible[0], "Line 18");
1792    /// assert_eq!(visible[2], "Line 20");
1793    /// ```
1794    ///
1795    /// Auto-correction after content changes:
1796    /// ```rust
1797    /// use bubbletea_widgets::viewport::Model;
1798    ///
1799    /// let mut viewport = Model::new(40, 10);
1800    ///
1801    /// // Set initial content and scroll down
1802    /// viewport.set_content(&(1..=50).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
1803    /// viewport.set_y_offset(30);
1804    ///
1805    /// // Replace with shorter content
1806    /// viewport.set_content("Line 1\nLine 2\nLine 3");
1807    /// // goto_bottom() is called automatically to fix invalid offset
1808    /// assert!(viewport.at_bottom());
1809    /// ```
1810    ///
1811    /// # Use Cases
1812    ///
1813    /// - **Navigation shortcuts**: Quick jump to document end
1814    /// - **Log viewing**: Jump to latest entries
1815    /// - **Content appending**: Position for new content
1816    /// - **Auto-correction**: Fix invalid positions after content changes
1817    pub fn goto_bottom(&mut self) {
1818        self.y_offset = self.max_y_offset();
1819    }
1820
1821    /// Sets the horizontal scrolling step size in characters.
1822    ///
1823    /// This method configures how many characters the viewport scrolls
1824    /// horizontally with each left/right scroll operation. The step size
1825    /// affects both `scroll_left()` and `scroll_right()` methods.
1826    ///
1827    /// # Arguments
1828    ///
1829    /// * `step` - Number of characters to scroll per horizontal step
1830    ///
1831    /// # Examples
1832    ///
1833    /// ```rust
1834    /// use bubbletea_widgets::viewport::Model;
1835    ///
1836    /// let mut viewport = Model::new(20, 5);
1837    /// viewport.set_content("This is a very long line that extends far beyond the viewport width");
1838    ///
1839    /// // Default step is 1 character
1840    /// viewport.scroll_right();
1841    /// assert_eq!(viewport.x_offset, 1);
1842    ///
1843    /// // Set larger step for faster scrolling
1844    /// viewport.set_horizontal_step(5);
1845    /// viewport.scroll_right();
1846    /// assert_eq!(viewport.x_offset, 6); // 1 + 5
1847    /// ```
1848    ///
1849    /// Different step sizes for different content types:
1850    /// ```rust
1851    /// use bubbletea_widgets::viewport::Model;
1852    ///
1853    /// let mut viewport = Model::new(40, 10);
1854    ///
1855    /// // Fine scrolling for precise text viewing
1856    /// viewport.set_horizontal_step(1);
1857    ///
1858    /// // Coarse scrolling for wide data tables
1859    /// viewport.set_horizontal_step(8); // Tab-like steps
1860    ///
1861    /// // Word-based scrolling
1862    /// viewport.set_horizontal_step(4); // Average word length
1863    /// ```
1864    ///
1865    /// # Use Cases
1866    ///
1867    /// - **Fine Control**: Single-character precision (step=1)
1868    /// - **Tab Columns**: Align with tab stops (step=4 or 8)
1869    /// - **Word Navigation**: Approximate word-based scrolling
1870    /// - **Performance**: Larger steps for faster navigation of wide content
1871    pub fn set_horizontal_step(&mut self, step: usize) {
1872        self.horizontal_step = step;
1873    }
1874
1875    /// Scrolls the viewport left by the configured horizontal step.
1876    ///
1877    /// This method moves the horizontal view to the left, revealing content
1878    /// that was previously hidden on the left side. The scroll amount is
1879    /// determined by the `horizontal_step` setting.
1880    ///
1881    /// # Examples
1882    ///
1883    /// ```rust
1884    /// use bubbletea_widgets::viewport::Model;
1885    ///
1886    /// let mut viewport = Model::new(10, 3);
1887    /// viewport.set_content("This is a very long line that needs horizontal scrolling");
1888    ///
1889    /// // Scroll right first to see the effect of scrolling left
1890    /// viewport.scroll_right();
1891    /// viewport.scroll_right();
1892    /// assert_eq!(viewport.x_offset, 2);
1893    ///
1894    /// // Scroll left
1895    /// viewport.scroll_left();
1896    /// assert_eq!(viewport.x_offset, 1);
1897    /// ```
1898    ///
1899    /// Boundary handling:
1900    /// ```rust
1901    /// use bubbletea_widgets::viewport::Model;
1902    ///
1903    /// let mut viewport = Model::new(20, 5);
1904    /// viewport.set_content("Short content");
1905    ///
1906    /// // Already at leftmost position, scroll_left has no effect
1907    /// assert_eq!(viewport.x_offset, 0);
1908    /// viewport.scroll_left();
1909    /// assert_eq!(viewport.x_offset, 0); // Still 0, can't scroll further left
1910    /// ```
1911    ///
1912    /// # Behavior
1913    ///
1914    /// - **Boundary Safe**: Uses saturating subtraction to prevent underflow
1915    /// - **Step-based**: Scrolls by `horizontal_step` amount
1916    /// - **Immediate**: Takes effect immediately, no animation
1917    /// - **Absolute Minimum**: Cannot scroll past offset 0 (leftmost position)
1918    pub fn scroll_left(&mut self) {
1919        self.x_offset = self.x_offset.saturating_sub(self.horizontal_step);
1920    }
1921
1922    /// Scrolls the viewport right by the configured horizontal step.
1923    ///
1924    /// This method moves the horizontal view to the right, revealing content
1925    /// that was previously hidden on the right side. The scroll amount is
1926    /// determined by the `horizontal_step` setting, and scrolling is limited
1927    /// by the longest line in the content.
1928    ///
1929    /// # Examples
1930    ///
1931    /// ```rust
1932    /// use bubbletea_widgets::viewport::Model;
1933    ///
1934    /// let mut viewport = Model::new(10, 3);
1935    /// viewport.set_content("This is a very long line that needs horizontal scrolling");
1936    ///
1937    /// // Initial view shows "This is " (width 10 minus 2 for frame = 8 chars)
1938    /// let visible = viewport.visible_lines();
1939    /// assert_eq!(visible[0].len(), 8);
1940    ///
1941    /// // Scroll right to see more
1942    /// viewport.scroll_right();
1943    /// let visible = viewport.visible_lines();
1944    /// // Now shows "his is a v" (shifted 1 character right)
1945    /// ```
1946    ///
1947    /// Boundary handling:
1948    /// ```rust
1949    /// use bubbletea_widgets::viewport::Model;
1950    ///
1951    /// let mut viewport = Model::new(20, 5);
1952    /// viewport.set_content("Short"); // Line shorter than viewport
1953    ///
1954    /// // Cannot scroll right when content fits in viewport
1955    /// viewport.scroll_right();
1956    /// assert_eq!(viewport.x_offset, 0); // No change
1957    /// ```
1958    ///
1959    /// Multiple step sizes:
1960    /// ```rust
1961    /// use bubbletea_widgets::viewport::Model;
1962    ///
1963    /// let mut viewport = Model::new(10, 3);
1964    /// viewport.set_content("Very long line for testing horizontal scrolling behavior");
1965    ///
1966    /// // Default single-character scrolling
1967    /// viewport.scroll_right();
1968    /// assert_eq!(viewport.x_offset, 1);
1969    ///
1970    /// // Change to larger steps
1971    /// viewport.set_horizontal_step(5);
1972    /// viewport.scroll_right();
1973    /// assert_eq!(viewport.x_offset, 6); // 1 + 5
1974    /// ```
1975    ///
1976    /// # Behavior
1977    ///
1978    /// - **Content-aware**: Maximum scroll is based on longest line width
1979    /// - **Viewport-relative**: Considers viewport width in maximum calculation
1980    /// - **Step-based**: Scrolls by `horizontal_step` amount
1981    /// - **Clamped**: Cannot scroll past the rightmost useful position
1982    pub fn scroll_right(&mut self) {
1983        let max_offset = self.longest_line_width.saturating_sub(self.width);
1984        self.x_offset = (self.x_offset + self.horizontal_step).min(max_offset);
1985    }
1986
1987    /// Get the maximum Y offset
1988    fn max_y_offset(&self) -> usize {
1989        let frame_size = self.style.get_vertical_frame_size();
1990        self.lines
1991            .len()
1992            .saturating_sub(self.height.saturating_sub(frame_size as usize))
1993    }
1994
1995    /// Returns a reference to the internal content lines.
1996    ///
1997    /// This method provides read-only access to all content lines stored in the viewport.
1998    /// Useful for inspection, searching, or analysis of content without copying.
1999    ///
2000    /// # Returns
2001    ///
2002    /// A slice containing all content lines as strings
2003    ///
2004    /// # Examples
2005    ///
2006    /// ```rust
2007    /// use bubbletea_widgets::viewport::Model;
2008    ///
2009    /// let mut viewport = Model::new(40, 10);
2010    /// viewport.set_content("Line 1\nLine 2\nLine 3");
2011    ///
2012    /// let lines = viewport.lines();
2013    /// assert_eq!(lines.len(), 3);
2014    /// assert_eq!(lines[0], "Line 1");
2015    /// assert_eq!(lines[2], "Line 3");
2016    /// ```
2017    ///
2018    /// Content inspection and search:
2019    /// ```rust
2020    /// use bubbletea_widgets::viewport::Model;
2021    ///
2022    /// let mut viewport = Model::new(40, 10);
2023    /// viewport.set_content("Line 1\nImportant line\nLine 3");
2024    ///
2025    /// // Search for specific content
2026    /// let lines = viewport.lines();
2027    /// let important_line = lines.iter().find(|line| line.contains("Important"));
2028    /// assert!(important_line.is_some());
2029    /// ```
2030    pub fn lines(&self) -> &[String] {
2031        &self.lines
2032    }
2033
2034    /// Returns the total number of content lines.
2035    ///
2036    /// This method returns the count of all content lines, regardless of viewport
2037    /// dimensions or scroll position. Useful for determining content size,
2038    /// calculating scroll percentages, or implementing navigation features.
2039    ///
2040    /// # Returns
2041    ///
2042    /// The total number of lines in the content
2043    ///
2044    /// # Examples
2045    ///
2046    /// ```rust
2047    /// use bubbletea_widgets::viewport::Model;
2048    ///
2049    /// let mut viewport = Model::new(40, 10);
2050    /// viewport.set_content("Line 1\nLine 2\nLine 3\nLine 4\nLine 5");
2051    ///
2052    /// assert_eq!(viewport.line_count(), 5);
2053    ///
2054    /// // Empty content
2055    /// viewport.set_content("");
2056    /// assert_eq!(viewport.line_count(), 1); // Empty string creates one empty line
2057    /// ```
2058    ///
2059    /// Navigation calculations:
2060    /// ```rust
2061    /// use bubbletea_widgets::viewport::Model;
2062    ///
2063    /// let mut viewport = Model::new(40, 10);
2064    /// viewport.set_content(&(1..=100).map(|i| format!("Line {}", i)).collect::<Vec<_>>().join("\n"));
2065    ///
2066    /// let total_lines = viewport.line_count();
2067    /// let viewport_height = viewport.height;
2068    ///
2069    /// // Calculate if scrolling is needed
2070    /// let needs_scrolling = total_lines > viewport_height;
2071    /// assert!(needs_scrolling);
2072    ///
2073    /// // Calculate maximum number of pages
2074    /// let max_pages = (total_lines + viewport_height - 1) / viewport_height;
2075    /// assert_eq!(max_pages, 10); // 100 lines / 10 height = 10 pages
2076    /// ```
2077    pub fn line_count(&self) -> usize {
2078        self.lines.len()
2079    }
2080}
2081
2082impl Default for Model {
2083    /// Creates a default viewport with standard terminal dimensions.
2084    ///
2085    /// The default viewport is sized for typical terminal windows (80x24) and
2086    /// includes all default configuration options. This is equivalent to calling
2087    /// `Model::new(80, 24)`.
2088    ///
2089    /// # Examples
2090    ///
2091    /// ```rust
2092    /// use bubbletea_widgets::viewport::Model;
2093    ///
2094    /// let viewport = Model::default();
2095    /// assert_eq!(viewport.width, 80);
2096    /// assert_eq!(viewport.height, 24);
2097    /// assert!(viewport.mouse_wheel_enabled);
2098    /// ```
2099    ///
2100    /// # Default Configuration
2101    ///
2102    /// - **Dimensions**: 80 characters × 24 lines (standard terminal size)
2103    /// - **Mouse wheel**: Enabled with 3-line scroll delta
2104    /// - **Scroll position**: At top-left (0, 0)
2105    /// - **Horizontal step**: 1 character per scroll
2106    /// - **Style**: No styling applied
2107    /// - **Key bindings**: Vim-style with arrow key alternatives
2108    fn default() -> Self {
2109        Self::new(80, 24)
2110    }
2111}
2112
2113impl BubbleTeaModel for Model {
2114    /// Initializes a new viewport instance for Bubble Tea applications.
2115    ///
2116    /// Creates a default viewport with standard terminal dimensions and no initial commands.
2117    /// This follows the Bubble Tea initialization pattern where components return their
2118    /// initial state and any startup commands.
2119    ///
2120    /// # Returns
2121    ///
2122    /// A tuple containing:
2123    /// - A default viewport instance (80x24)
2124    /// - `None` (no initialization commands needed)
2125    ///
2126    /// # Examples
2127    ///
2128    /// ```rust
2129    /// use bubbletea_widgets::viewport::Model;
2130    /// use bubbletea_rs::Model as BubbleTeaModel;
2131    ///
2132    /// let (viewport, cmd) = Model::init();
2133    /// assert_eq!(viewport.width, 80);
2134    /// assert_eq!(viewport.height, 24);
2135    /// assert!(cmd.is_none());
2136    /// ```
2137    fn init() -> (Self, Option<Cmd>) {
2138        (Self::default(), None)
2139    }
2140
2141    /// Processes messages and updates viewport state.
2142    ///
2143    /// This method handles keyboard input for viewport navigation, implementing
2144    /// the standard Bubble Tea update pattern. It processes key messages against
2145    /// the configured key bindings and updates the viewport scroll position accordingly.
2146    ///
2147    /// # Arguments
2148    ///
2149    /// * `msg` - The message to process (typically keyboard input)
2150    ///
2151    /// # Returns
2152    ///
2153    /// Always returns `None` as viewport operations don't generate commands
2154    ///
2155    /// # Supported Key Bindings
2156    ///
2157    /// The default key bindings include:
2158    /// - **Page navigation**: `f`/`PgDn`/`Space` (page down), `b`/`PgUp` (page up)
2159    /// - **Half-page navigation**: `d` (half page down), `u` (half page up)
2160    /// - **Line navigation**: `j`/`↓` (line down), `k`/`↑` (line up)
2161    /// - **Horizontal navigation**: `l`/`→` (scroll right), `h`/`←` (scroll left)
2162    ///
2163    /// # Examples
2164    ///
2165    /// ```rust
2166    /// use bubbletea_widgets::viewport::Model;
2167    /// use bubbletea_rs::{Model as BubbleTeaModel, KeyMsg};
2168    /// use crossterm::event::{KeyCode, KeyModifiers};
2169    ///
2170    /// let mut viewport = Model::default();
2171    /// viewport.set_content("Line 1\nLine 2\nLine 3\nLine 4\nLine 5");
2172    ///
2173    /// // Simulate pressing 'j' to scroll down
2174    /// let key_msg = KeyMsg {
2175    ///     key: KeyCode::Char('j'),
2176    ///     modifiers: KeyModifiers::NONE,
2177    /// };
2178    ///
2179    /// let cmd = viewport.update(Box::new(key_msg));
2180    /// assert!(cmd.is_none());
2181    /// ```
2182    ///
2183    /// # Integration with Bubble Tea
2184    ///
2185    /// This method integrates seamlessly with Bubble Tea's message-driven architecture:
2186    /// ```rust
2187    /// use bubbletea_widgets::viewport::Model;
2188    /// use bubbletea_rs::{Model as BubbleTeaModel, Msg};
2189    ///
2190    /// struct App {
2191    ///     viewport: Model,
2192    /// }
2193    ///
2194    /// impl BubbleTeaModel for App {
2195    /// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) {
2196    /// #       (Self { viewport: Model::new(80, 24) }, None)
2197    /// #   }
2198    ///     
2199    ///     fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
2200    ///         // Forward messages to viewport
2201    ///         self.viewport.update(msg);
2202    ///         None
2203    ///     }
2204    /// #
2205    /// #   fn view(&self) -> String { self.viewport.view() }
2206    /// }
2207    /// ```
2208    fn update(&mut self, msg: Msg) -> Option<Cmd> {
2209        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2210            if self.keymap.page_down.matches(key_msg) {
2211                self.page_down();
2212            } else if self.keymap.page_up.matches(key_msg) {
2213                self.page_up();
2214            } else if self.keymap.half_page_down.matches(key_msg) {
2215                self.half_page_down();
2216            } else if self.keymap.half_page_up.matches(key_msg) {
2217                self.half_page_up();
2218            } else if self.keymap.down.matches(key_msg) {
2219                self.scroll_down(1);
2220            } else if self.keymap.up.matches(key_msg) {
2221                self.scroll_up(1);
2222            } else if self.keymap.left.matches(key_msg) {
2223                self.scroll_left();
2224            } else if self.keymap.right.matches(key_msg) {
2225                self.scroll_right();
2226            }
2227        }
2228        // Mouse wheel basic support if MouseMsg is available in bubbletea-rs
2229        // Note: bubbletea-rs MouseMsg does not currently expose wheel events in this crate version.
2230        None
2231    }
2232
2233    /// Renders the viewport content as a styled string.
2234    ///
2235    /// This method generates the visual representation of the viewport by retrieving
2236    /// the currently visible lines and applying any configured lipgloss styling.
2237    /// The output is ready for display in a terminal interface.
2238    ///
2239    /// # Returns
2240    ///
2241    /// A styled string containing the visible content, ready for terminal output
2242    ///
2243    /// # Examples
2244    ///
2245    /// ```rust
2246    /// use bubbletea_widgets::viewport::Model;
2247    /// use bubbletea_rs::Model as BubbleTeaModel;
2248    ///
2249    /// let mut viewport = Model::new(20, 5);
2250    /// viewport.set_content("Line 1\nLine 2\nLine 3\nLine 4");
2251    ///
2252    /// let output = viewport.view();
2253    /// assert!(output.contains("Line 1"));
2254    /// assert!(output.contains("Line 2"));
2255    /// assert!(output.contains("Line 3"));
2256    /// assert!(!output.contains("Line 4")); // Not visible in 5-line viewport (3 effective)
2257    /// ```
2258    ///
2259    /// With styling applied:
2260    /// ```rust
2261    /// use bubbletea_widgets::viewport::Model;
2262    /// use bubbletea_rs::Model as BubbleTeaModel;
2263    /// use lipgloss_extras::prelude::*;
2264    ///
2265    /// let mut viewport = Model::new(20, 3)
2266    ///     .with_style(
2267    ///         Style::new()
2268    ///             .foreground(Color::from("#FF0000"))
2269    ///             .background(Color::from("#000000"))
2270    ///     );
2271    ///
2272    /// viewport.set_content("Styled content");
2273    /// let styled_output = viewport.view();
2274    /// // Output includes ANSI escape codes for styling
2275    /// ```
2276    ///
2277    /// # Rendering Behavior
2278    ///
2279    /// - **Visible Lines Only**: Only renders content within the current viewport
2280    /// - **Horizontal Clipping**: Content wider than viewport is clipped appropriately  
2281    /// - **Style Application**: Applied lipgloss styles are rendered into the output
2282    /// - **Line Joining**: Multiple lines are joined with newline characters
2283    /// - **Frame Accounting**: Styling frame sizes are automatically considered
2284    fn view(&self) -> String {
2285        let visible = self.visible_lines();
2286        let mut output = String::new();
2287
2288        for (i, line) in visible.iter().enumerate() {
2289            if i > 0 {
2290                output.push('\n');
2291            }
2292            output.push_str(line);
2293        }
2294
2295        // Apply style if set
2296        self.style.render(&output)
2297    }
2298}
2299
2300/// Calculates the display width of the longest line in a collection.
2301///
2302/// This internal helper function determines the maximum display width among all
2303/// provided lines, using proper Unicode width calculation via the `lg_width` function.
2304/// This is essential for horizontal scrolling calculations and determining the
2305/// maximum horizontal scroll offset.
2306///
2307/// # Arguments
2308///
2309/// * `lines` - A slice of strings to measure
2310///
2311/// # Returns
2312///
2313/// The width in characters of the widest line, or 0 if no lines provided
2314///
2315/// # Implementation Notes
2316///
2317/// - Uses `lg_width()` for proper Unicode width calculation
2318/// - Handles empty collections gracefully
2319/// - Accounts for wide characters (CJK, emojis, etc.)
2320fn find_longest_line_width(lines: &[String]) -> usize {
2321    lines.iter().map(|line| lg_width(line)).max().unwrap_or(0)
2322}
2323
2324/// Extracts a substring based on display width positions for horizontal scrolling.
2325///
2326/// This internal helper function cuts a string to show only the portion between
2327/// specified display width positions. It properly handles Unicode characters with
2328/// varying display widths, making it essential for horizontal scrolling in the viewport.
2329///
2330/// # Arguments
2331///
2332/// * `s` - The source string to cut
2333/// * `start` - The starting display width position (inclusive)
2334/// * `end` - The ending display width position (exclusive)
2335///
2336/// # Returns
2337///
2338/// A string containing only the characters within the specified width range
2339///
2340/// # Implementation Details
2341///
2342/// - **Unicode-aware**: Properly handles wide characters (CJK, emojis)
2343/// - **Width-based**: Uses display width, not character count
2344/// - **Boundary safe**: Returns empty string if start is beyond string width
2345/// - **Performance optimized**: Single pass through characters when possible
2346///
2347/// # Examples (Internal Use)
2348///
2349/// ```ignore
2350/// // Wide characters take 2 display columns
2351/// let result = cut_string("Hello 世界 World", 3, 8);
2352/// // Shows characters from display column 3 to 7
2353/// ```
2354fn cut_string(s: &str, start: usize, end: usize) -> String {
2355    if start >= lg_width(s) {
2356        return String::new();
2357    }
2358
2359    let chars: Vec<char> = s.chars().collect();
2360    let mut current_width = 0;
2361    let mut start_idx = 0;
2362    let mut end_idx = chars.len();
2363
2364    // Find start index
2365    for (i, &ch) in chars.iter().enumerate() {
2366        if current_width >= start {
2367            start_idx = i;
2368            break;
2369        }
2370        current_width += ch.width().unwrap_or(0);
2371    }
2372
2373    // Find end index
2374    current_width = 0;
2375    for (i, &ch) in chars.iter().enumerate() {
2376        if current_width >= end {
2377            end_idx = i;
2378            break;
2379        }
2380        current_width += ch.width().unwrap_or(0);
2381    }
2382
2383    chars[start_idx..end_idx].iter().collect()
2384}
2385
2386/// Creates a new viewport with the specified dimensions.
2387///
2388/// This is a convenience function that creates a new viewport instance.
2389/// It's equivalent to calling `Model::new(width, height)` directly, but
2390/// provides a more functional style API that some users may prefer.
2391///
2392/// # Arguments
2393///
2394/// * `width` - Display width in characters
2395/// * `height` - Display height in lines
2396///
2397/// # Returns
2398///
2399/// A new viewport `Model` configured with the specified dimensions
2400///
2401/// # Examples
2402///
2403/// ```rust
2404/// use bubbletea_widgets::viewport;
2405///
2406/// // Functional style
2407/// let viewport = viewport::new(80, 24);
2408///
2409/// // Equivalent to:
2410/// let viewport = viewport::Model::new(80, 24);
2411/// ```
2412///
2413/// # Use Cases
2414///
2415/// - **Functional Style**: When preferring function calls over constructors
2416/// - **Import Convenience**: Shorter syntax with `use bubbletea_widgets::viewport::new`
2417/// - **API Consistency**: Matches the pattern used by other bubbles components
2418pub fn new(width: usize, height: usize) -> Model {
2419    Model::new(width, height)
2420}