linutil_tui 26.5.21

Chris Titus Tech's Linux Toolbox - Linutil is a distro-agnostic toolbox designed to simplify everyday Linux tasks.
use crate::theme::Theme;
use image::{imageops::FilterType, RgbaImage};
use ratatui::{prelude::*, widgets::Paragraph};
use ratatui_image::{
    picker::{Picker, ProtocolType},
    protocol::StatefulProtocol,
    Resize, StatefulImage,
};

const LOGO_TEXT_GAP: u16 = 0;
const LOGO_ALPHA_CUTOFF: u8 = 10;
const LOGO_SCALE_NUM: u32 = 70;
const LOGO_SCALE_DEN: u32 = 100;

enum Renderer {
    Protocol {
        protocol: Box<StatefulProtocol>,
        resize: Resize,
    },
    Blocks,
}

pub struct Logo {
    renderer: Renderer,
    rgba: RgbaImage,
    font_size: (u16, u16),
    image_size: (u32, u32),
    cached_size: (u16, u16),
    cached_lines: Vec<Line<'static>>,
    last_area_size: (u16, u16),
}

impl Logo {
    pub fn load() -> Option<Self> {
        let dyn_image = image::load_from_memory(include_bytes!("../assets/ctt_logo.png")).ok()?;
        let rgba = dyn_image.to_rgba8();
        let image_size = rgba.dimensions();

        let mut picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
        picker.set_background_color(Some([0, 0, 0, 0]));
        let font_size = picker.font_size();
        let font_size = (font_size.width, font_size.height);
        let renderer = if picker.protocol_type() == ProtocolType::Halfblocks {
            Renderer::Blocks
        } else {
            let protocol =
                Box::new(picker.new_resize_protocol(image::DynamicImage::ImageRgba8(rgba.clone())));
            Renderer::Protocol {
                protocol,
                resize: Resize::Scale(Some(FilterType::Triangle)),
            }
        };

        Some(Self {
            renderer,
            rgba,
            font_size,
            image_size,
            cached_size: (0, 0),
            cached_lines: Vec::new(),
            last_area_size: (0, 0),
        })
    }

    pub fn area_height_for_width(&self, width: u16, max_height: u16) -> u16 {
        if width == 0 || max_height == 0 {
            return 0;
        }

        let scaled_width = self.scaled_width(width);
        let max_image_height = max_height.saturating_sub(LOGO_TEXT_GAP + 1);
        if max_image_height == 0 {
            return max_height.min(1);
        }

        let mut image_height = self.rows_for_width(scaled_width);
        if image_height > max_image_height {
            image_height = max_image_height;
        }

        image_height + LOGO_TEXT_GAP + 1
    }

    pub fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
        if area.height == 0 || area.width == 0 {
            return;
        }

        self.refresh_protocol_if_needed(area);

        let max_image_height = area.height.saturating_sub(LOGO_TEXT_GAP + 1);
        let (draw_width, draw_height) = self.draw_size(area.width, max_image_height);

        if draw_width > 0 && draw_height > 0 {
            let centered_x = area.x + area.width.saturating_sub(draw_width) / 2;
            let max_x = area.x + area.width.saturating_sub(draw_width);
            let image_x = centered_x.min(max_x);
            let image_area = Rect::new(image_x, area.y, draw_width, draw_height);
            let mut use_blocks = matches!(self.renderer, Renderer::Blocks);

            if !use_blocks {
                if let Renderer::Protocol { protocol, resize } = &mut self.renderer {
                    let widget =
                        StatefulImage::<StatefulProtocol>::default().resize(resize.clone());
                    frame.render_stateful_widget(widget, image_area, protocol.as_mut());
                    if let Some(result) = protocol.last_encoding_result() {
                        if result.is_err() {
                            use_blocks = true;
                        }
                    }
                }
            }

            if use_blocks {
                self.renderer = Renderer::Blocks;
                self.render_blocks(frame, image_area);
            }
        }

