demo/
main.rs

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        // Set completely transparent background (experimental, only works for iTerm2 and Kitty).
84        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 it be surrounded by styled text.
251            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}