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