inlyne 0.3.0

Introducing Inlyne, a GPU powered yet browsless tool to help you quickly view markdown files in the blink of an eye.
use crate::utils::{Align, Line, Point, Rect, Selection, Size};
use wgpu_glyph::{
    ab_glyph::{Font, FontArc, PxScale},
    Extra, FontId, GlyphCruncher, HorizontalAlign, Layout, Section, SectionGlyph,
};

#[derive(Clone, Debug, Default)]
pub struct TextBox {
    pub indent: f32,
    pub texts: Vec<Text>,
    pub is_code_block: bool,
    pub is_quote_block: Option<usize>,
    pub is_checkbox: Option<bool>,
    pub is_anchor: Option<String>,
    pub align: Align,
    pub hidpi_scale: f32,
    pub padding_height: f32,
    pub background_color: Option<[f32; 4]>,
}

impl TextBox {
    pub fn new(texts: Vec<Text>, hidpi_scale: f32) -> TextBox {
        TextBox {
            texts,
            hidpi_scale,
            ..Default::default()
        }
    }

    pub fn set_code_block(&mut self, is_code_block: bool) {
        self.is_code_block = is_code_block;
    }

    pub fn set_quote_block(&mut self, nest: Option<usize>) {
        self.is_quote_block = nest;
    }

    pub fn set_checkbox(&mut self, is_checked: Option<bool>) {
        self.is_checkbox = is_checked;
    }

    pub fn set_anchor(&mut self, anchor: Option<String>) {
        self.is_anchor = anchor;
    }

    pub fn set_background_color(&mut self, color: Option<[f32; 4]>) {
        self.background_color = color;
    }

    pub fn with_padding(mut self, padding_height: f32) -> Self {
        self.padding_height = padding_height;
        self
    }

    pub fn set_align(&mut self, align: Align) {
        self.align = align;
    }

    pub fn find_hoverable<'a, T: GlyphCruncher>(
        &'a self,
        glyph_brush: &'a mut T,
        loc: Point,
        screen_position: Point,
        bounds: Size,
        zoom: f32,
    ) -> Option<&'a Text> {
        let fonts: Vec<FontArc> = glyph_brush.fonts().to_vec();
        glyph_brush
            .glyphs(&self.glyph_section(screen_position, bounds, zoom))
            .find(|glyph| {
                let bounds = Rect::from((fonts[glyph.font_id.0]).glyph_bounds(&glyph.glyph));
                bounds.contains(loc)
            })
            .map(|glyph| &self.texts[glyph.section_index])
    }

    pub fn glyph_bounds<T: GlyphCruncher>(
        &self,
        glyph_brush: &mut T,
        screen_position: Point,
        bounds: Size,
        zoom: f32,
    ) -> Vec<(Rect, SectionGlyph)> {
        let mut glyph_bounds = Vec::new();
        let fonts: Vec<FontArc> = glyph_brush.fonts().to_vec();
        for glyph in glyph_brush.glyphs(&self.glyph_section(screen_position, bounds, zoom)) {
            let bounds = Rect::from((fonts[glyph.font_id.0]).glyph_bounds(&glyph.glyph));
            glyph_bounds.push((bounds, glyph.clone()));
        }
        glyph_bounds
    }

    pub fn size<T: GlyphCruncher>(
        &self,
        glyph_brush: &mut T,
        screen_position: Point,
        bounds: Size,
        zoom: f32,
    ) -> Size {
        if self.texts.is_empty() {
            return (0., self.padding_height * self.hidpi_scale * zoom);
        }

        if let Some(bounds) =
            glyph_brush.glyph_bounds(&self.glyph_section(screen_position, bounds, zoom))
        {
            (
                bounds.width(),
                bounds.height() + self.padding_height * self.hidpi_scale * zoom,
            )
        } else {
            (0., self.padding_height * self.hidpi_scale * zoom)
        }
    }

    pub fn glyph_section(&self, mut screen_position: Point, bounds: Size, zoom: f32) -> Section {
        let texts = self.texts.iter().map(|t| t.wgpu_text(zoom)).collect();

        let horizontal_align = match self.align {
            Align::Center => {
                screen_position = (screen_position.0 + bounds.0 / 2., screen_position.1);
                HorizontalAlign::Center
            }
            Align::Left => HorizontalAlign::Left,
            Align::Right => {
                screen_position = (bounds.0 + screen_position.0, screen_position.1);
                HorizontalAlign::Right
            }
        };
        Section {
            screen_position,
            bounds,
            text: texts,
            ..wgpu_glyph::Section::default()
                .with_layout(Layout::default().h_align(horizontal_align))
        }
    }

    pub fn render_lines<T: GlyphCruncher>(
        &self,
        glyph_brush: &mut T,
        screen_position: Point,
        bounds: Size,
        zoom: f32,
    ) -> Vec<Line> {
        let mut has_lines = false;
        for text in &self.texts {
            if text.is_striked || text.is_underlined {
                has_lines = true;
                break;
            }
        }
        if !has_lines {
            return Vec::new();
        }
        let mut lines = Vec::new();
        for (glyph_bounds, glyph) in self.glyph_bounds(glyph_brush, screen_position, bounds, zoom) {
            if self.texts[glyph.section_index].is_underlined {
                lines.push((
                    (glyph_bounds.pos.0, glyph_bounds.max().1),
                    (glyph_bounds.max().0, glyph_bounds.max().1),
                ));
            }
            if self.texts[glyph.section_index].is_striked {
                let mid_height = glyph_bounds.pos.1 + glyph_bounds.size.1 / 2.;
                lines.push((
                    (glyph_bounds.pos.0, mid_height),
                    (glyph_bounds.max().0, mid_height),
                ));
            }
        }

        lines
    }

    pub fn render_selection<T: GlyphCruncher>(
        &self,
        glyph_brush: &mut T,
        screen_position: Point,
        bounds: Size,
        zoom: f32,
        mut selection: Selection,
    ) -> (Vec<Rect>, String) {
        let mut selection_rects = Vec::new();
        let mut selection_text = String::new();
        if selection.0 == selection.1 {
            return (selection_rects, selection_text);
        }
        if selection.0 .1 > selection.1 .1 {
            std::mem::swap(&mut selection.0, &mut selection.1);
        }
        let rect = Rect::new(screen_position, bounds);
        if rect.contains(selection.0) {
            for (glyph_bounds, glyph) in
                self.glyph_bounds(glyph_brush, screen_position, bounds, zoom)
            {
                if (glyph_bounds.pos.1 >= selection.0 .1 && glyph_bounds.max().1 <= selection.1 .1)
                    || (glyph_bounds.max().1 <= selection.1 .1
                        && glyph_bounds.max().1 >= selection.0 .1
                        && glyph_bounds.max().0 >= selection.0 .0)
                    || (glyph_bounds.max().1 >= selection.1 .1
                        && glyph_bounds.pos.1 <= selection.0 .1
                        && glyph_bounds.pos.0 <= selection.0 .0.max(selection.1 .0)
                        && glyph_bounds.max().0 >= selection.0 .0.min(selection.1 .0))
                {
                    selection_rects.push(glyph_bounds);
                    if let Some(char) = self.texts[glyph.section_index]
                        .text
                        .chars()
                        .nth(glyph.byte_index)
                    {
                        selection_text.push(char);
                    }
                }
            }
            selection_text.push('\n');
        }
        if rect.pos.1 >= selection.0 .1.min(selection.1 .1)
            && rect.max().1 <= selection.0 .1.max(selection.1 .1)
        {
            selection_rects.push(rect.clone());
            for text in &self.texts {
                selection_text.push_str(&text.text);
            }
            selection_text.push('\n');
        }
        if rect.contains(selection.1) {
            for (glyph_bounds, glyph) in
                self.glyph_bounds(glyph_brush, screen_position, bounds, zoom)
            {
                if (glyph_bounds.pos.1 >= selection.0 .1 && glyph_bounds.max().1 <= selection.1 .1)
                    || (glyph_bounds.pos.1 <= selection.1 .1
                        && glyph_bounds.pos.1 >= selection.0 .1
                        && glyph_bounds.pos.0 <= selection.1 .0)
                {
                    selection_rects.push(glyph_bounds);
                    if let Some(char) = self.texts[glyph.section_index]
                        .text
                        .chars()
                        .nth(glyph.byte_index)
                    {
                        selection_text.push(char);
                    }
                }
            }
            selection_text.push('\n');
        }
        (selection_rects, selection_text)
    }
}

