1use std::sync::atomic::{
2 AtomicU32,
3 Ordering,
4};
5
6use crate::{
7 Component,
8 RenderError,
9 Rendered,
10 image::ImageProtocol,
11 layout::Rect,
12 renderer::ImageCommand,
13};
14
15static NEXT_IMAGE_ID: AtomicU32 = AtomicU32::new(1);
16
17const DEFAULT_CELL_WIDTH_PX: u32 = 10;
23const DEFAULT_CELL_HEIGHT_PX: u32 = 20;
24
25const DEFAULT_MAX_COLS: u16 = 40;
28
29const DEFAULT_MAX_ROWS: u16 = 20;
31
32pub struct ImageWidget {
39 id: u32,
40 data: Vec<u8>,
41 mime_type: String,
42 placeholder: String,
43 protocol: ImageProtocol,
44 cols: u16,
45 rows: u16,
46}
47
48impl ImageWidget {
49 pub fn new(data: Vec<u8>, mime_type: impl Into<String>, placeholder: Option<String>) -> Self {
56 let mime_type = mime_type.into();
57 let (cols, rows) = compute_default_size(&data, &mime_type);
58 Self {
59 id: NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed),
60 data,
61 mime_type,
62 placeholder: placeholder.unwrap_or_else(|| "[image]".to_string()),
63 protocol: ImageProtocol::default(),
64 cols,
65 rows,
66 }
67 }
68
69 pub fn with_protocol(mut self, protocol: ImageProtocol) -> Self {
71 self.protocol = protocol;
72 self
73 }
74
75 pub fn with_size(mut self, cols: u16, rows: u16) -> Self {
81 self.cols = cols.max(1);
82 self.rows = rows.max(1);
83 self
84 }
85
86 fn placeholder_line(&self, width: u16) -> String {
88 let target = width as usize;
89 let placeholder_vw = crate::utils::visible_width(&self.placeholder);
90 if placeholder_vw >= target {
91 return crate::utils::truncate_to_width(&self.placeholder, width, "");
92 }
93 let pad = target - placeholder_vw;
94 let mut line = self.placeholder.clone();
95 line.push_str(&" ".repeat(pad));
96 line
97 }
98}
99
100impl Component for ImageWidget {
101 fn render(&self, width: u16) -> Result<Rendered, RenderError> {
102 let display_cols = self.cols.min(width);
103 let encoded = match self.protocol {
104 | ImageProtocol::Kitty => {
105 crate::image::encode_kitty(self.id, &self.data, display_cols, self.rows)
106 },
107 | ImageProtocol::Iterm2 => crate::image::encode_iterm2(&self.data, &self.mime_type),
108 };
109 let images = if encoded.is_empty() {
110 Vec::new()
111 } else {
112 vec![ImageCommand {
113 id: self.id,
114 data: encoded,
115 row: 0,
116 col: 0,
117 }]
118 };
119
120 let mut lines = Vec::new();
121 if self.rows > 0 {
122 lines.push(self.placeholder_line(display_cols));
123 }
124 for _ in 1..self.rows {
125 lines.push(" ".repeat(display_cols as usize));
126 }
127
128 Ok(Rendered {
129 lines,
130 cursor: None,
131 images,
132 })
133 }
134
135 fn render_rect(&self, rect: Rect) -> Result<Rendered, RenderError> {
136 let mut rendered = match self.render(rect.width) {
137 | Ok(r) => r,
138 | Err(e) => return Err(e),
139 };
140 rendered.lines.truncate(rect.height as usize);
143 Ok(rendered)
144 }
145}
146
147fn compute_default_size(data: &[u8], mime_type: &str) -> (u16, u16) {
153 let dims = match mime_type {
154 | "image/png" => crate::image::get_png_dimensions(data),
155 | "image/jpeg" | "image/jpg" => crate::image::get_jpeg_dimensions(data),
156 | "image/gif" => crate::image::get_gif_dimensions(data),
157 | "image/webp" => crate::image::get_webp_dimensions(data),
158 | _ => None,
159 };
160 let (pixel_w, pixel_h) = match dims {
161 | Some(d) => d,
162 | None => return (20, 10),
163 };
164 if pixel_w == 0 || pixel_h == 0 {
165 return (20, 10);
166 }
167
168 let max_cols = DEFAULT_MAX_COLS as u32;
169 let max_rows = DEFAULT_MAX_ROWS as u32;
170 let cols = (pixel_w / DEFAULT_CELL_WIDTH_PX).clamp(1, max_cols);
171 let rows = ((pixel_h * cols * DEFAULT_CELL_WIDTH_PX) / (pixel_w * DEFAULT_CELL_HEIGHT_PX))
172 .clamp(1, max_rows) as u16;
173 let cols = cols as u16;
174 (cols, rows)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn image_widget_default_size_from_png_dimensions() {
183 let mut data = vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
185 data.extend_from_slice(&[0; 8]);
186 data.extend_from_slice(&100u32.to_be_bytes());
187 data.extend_from_slice(&200u32.to_be_bytes());
188 let widget = ImageWidget::new(data, "image/png", None);
189 assert_eq!(widget.cols, 10);
191 assert_eq!(widget.rows, 10);
192 }
193
194 #[test]
195 fn image_widget_with_size_override() {
196 let widget = ImageWidget::new(vec![], "image/png", None).with_size(15, 8);
197 assert_eq!(widget.cols, 15);
198 assert_eq!(widget.rows, 8);
199 }
200
201 #[test]
202 fn image_widget_render_reserves_multiple_lines() {
203 let widget = ImageWidget::new(vec![0x89, 0x50], "image/png", None).with_size(10, 3);
204 let rendered = widget.render(80).unwrap();
205 assert_eq!(rendered.lines.len(), 3);
206 assert!(rendered.lines[0].starts_with("[image]"));
207 assert_eq!(crate::utils::visible_width(&rendered.lines[1]), 10);
208 }
209
210 #[test]
211 fn image_widget_render_rect_clips_to_height() {
212 let widget = ImageWidget::new(vec![0x89, 0x50], "image/png", None).with_size(5, 5);
213 let rect = Rect::new(0, 0, 80, 2);
214 let rendered = widget.render_rect(rect).unwrap();
215 assert_eq!(rendered.lines.len(), 2);
216 }
217
218 #[test]
219 fn image_widget_render_includes_kitty_dimensions() {
220 let widget = ImageWidget::new(vec![0x89, 0x50], "image/png", None).with_size(12, 6);
221 let rendered = widget.render(80).unwrap();
222 assert_eq!(rendered.images.len(), 1);
223 let data = &rendered.images[0].data;
224 assert!(data.contains("c=12"));
225 assert!(data.contains("r=6"));
226 }
227}