1#[cfg(all(
2 not(feature = "crossterm"),
3 not(feature = "termion"),
4 not(feature = "termwiz")
5))]
6compile_error!("The demo needs one of the crossterm, termion, or termwiz features");
7
8#[cfg(feature = "crossterm")]
9mod crossterm;
10#[cfg(feature = "termion")]
11mod termion;
12#[cfg(feature = "termwiz")]
13mod termwiz;
14
15use std::{env, error::Error, num::Wrapping as w, path::PathBuf, time::Duration};
16
17use image::DynamicImage;
18use ratatui::{
19 backend::Backend,
20 layout::{Constraint, Direction, Layout, Rect},
21 style::{Color, Stylize},
22 text::{Line, Text},
23 widgets::{Block, Borders, Paragraph, Wrap},
24 Frame, Terminal,
25};
26use ratatui_image::{
27 picker::Picker,
28 protocol::{Protocol, StatefulProtocol},
29 Image, Resize, StatefulImage,
30};
31
32fn main() -> Result<(), Box<dyn Error>> {
33 #[cfg(feature = "crossterm")]
34 crate::crossterm::run()?;
35 #[cfg(feature = "termion")]
36 crate::termion::run()?;
37 #[cfg(feature = "termwiz")]
38 crate::termwiz::run()?;
39 Ok(())
40}
41
42#[derive(Debug)]
43enum ShowImages {
44 All,
45 Fixed,
46 Resized,
47}
48
49struct App {
50 title: String,
51 should_quit: bool,
52 tick_rate: Duration,
53 background: String,
54 split_percent: u16,
55 show_images: ShowImages,
56
57 image_source_path: PathBuf,
58 image_static_offset: (u16, u16),
59
60 picker: Picker,
61 image_source: DynamicImage,
62 image_static: Protocol,
63 image_fit_state: StatefulProtocol,
64 image_crop_state: StatefulProtocol,
65 image_scale_state: StatefulProtocol,
66}
67
68fn size() -> Rect {
69 Rect::new(0, 0, 30, 16)
70}
71
72impl App {
73 pub fn new<B: Backend>(_: &mut Terminal<B>) -> Self {
74 let title = format!(
75 "Demo ({})",
76 env::var("TERM").unwrap_or("unknown".to_string())
77 );
78
79 let ada = "./assets/Ada.png";
80 let image_source = image::io::Reader::open(ada).unwrap().decode().unwrap();
81
82 let mut picker = Picker::from_query_stdio().unwrap();
83 picker.set_background_color([0, 0, 0, 0]);
85
86 let image_static = picker
87 .new_protocol(image_source.clone(), size(), Resize::Fit(None))
88 .unwrap();
89 let image_fit_state = picker.new_resize_protocol(image_source.clone());
90 let image_crop_state = picker.new_resize_protocol(image_source.clone());
91 let image_scale_state = picker.new_resize_protocol(image_source.clone());
92
93 let mut background = String::new();
94
95 let mut r: [u64; 2] = [0x8a5cd789635d2dff, 0x121fd2155c472f96];
96 for _ in 0..5_000 {
97 let mut s1 = w(r[0]);
98 let s0 = w(r[1]);
99 let result = s0 + s1;
100 r[0] = s0.0;
101 s1 ^= s1 << 23;
102 r[1] = (s1 ^ s0 ^ (s1 >> 18) ^ (s0 >> 5)).0;
103 let c = match result.0 % 4 {
104 0 => '.',
105 1 => ' ',
106 _ => '…',
107 };
108 background.push(c);
109 }
110
111 Self {
112 title,
113 should_quit: false,
114 tick_rate: Duration::from_millis(1000),
115 background,
116 show_images: ShowImages::All,
117 split_percent: 70,
118 picker,
119 image_source,
120 image_source_path: ada.into(),
121
122 image_static,
123 image_fit_state,
124 image_crop_state,
125 image_scale_state,
126
127 image_static_offset: (0, 0),
128 }
129 }
130 pub fn on_key(&mut self, c: char) {
131 match c {
132 'q' => {
133 self.should_quit = true;
134 }
135 't' => {
136 self.show_images = match self.show_images {
137 ShowImages::All => ShowImages::Fixed,
138 ShowImages::Fixed => ShowImages::Resized,
139 ShowImages::Resized => ShowImages::All,
140 }
141 }
142 'i' => {
143 self.picker
144 .set_protocol_type(self.picker.protocol_type().next());
145 self.reset_images();
146 }
147 'o' => {
148 let path = match self.image_source_path.to_str() {
149 Some("./assets/Ada.png") => "./assets/Jenkins.jpg",
150 Some("./assets/Jenkins.jpg") => "./assets/NixOS.png",
151 _ => "./assets/Ada.png",
152 };
153 self.image_source = image::io::Reader::open(path).unwrap().decode().unwrap();
154 self.image_source_path = path.into();
155 self.reset_images();
156 }
157 'H' => {
158 if self.split_percent >= 10 {
159 self.split_percent -= 10;
160 }
161 }
162 'L' => {
163 if self.split_percent <= 90 {
164 self.split_percent += 10;
165 }
166 }
167 'h' => {
168 if self.image_static_offset.0 > 0 {
169 self.image_static_offset.0 -= 1;
170 }
171 }
172 'j' => {
173 self.image_static_offset.1 += 1;
174 }
175 'k' => {
176 if self.image_static_offset.1 > 0 {
177 self.image_static_offset.1 -= 1;
178 }
179 }
180 'l' => {
181 self.image_static_offset.0 += 1;
182 }
183 _ => {}
184 }
185 }
186
187 fn reset_images(&mut self) {
188 self.image_static = self
189 .picker
190 .new_protocol(self.image_source.clone(), size(), Resize::Fit(None))
191 .unwrap();
192 self.image_fit_state = self.picker.new_resize_protocol(self.image_source.clone());
193 self.image_crop_state = self.picker.new_resize_protocol(self.image_source.clone());
194 self.image_scale_state = self.picker.new_resize_protocol(self.image_source.clone());
195 }
196
197 pub fn on_tick(&mut self) {}
198
199 fn render_resized_image(&mut self, f: &mut Frame<'_>, resize: Resize, area: Rect) {
200 let (state, name, color) = match resize {
201 Resize::Fit(_) => (&mut self.image_fit_state, "Fit", Color::Magenta),
202 Resize::Crop(_) => (&mut self.image_crop_state, "Crop", Color::Green),
203 Resize::Scale(_) => (&mut self.image_scale_state, "Scale", Color::Blue),
204 };
205 let block = block(name);
206 let inner_area = block.inner(area);
207 f.render_widget(paragraph(self.background.as_str().bg(color)), inner_area);
208 match self.show_images {
209 ShowImages::Fixed => (),
210 _ => {
211 let image = StatefulImage::default().resize(resize);
212 f.render_stateful_widget(image, inner_area, state);
213 }
214 };
215 f.render_widget(block, area);
216 }
217}
218
219fn ui(f: &mut Frame<'_>, app: &mut App) {
220 let outer_block = Block::default()
221 .borders(Borders::TOP)
222 .title(app.title.as_str());
223
224 let chunks = Layout::default()
225 .direction(Direction::Horizontal)
226 .constraints(
227 [
228 Constraint::Percentage(app.split_percent),
229 Constraint::Percentage(100 - app.split_percent),
230 ]
231 .as_ref(),
232 )
233 .split(outer_block.inner(f.area()));
234 f.render_widget(outer_block, f.area());
235
236 let left_chunks = vertical_layout().split(chunks[0]);
237 let right_chunks = vertical_layout().split(chunks[1]);
238
239 let block_left_top = block("Fixed");
240 let area = block_left_top.inner(left_chunks[0]);
241 f.render_widget(
242 paragraph(app.background.as_str()).style(Color::Yellow),
243 area,
244 );
245 f.render_widget(block_left_top, left_chunks[0]);
246 match app.show_images {
247 ShowImages::Resized => {}
248 _ => {
249 let image = Image::new(&mut app.image_static);
250 let offset_area = Rect {
252 x: area.x + 1,
253 y: area.y + 1,
254 width: area.width.saturating_sub(2),
255 height: area.height.saturating_sub(2),
256 };
257 f.render_widget(image, offset_area);
258 }
259 }
260
261 let chunks_left_bottom = Layout::default()
262 .direction(Direction::Horizontal)
263 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
264 .split(left_chunks[1]);
265
266 app.render_resized_image(f, Resize::Crop(None), chunks_left_bottom[0]);
267 app.render_resized_image(f, Resize::Scale(None), chunks_left_bottom[1]);
268 app.render_resized_image(f, Resize::Fit(None), right_chunks[0]);
269
270 let block_right_bottom = block("Help");
271 let area = block_right_bottom.inner(right_chunks[1]);
272 f.render_widget(
273 paragraph(vec![
274 Line::from("Key bindings:"),
275 Line::from("H/L: resize"),
276 Line::from(format!(
277 "i: cycle image protocols (current: {:?})",
278 app.picker.protocol_type()
279 )),
280 Line::from("o: cycle image"),
281 Line::from(format!("t: toggle ({:?})", app.show_images)),
282 Line::from(format!("Font size: {:?}", app.picker.font_size())),
283 ]),
284 area,
285 );
286 f.render_widget(block_right_bottom, right_chunks[1]);
287}
288
289fn paragraph<'a, T: Into<Text<'a>>>(str: T) -> Paragraph<'a> {
290 Paragraph::new(str).wrap(Wrap { trim: true })
291}
292
293fn vertical_layout() -> Layout {
294 Layout::default()
295 .direction(Direction::Vertical)
296 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
297}
298
299fn block(name: &str) -> Block<'_> {
300 Block::default().borders(Borders::ALL).title(name)
301}