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