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
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_extras::prelude::*;
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_extras::prelude::*;
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_extras::prelude::*;
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_extras::prelude::*;
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_extras::prelude::*;
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_extras::prelude::*;
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_extras::prelude::*;
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_extras::prelude::*;
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}