        let text_y = if draw_height > 0 {
            area.y + draw_height + LOGO_TEXT_GAP
        } else {
            area.y
        };
        if text_y < area.y + area.height {
            let text_area = Rect::new(area.x, text_y, area.width, 1);
            let label = Line::styled(
                format!("Linutil V{}", env!("CARGO_PKG_VERSION")),
                Style::default().fg(theme.tab_color()).bold(),
            );
            let text = Paragraph::new(label).alignment(Alignment::Center);
            frame.render_widget(text, text_area);
        }
    }

    fn draw_size(&self, width: u16, max_height: u16) -> (u16, u16) {
        if width == 0 || max_height == 0 {
            return (0, 0);
        }

        let scaled_width = self.scaled_width(width);
        let mut draw_width = scaled_width;
        let mut draw_height = self.rows_for_width(draw_width);
        if draw_height > max_height {
            draw_height = max_height;
            draw_width = self.width_for_height(draw_height).min(scaled_width);
        }

        (draw_width, draw_height)
    }

    fn rows_for_width(&self, width: u16) -> u16 {
        if width == 0 || self.image_size.0 == 0 || self.font_size.0 == 0 || self.font_size.1 == 0 {
            return 0;
        }

        let pixel_width = u64::from(width) * u64::from(self.font_size.0);
        let scaled_pixel_height =
            u64::from(self.image_size.1) * pixel_width / u64::from(self.image_size.0);
        let row_height = u64::from(self.font_size.1);
        let rows = scaled_pixel_height.div_ceil(row_height);

        rows.min(u64::from(u16::MAX)) as u16
    }

    fn width_for_height(&self, height: u16) -> u16 {
        if height == 0 || self.image_size.1 == 0 || self.font_size.0 == 0 || self.font_size.1 == 0 {
            return 0;
        }

        let pixel_height = u64::from(height) * u64::from(self.font_size.1);
        let scaled_pixel_width =
            u64::from(self.image_size.0) * pixel_height / u64::from(self.image_size.1);
        let col_width = u64::from(self.font_size.0);
        let cols = scaled_pixel_width.div_ceil(col_width);

        cols.min(u64::from(u16::MAX)) as u16
    }

    fn render_blocks(&mut self, frame: &mut Frame, area: Rect) {
        if area.width == 0 || area.height == 0 {
            return;
        }

        let size = (area.width, area.height);
        if self.cached_size != size {
            self.cached_size = size;
            self.cached_lines.clear();

            let resized = image::imageops::resize(
                &self.rgba,
                area.width as u32,
                area.height as u32,
                FilterType::Triangle,
            );

            for y in 0..area.height {
                let mut spans = Vec::with_capacity(area.width as usize);
                for x in 0..area.width {
                    let pixel = resized.get_pixel(x as u32, y as u32).0;
                    if pixel[3] < LOGO_ALPHA_CUTOFF {
                        spans.push(Span::raw(" "));
                    } else {
                        spans.push(Span::styled(
                            "#",
                            Style::default().fg(Color::Rgb(pixel[0], pixel[1], pixel[2])),
                        ));
                    }
                }
                self.cached_lines.push(Line::from(spans));
            }
        }

        frame.render_widget(Paragraph::new(Text::from(self.cached_lines.clone())), area);
    }

    fn refresh_protocol_if_needed(&mut self, area: Rect) {
        let area_size = (area.width, area.height);
        if self.last_area_size == area_size {
            return;
        }
        self.last_area_size = area_size;

        if !matches!(self.renderer, Renderer::Protocol { .. }) {
            return;
        }

        let mut picker = match Picker::from_query_stdio() {
            Ok(picker) => picker,
            Err(_) => return,
        };
        picker.set_background_color(Some([0, 0, 0, 0]));
        let new_font_size = picker.font_size();
        let new_font_size = (new_font_size.width, new_font_size.height);
        let protocol_type = picker.protocol_type();

        if protocol_type == ProtocolType::Halfblocks {
            self.renderer = Renderer::Blocks;
            self.font_size = new_font_size;
            self.cached_size = (0, 0);
            return;
        }

        if self.font_size != new_font_size {
            self.font_size = new_font_size;
            self.renderer = Renderer::Protocol {
                protocol: Box::new(
                    picker.new_resize_protocol(image::DynamicImage::ImageRgba8(self.rgba.clone())),
                ),
                resize: Resize::Scale(Some(FilterType::Triangle)),
            };
        }
    }

    fn scaled_width(&self, width: u16) -> u16 {
        if width == 0 {
            return 0;
        }
        let scaled = (u32::from(width) * LOGO_SCALE_NUM + (LOGO_SCALE_DEN / 2)) / LOGO_SCALE_DEN;
        scaled.clamp(1, u16::MAX as u32) as u16
    }
}