#[derive(Debug, Clone, Default)]
pub struct Text {
    pub text: String,
    pub size: f32,
    pub color: Option<[f32; 4]>,
    pub link: Option<String>,
    pub is_bold: bool,
    pub is_italic: bool,
    pub is_underlined: bool,
    pub is_striked: bool,
    pub font: usize,
    pub hidpi_scale: f32,
    pub default_color: [f32; 4],
}

impl Text {
    pub fn new(text: String, hidpi_scale: f32, default_text_color: [f32; 4]) -> Self {
        Self {
            text,
            size: 16.,
            hidpi_scale,
            default_color: default_text_color,
            ..Default::default()
        }
    }

    pub fn with_size(mut self, size: f32) -> Self {
        self.size = size;
        self
    }

    pub fn with_color(mut self, color: [f32; 4]) -> Self {
        self.color = Some(color);
        self
    }

    pub fn with_link(mut self, link: String) -> Self {
        self.link = Some(link);
        self
    }

    pub fn make_bold(mut self, bold: bool) -> Self {
        self.is_bold = bold;
        self
    }

    pub fn make_italic(mut self, italic: bool) -> Self {
        self.is_italic = italic;
        self
    }

    pub fn make_underlined(mut self, underlined: bool) -> Self {
        self.is_underlined = underlined;
        self
    }

    pub fn make_striked(mut self, striked: bool) -> Self {
        self.is_striked = striked;
        self
    }

    pub fn with_font(mut self, font_index: usize) -> Self {
        self.font = font_index;
        self
    }

    fn font_id(&self) -> FontId {
        let base = self.font * 4;
        let font = if self.is_bold {
            if self.is_italic {
                base + 3
            } else {
                base + 2
            }
        } else if self.is_italic {
            base + 1
        } else {
            base
        };
        FontId(font)
    }

    fn color(&self) -> [f32; 4] {
        if let Some(color) = self.color {
            color
        } else {
            self.default_color
        }
    }

    pub fn wgpu_text(&self, zoom: f32) -> wgpu_glyph::Text {
        wgpu_glyph::Text {
            text: &self.text,
            scale: PxScale::from(self.size * self.hidpi_scale * zoom),
            font_id: self.font_id(),
            extra: Extra {
                color: self.color(),
                z: 0.0,
            },
        }
    }
}