use super::*; pub(crate) fn shimmer_spans(text: &str, elapsed: Duration) -> Vec> { let chars: Vec = text.chars().collect(); if chars.is_empty() { return Vec::new(); } let padding = 10usize; let period = chars.len() + padding * 2; let sweep_seconds = 2.0f32; let pos = ((elapsed.as_secs_f32() % sweep_seconds) / sweep_seconds * (period as f32)) as usize; let band_half_width = 5.0f32; let base = crate::theme::theme().shimmer_base; let hilite = crate::theme::theme().text_bright; chars .iter() .enumerate() .map(|(i, ch)| { let dist = ((i as isize + padding as isize) - pos as isize).abs() as f32; let t = if dist <= band_half_width { 0.5 * (1.0 + (std::f32::consts::PI * (dist / band_half_width)).cos()) } else { 0.0 }; let k = t.clamp(0.0, 1.0) * 0.9; let style = Style::default() .fg(crate::tui::blend_color(base, hilite, k)) .add_modifier(if t > 0.3 { Modifier::BOLD } else { Modifier::empty() }); Span::styled(ch.to_string(), style) }) .collect() } pub(crate) struct InputBox { pub(crate) lines: Vec>, pub(crate) band_h: usize, pub(crate) lead_h: usize, pub(crate) x: usize, pub(crate) width: usize, pub(crate) cur_row: usize, pub(crate) cur_col: usize, } pub(crate) type InputCell = (char, Style); fn marker_kind_label(marker_text: &str) -> &'static str { match marker_text .get(1..marker_text.find(':').unwrap_or(1)) .unwrap_or_default() { "image" => "Image", "video" => "Video", "audio" => "Audio", "pdf" => "PDF", _ => "File", } } pub(crate) fn chip_cells( chips: &[(&str, &str)], pill: Style, label: Style, gap: Style, ) -> Vec { let mut cells: Vec = Vec::new(); for (i, (kind, name)) in chips.iter().enumerate() { if i > 0 { cells.push((' ', gap)); } cells.extend(format!(" {kind} ").chars().map(|c| (c, pill))); cells.extend(format!(" {name} ").chars().map(|c| (c, label))); } cells } type WrapResult = (Vec<(usize, usize)>, Option<(usize, usize)>); pub(crate) fn wrap_line(chars: &[char], content_w: usize, cursor: Option) -> WrapResult { let mut rows: Vec<(usize, usize)> = Vec::new(); let mut start = 0usize; while start < chars.len() { let end = crate::text_width::word_window_end(chars, start, content_w); rows.push((start, end)); start = end; } if rows.is_empty() { rows.push((0, 0)); } let at = cursor.map(|c| { let row = rows .iter() .position(|&(_, end)| c <= end) .unwrap_or(rows.len() - 1); let (s, e) = rows[row]; ( row, crate::text_width::chars_width(&chars[s..c.clamp(s, e)]), ) }); (rows, at) } fn draft_cursor(text: &str, cursor: usize) -> (usize, usize) { let lines: Vec<&str> = text.split('\n').collect(); let mut line_start = 0usize; for (i, line) in lines.iter().enumerate() { let line_end = line_start + line.len(); if cursor <= line_end || i + 1 == lines.len() { let within = cursor.saturating_sub(line_start).min(line.len()); let chars = line.char_indices().take_while(|(b, _)| *b < within).count(); return (i, chars); } line_start = line_end + 1; } (0, 0) } pub(crate) fn styled_runs(cells: &[InputCell]) -> Vec> { let mut spans: Vec> = Vec::new(); let mut run = String::new(); let mut style: Option