bubbletea_rs/gradient.rs
1//! # Gradient Utilities
2//!
3//! This module provides high-performance utilities for rendering gradient-colored text
4//! in terminal applications, specifically designed for progress bars and other UI elements
5//! in the Bubble Tea TUI framework.
6//!
7//! The module focuses on performance by manually generating ANSI escape sequences rather
8//! than using higher-level styling libraries, making it suitable for real-time rendering
9//! of animated progress bars and other dynamic UI elements.
10//!
11//! ## Features
12//!
13//! - Fast RGB color interpolation for smooth gradients
14//! - Optimized ANSI escape sequence generation
15//! - Buffer reuse support for high-frequency rendering
16//! - Charm Bubble Tea compatible default gradient colors
17//!
18//! ## Example
19//!
20//! ```rust
21//! use bubbletea_rs::gradient::{gradient_filled_segment, charm_default_gradient, lerp_rgb};
22//!
23//! // Create a gradient progress bar
24//! let progress_bar = gradient_filled_segment(20, '█');
25//! println!("{}", progress_bar);
26//!
27//! // Get the default Charm gradient colors
28//! let (start, end) = charm_default_gradient();
29//! println!("Start: RGB({}, {}, {})", start.0, start.1, start.2);
30//!
31//! // Interpolate between colors
32//! let mid_color = lerp_rgb(start, end, 0.5);
33//! println!("Mid-point: RGB({}, {}, {})", mid_color.0, mid_color.1, mid_color.2);
34//! ```
35
36// Note: Crossterm style imports removed for manual ANSI sequence generation for better performance
37
38/// Fast u8 to string conversion without allocations.
39///
40/// This function manually converts a u8 value (0-255) to its decimal string representation
41/// and appends it directly to the provided string buffer. This avoids the overhead of
42/// format macros and temporary allocations when building ANSI escape sequences.
43///
44/// # Arguments
45///
46/// * `s` - The string buffer to append the decimal representation to
47/// * `value` - The u8 value to convert (0-255)
48///
49/// # Performance Notes
50///
51/// This function is optimized for the specific use case of generating ANSI color codes
52/// where we frequently need to convert RGB values (0-255) to strings. It uses a
53/// stack-allocated array to build the digits and avoids any heap allocations.
54///
55/// # Examples
56///
57/// ```rust,ignore
58/// // This is a private function used internally for ANSI sequence generation
59/// let mut buffer = String::new();
60/// write_u8_to_string(&mut buffer, 255);
61/// assert_eq!(buffer, "255");
62///
63/// buffer.clear();
64/// write_u8_to_string(&mut buffer, 0);
65/// assert_eq!(buffer, "0");
66/// ```
67#[inline]
68fn write_u8_to_string(s: &mut String, mut value: u8) {
69 if value == 0 {
70 s.push('0');
71 return;
72 }
73
74 // Convert to decimal digits (at most 3 digits for u8)
75 let mut digits = [0u8; 3];
76 let mut count = 0;
77
78 while value > 0 {
79 digits[count] = value % 10;
80 value /= 10;
81 count += 1;
82 }
83
84 // Push digits in reverse order (most significant first)
85 for i in (0..count).rev() {
86 s.push((b'0' + digits[i]) as char);
87 }
88}
89
90/// Returns the default gradient color endpoints used by Charm's Bubble Tea framework.
91///
92/// This function provides the standard gradient colors used in Bubble Tea progress bars
93/// and other UI elements, ensuring visual consistency with the original Go implementation.
94/// The gradient transitions from a pink-purple color to a bright yellow-green.
95///
96/// # Returns
97///
98/// A tuple containing two RGB color tuples:
99/// - First tuple: Start color `#FF7CCB` (255, 124, 203) - pink-purple
100/// - Second tuple: End color `#FDFF8C` (253, 255, 140) - yellow-green
101///
102/// # Examples
103///
104/// ```rust
105/// use bubbletea_rs::gradient::charm_default_gradient;
106///
107/// let (start, end) = charm_default_gradient();
108/// assert_eq!(start, (0xFF, 0x7C, 0xCB)); // #FF7CCB
109/// assert_eq!(end, (0xFD, 0xFF, 0x8C)); // #FDFF8C
110///
111/// println!("Start: RGB({}, {}, {})", start.0, start.1, start.2);
112/// println!("End: RGB({}, {}, {})", end.0, end.1, end.2);
113/// ```
114///
115/// # See Also
116///
117/// - [`lerp_rgb`] - For interpolating between these colors
118/// - [`gradient_filled_segment`] - For creating gradient text using these colors
119#[inline]
120pub fn charm_default_gradient() -> ((u8, u8, u8), (u8, u8, u8)) {
121 ((0xFF, 0x7C, 0xCB), (0xFD, 0xFF, 0x8C))
122}
123
124/// Performs linear interpolation between two RGB colors.
125///
126/// This function computes an intermediate RGB color at position `t` along the linear
127/// path between the `start` and `end` colors. The interpolation is performed separately
128/// for each color channel (red, green, blue) and the results are rounded to the nearest
129/// integer values.
130///
131/// # Arguments
132///
133/// * `start` - The starting RGB color as a tuple of (red, green, blue) values (0-255)
134/// * `end` - The ending RGB color as a tuple of (red, green, blue) values (0-255)
135/// * `t` - The interpolation parameter, where 0.0 returns `start`, 1.0 returns `end`,
136/// and values in between return interpolated colors. Values outside \[0,1\] are clamped.
137///
138/// # Returns
139///
140/// An RGB color tuple representing the interpolated color at position `t`.
141///
142/// # Examples
143///
144/// ```rust
145/// use bubbletea_rs::gradient::lerp_rgb;
146///
147/// let red = (255, 0, 0);
148/// let blue = (0, 0, 255);
149///
150/// // Get the starting color
151/// let start = lerp_rgb(red, blue, 0.0);
152/// assert_eq!(start, (255, 0, 0));
153///
154/// // Get the ending color
155/// let end = lerp_rgb(red, blue, 1.0);
156/// assert_eq!(end, (0, 0, 255));
157///
158/// // Get a color halfway between red and blue (purple)
159/// let middle = lerp_rgb(red, blue, 0.5);
160/// assert_eq!(middle, (128, 0, 128));
161///
162/// // Values outside [0,1] are clamped
163/// let clamped = lerp_rgb(red, blue, 2.0);
164/// assert_eq!(clamped, (0, 0, 255)); // Same as t=1.0
165/// ```
166///
167/// # Performance Notes
168///
169/// This function uses floating-point arithmetic for interpolation and rounds the final
170/// results. It's optimized for gradient generation where smooth color transitions are
171/// more important than absolute performance.
172#[inline]
173pub fn lerp_rgb(start: (u8, u8, u8), end: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
174 let t = t.clamp(0.0, 1.0);
175 let r = (start.0 as f64 + (end.0 as f64 - start.0 as f64) * t).round() as u8;
176 let g = (start.1 as f64 + (end.1 as f64 - start.1 as f64) * t).round() as u8;
177 let b = (start.2 as f64 + (end.2 as f64 - start.2 as f64) * t).round() as u8;
178 (r, g, b)
179}
180
181/// Creates a gradient-colored text segment for terminal display.
182///
183/// This function generates a string containing a gradient-colored sequence of characters
184/// using ANSI escape codes. Each character is colored using linear interpolation between
185/// Charm's default gradient colors, creating a smooth color transition from left to right.
186/// This is commonly used for progress bars, loading indicators, and other visual elements
187/// in terminal user interfaces.
188///
189/// # Arguments
190///
191/// * `filled_width` - The number of characters to include in the gradient segment.
192/// If 0, returns an empty string.
193/// * `ch` - The character to repeat for each position in the gradient (commonly '█', '▓', etc.)
194///
195/// # Returns
196///
197/// A `String` containing the gradient-colored characters with embedded ANSI escape sequences.
198/// Each character includes both the foreground color code and a reset sequence.
199///
200/// # Performance Notes
201///
202/// This function pre-allocates string capacity and manually constructs ANSI sequences
203/// to avoid the overhead of format macros and style library allocations. It's optimized
204/// for repeated use in animation loops and real-time rendering.
205///
206/// # Examples
207///
208/// ```rust
209/// use bubbletea_rs::gradient::gradient_filled_segment;
210///
211/// // Create a 10-character gradient progress bar
212/// let progress = gradient_filled_segment(10, '█');
213/// println!("{}", progress);
214///
215/// // Create a loading spinner segment
216/// let spinner = gradient_filled_segment(5, '▓');
217/// print!("Loading: {}\r", spinner);
218///
219/// // Empty width returns empty string
220/// let empty = gradient_filled_segment(0, '█');
221/// assert_eq!(empty, "");
222/// ```
223///
224/// # ANSI Escape Sequence Format
225///
226/// Each character in the output follows the pattern:
227/// `\x1b[38;2;r;g;bm{char}\x1b[0m`
228///
229/// Where:
230/// - `\x1b[38;2;r;g;bm` sets the foreground color to RGB(r,g,b)
231/// - `{char}` is the specified character
232/// - `\x1b[0m` resets all formatting
233///
234/// # See Also
235///
236/// - [`gradient_filled_segment_with_buffer`] - Buffer-reusing variant for better performance
237/// - [`charm_default_gradient`] - The gradient colors used by this function
238/// - [`lerp_rgb`] - The color interpolation function used internally
239pub fn gradient_filled_segment(filled_width: usize, ch: char) -> String {
240 let (start, end) = charm_default_gradient();
241 if filled_width == 0 {
242 return String::new();
243 }
244
245 // Pre-allocate with better capacity estimation
246 // ANSI color codes are typically ~19 bytes: \x1b[38;2;r;g;bmCHAR\x1b[0m
247 let estimated_capacity = filled_width * 25; // 25 bytes per colored char (with some padding)
248 let mut s = String::with_capacity(estimated_capacity);
249
250 for i in 0..filled_width {
251 let t = if filled_width <= 1 {
252 0.0
253 } else {
254 i as f64 / (filled_width - 1) as f64
255 };
256 let (r, g, b) = lerp_rgb(start, end, t);
257
258 // Manually construct ANSI escape sequence to avoid style() allocations
259 // Format: \x1b[38;2;r;g;bm{char}\x1b[0m
260 s.push_str("\x1b[38;2;");
261 write_u8_to_string(&mut s, r);
262 s.push(';');
263 write_u8_to_string(&mut s, g);
264 s.push(';');
265 write_u8_to_string(&mut s, b);
266 s.push('m');
267 s.push(ch);
268 s.push_str("\x1b[0m"); // Reset color
269 }
270 s
271}
272
273/// Creates a gradient-colored text segment using a reusable buffer for optimal performance.
274///
275/// This is a buffer-reusing variant of [`gradient_filled_segment`] designed for scenarios
276/// where gradient segments are generated frequently, such as animated progress bars or
277/// real-time UI updates. By reusing the same buffer, this function eliminates repeated
278/// heap allocations and improves performance in tight rendering loops.
279///
280/// # Arguments
281///
282/// * `filled_width` - The number of characters to include in the gradient segment.
283/// If 0, the function clears the buffer and returns an empty string reference.
284/// * `ch` - The character to repeat for each position in the gradient (commonly '█', '▓', etc.)
285/// * `buffer` - A mutable reference to a `String` that will be cleared and used to build
286/// the gradient segment. The buffer's capacity is preserved and extended if needed.
287///
288/// # Returns
289///
290/// A string slice (`&str`) reference to the buffer's contents containing the gradient-colored
291/// characters with embedded ANSI escape sequences.
292///
293/// # Performance Benefits
294///
295/// - **No allocations**: Reuses the provided buffer's existing capacity
296/// - **Reduced fragmentation**: Avoids creating temporary strings
297/// - **Cache efficiency**: Better memory locality when used in loops
298/// - **Optimal for animation**: Perfect for 60fps+ rendering scenarios
299///
300/// # Examples
301///
302/// ```rust
303/// use bubbletea_rs::gradient::gradient_filled_segment_with_buffer;
304///
305/// let mut buffer = String::new();
306///
307/// // Simulate an animated progress bar
308/// for progress in 0..=10 {
309/// let segment = gradient_filled_segment_with_buffer(progress, '█', &mut buffer);
310/// println!("Progress: [{}{}]", segment, " ".repeat(10 - progress));
311/// // Buffer is automatically reused for the next iteration
312/// }
313///
314/// // The buffer retains its capacity for future use
315/// assert!(buffer.capacity() >= 250); // Approximate capacity after 10 characters
316/// ```
317///
318/// # Usage Patterns
319///
320/// ```rust,no_run
321/// use bubbletea_rs::gradient::gradient_filled_segment_with_buffer;
322///
323/// // Pattern 1: Reuse buffer in animation loop
324/// let mut gradient_buffer = String::new();
325/// # fn calculate_progress_width() -> usize { 10 }
326/// # fn render_ui_with_progress_bar(bar: &str) { println!("{}", bar); }
327/// loop {
328/// let width = calculate_progress_width();
329/// let bar = gradient_filled_segment_with_buffer(width, '█', &mut gradient_buffer);
330/// render_ui_with_progress_bar(bar);
331/// # break; // Prevent infinite loop in doc test
332/// }
333///
334/// // Pattern 2: Multiple gradient elements with separate buffers
335/// let mut bar_buffer = String::new();
336/// let mut spinner_buffer = String::new();
337///
338/// let progress_bar = gradient_filled_segment_with_buffer(15, '█', &mut bar_buffer);
339/// let loading_spinner = gradient_filled_segment_with_buffer(3, '▓', &mut spinner_buffer);
340/// ```
341///
342/// # Buffer Management
343///
344/// - The buffer is cleared on each call but its capacity is preserved
345/// - If the buffer's capacity is insufficient, it will be extended as needed
346/// - The buffer can be reused indefinitely across multiple calls
347/// - For optimal performance, pre-allocate buffer capacity if the maximum width is known
348///
349/// # See Also
350///
351/// - [`gradient_filled_segment`] - Single-use variant that returns an owned String
352/// - [`charm_default_gradient`] - The gradient colors used by this function
353/// - [`lerp_rgb`] - The color interpolation function used internally
354pub fn gradient_filled_segment_with_buffer(
355 filled_width: usize,
356 ch: char,
357 buffer: &mut String,
358) -> &str {
359 buffer.clear();
360
361 let (start, end) = charm_default_gradient();
362 if filled_width == 0 {
363 return buffer;
364 }
365
366 // Reserve capacity for the gradient
367 let estimated_capacity = filled_width * 25;
368 buffer.reserve(estimated_capacity);
369
370 for i in 0..filled_width {
371 let t = if filled_width <= 1 {
372 0.0
373 } else {
374 i as f64 / (filled_width - 1) as f64
375 };
376 let (r, g, b) = lerp_rgb(start, end, t);
377
378 // Manually construct ANSI escape sequence
379 buffer.push_str("\x1b[38;2;");
380 write_u8_to_string(buffer, r);
381 buffer.push(';');
382 write_u8_to_string(buffer, g);
383 buffer.push(';');
384 write_u8_to_string(buffer, b);
385 buffer.push('m');
386 buffer.push(ch);
387 buffer.push_str("\x1b[0m");
388 }
389 buffer
390}