mcsdf-font-tech-demo 1.0.1

Multi-channel signed distance fields font tech demo
use crate::ui::widget::{UIFrameInput, UILayout, UIPoint, UISize, UIWidget};
use glium::backend::{Context, Facade};
use glium::draw_parameters::DrawParameters;
use glium::index::PrimitiveType;
use glium::texture::{ClientFormat, MipmapsOption, RawImage2d, Texture2d, TextureCreationError};
use glium::uniforms::{AsUniformValue, UniformValue};
use glium::{
    implement_vertex, program, uniform, Blend, Frame, IndexBuffer, Program, Rect as GLRect,
    Surface, VertexBuffer,
};
use mcsdf::font::{Font, TextBlockLayout, TextureRenderBatch};
use mcsdf::texture::Texture;
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;

use std::collections::VecDeque;
use std::rc::Rc;
#[derive(Clone, Copy, Debug)]
pub struct Color {
    pub r: f32,
    pub g: f32,
    pub b: f32,
}

impl Color {
    pub fn new(r: f32, g: f32, b: f32) -> Self {
        Color { r, g, b }
    }

    pub fn black() -> Self {
        Color {
            r: 0.0,
            g: 0.0,
            b: 0.0,
        }
    }

    pub fn white() -> Self {
        Color {
            r: 1.0,
            g: 1.0,
            b: 1.0,
        }
    }
}

impl AsUniformValue for Color {
    fn as_uniform_value(&self) -> UniformValue {
        UniformValue::Vec4([self.r, self.g, self.b, 1.0])
    }
}

#[derive(Clone, Copy, Debug)]
pub struct UITextAreaStyle {
    pub text_size: f32,
    pub inner_dist: f32,
    pub outer_dist: f32,
    pub sharpness: f32,
    pub shadow_dist: f32,
    pub text_color: Color,
    pub shadow_color: Color,
    pub shadow_pos: f32,
    pub shadow_size: f32,
    pub shadow_alpha: f32,
    pub texture_visibility: f32,
    pub animation: bool,
}

impl Default for UITextAreaStyle {
    fn default() -> Self {
        UITextAreaStyle {
            text_size: 32.0,
            inner_dist: 0.0,
            outer_dist: 0.5,
            sharpness: 0.4,
            shadow_dist: 0.5,
            text_color: Color::black(),
            shadow_color: Color::black(),
            shadow_pos: 0.0,
            shadow_size: 0.0,
            shadow_alpha: 0.0,
            texture_visibility: 0.0,
            animation: false,
        }
    }
}

pub struct UITextAreaContext {
    context: Rc<Context>,
    program: Program,
    font: Font,
    texture_cache: HashMap<u32, Texture2d>,
}

impl UITextAreaContext {
    #[allow(clippy::redundant_closure)]
    pub fn new<F: ?Sized + Facade>(facade: &F, font: Font) -> Self {
        let context = facade.get_context().clone();
        let texture_cache = HashMap::new();

        let program = program!(facade, 140 => {
        vertex: r#"
            #version 140

            in vec2 pos;
            in vec2 coord;

            out vec2 vCoord;
            out vec2 vPos;

            uniform float uFontSize;
            uniform vec2 uScreen;
            uniform vec2 uPosition;

            void main() {
                vPos = (uPosition + pos * uFontSize) * 2.0 / uScreen - 1.0;
                vCoord = coord;
                gl_Position = vec4(vPos, 0.0, 1.0);
            }
        "#,
        fragment: r#"
            #version 140

            in vec2 vCoord;
            in vec2 vPos;

            out vec4 color;

            uniform sampler2D uTexture;
            uniform float uSharpness;
            uniform float uInnerDist;
            uniform float uOuterDist;
            uniform vec4 uColor;
            uniform vec4 uShadowColor;
            uniform float uShadowPos;
            uniform float uShadowSize;
            uniform float uShadowAlpha;
            uniform float uTextureVisibility;
            uniform vec2 uMouse;
            uniform bool uAnimation;
            uniform vec2 uScreen;
            uniform float uFontSize;

            float median(float a, float b, float c) {
                return max(min(a,b), min(max(a,b),c));
            }

            void main() {
                vec4 t = texture(uTexture, vCoord);
                float d = median(t.r, t.g, t.b);

                if (uAnimation) {
                    float mouse_dist = length(vPos - (uMouse / uScreen - vec2(0.5)) * 2.0);
                    d = d * (1.0 + 1.0 * clamp(1.0 - mouse_dist * 2.0, 0.0, 1.0));
                }

                vec4 outline_color = uColor;
                float outer_alpha = smoothstep(uOuterDist - uSharpness, uOuterDist + uSharpness, d);
                float inner_alpha = uInnerDist == 1.0 ? 1.0 : smoothstep(uInnerDist + uSharpness, uInnerDist - uSharpness, d);
                outline_color.a = inner_alpha * outer_alpha;

                vec4 shadow_color = uShadowColor;
                shadow_color.a = (1.0 - clamp(abs(d - uShadowPos) / uShadowSize, 0.0, 1.0)) * uShadowAlpha;

                vec4 font_color = mix(outline_color, shadow_color, 1.0 - outline_color.a);
                color = mix(font_color, t, uTextureVisibility);
            }
        "#,
        })
        .expect("Cannot create program for text area");

        Self {
            context,
            program,
            font,
            texture_cache,
        }
    }

    pub fn invalidate(&mut self) {
        self.texture_cache = HashMap::new();
    }

    pub fn set_texture_size(&mut self, texture_size: f32) {
        self.font
            .set_texture_size(texture_size as u32, texture_size as u32);
        self.invalidate();
    }

    pub fn set_font_size(&mut self, font_size: f32) {
        self.font.set_font_size(font_size as u8);
        self.invalidate();
    }

    pub fn set_shadow_size(&mut self, shadow_size: f32) {
        self.font.set_shadow_size(shadow_size as u8);
        self.invalidate();
    }

    pub fn update_texture_cache(
        &mut self,
        id: u32,
        texture: &Texture,
    ) -> Result<(), TextureCreationError> {
        let raw_texture = RawImage2d {
            data: Cow::Borrowed(texture.get_data()),
            width: texture.get_width(),
            height: texture.get_height(),
            format: ClientFormat::U8U8U8,
        };

        let new_texture = if let Some(current_texture) = self.texture_cache.get_mut(&id) {
            current_texture.write(
                GLRect {
                    left: 0,
                    bottom: 0,
                    width: texture.get_width(),
                    height: texture.get_height(),
                },
                raw_texture,
            );
            None
        } else {
            Some(Texture2d::with_mipmaps(
                &self.context,
                raw_texture,
                MipmapsOption::NoMipmap,
            )?)
        };

        if let Some(new_texture) = new_texture {
            self.texture_cache.insert(id, new_texture);
        }

        Ok(())
    }

    pub fn get_texture(&self, id: u32) -> Option<&Texture2d> {
        self.texture_cache.get(&id)
    }

    pub fn get_texture_render_batches(&mut self) -> Vec<TextureRenderBatch> {
        self.font.get_texture_render_batches()
    }
}

#[derive(Copy, Clone, Debug)]
struct UITextAreaGlyphVertex {
    pos: [f32; 2],
    coord: [f32; 2],
}

implement_vertex!(UITextAreaGlyphVertex, pos, coord);

impl UITextAreaGlyphVertex {
    fn new(pos_x: f32, pos_y: f32, coord_x: f32, coord_y: f32) -> Self {
        Self {
            pos: [pos_x, pos_y],
            coord: [coord_x, coord_y],
        }
    }
}

struct UITextAreaRenderPass {
    vertex_buffer: VertexBuffer<UITextAreaGlyphVertex>,
    index_buffer: IndexBuffer<u16>,
}

pub struct UITextArea {
    style: UITextAreaStyle,
    passes: HashMap<u32, UITextAreaRenderPass>,
    context: Rc<RefCell<UITextAreaContext>>,
    last_size: UISize,
    last_text: String,
    offset: UIPoint,
    drag_offset: UIPoint,
    drag_start: Option<UIPoint>,
    zoom: f32,
    mouse_x: f32,
    mouse_y: f32,
}

impl UITextArea {
    pub fn new(
        context: Rc<RefCell<UITextAreaContext>>,
        text: &str,
        style: UITextAreaStyle,
    ) -> Self {
        Self {
            context,
            last_size: UISize::zero(),
            last_text: text.into(),
            offset: UIPoint::zero(),
            drag_offset: UIPoint::zero(),
            drag_start: None,
            zoom: 1.0,
            passes: HashMap::new(),
            style,
            mouse_x: 0.0,
            mouse_y: 0.0,
        }
    }

    pub fn get_style(&self) -> UITextAreaStyle {
        self.style
    }

    pub fn set_style(&mut self, style: UITextAreaStyle) {
        self.style = style;
    }

    pub fn set_text(&mut self, text: &str) {
        if self.last_text != text {
            self.last_text = text.into();
            self.invalidate();
        }
    }

    pub fn invalidate(&mut self) {
        let mut context = self.context.borrow_mut();

        enum FormattedText<'a> {
            End,
            NewLine,
            Word(&'a str),
        }

        struct ProcessTextCtx {
            line_y: f32,
            line_total_space: f32,
            line_word_space: f32,
            line_words: VecDeque<TextBlockLayout>,
        }

        struct PassData {
            vertices: Vec<UITextAreaGlyphVertex>,
            indices: Vec<u16>,
        }

        struct RenderWordContext {
            passes: HashMap<u32, PassData>,
        }

        let line_gap = context.font.get_line_gap();
        let ascent = context.font.get_ascent();
        let descent = context.font.get_descent();
        let line_height = line_gap + ascent - descent;
        let line_max_width = self.last_size.width / self.style.text_size;
        let line_min_space = 0.3;

        let mut render_word_ctx = RenderWordContext {
            passes: HashMap::new(),
        };

        let mut render_word = |word_layout: &TextBlockLayout, x: f32, y: f32| {
            let ctx = &mut render_word_ctx;
            for glyph_layout in &word_layout.glyph_layouts {
                let pass_data = ctx
                    .passes
                    .entry(glyph_layout.texture_id)
                    .or_insert(PassData {
                        vertices: Vec::new(),
                        indices: Vec::new(),
                    });

                let new_index = pass_data.vertices.len();
                let scr = glyph_layout.screen_coord;
                let tex = glyph_layout.texture_coord;

                let tl =
                    UITextAreaGlyphVertex::new(scr.min.x + x, scr.max.y + y, tex.min.x, tex.max.y);
                let tr =
                    UITextAreaGlyphVertex::new(scr.max.x + x, scr.max.y + y, tex.max.x, tex.max.y);
                let bl =
                    UITextAreaGlyphVertex::new(scr.min.x + x, scr.min.y + y, tex.min.x, tex.min.y);
                let br =
                    UITextAreaGlyphVertex::new(scr.max.x + x, scr.min.y + y, tex.max.x, tex.min.y);

                pass_data.vertices.push(tl);
                pass_data.vertices.push(tr);
                pass_data.vertices.push(br);
                pass_data.vertices.push(bl);

                pass_data.indices.push(new_index as u16);
                pass_data.indices.push((new_index + 1) as u16);
                pass_data.indices.push((new_index + 2) as u16);
                pass_data.indices.push((new_index + 2) as u16);
                pass_data.indices.push((new_index + 3) as u16);
                pass_data.indices.push(new_index as u16);
            }
        };

        let mut layout_line = |text_ctx: &mut ProcessTextCtx, align: bool| {
            let word_count = text_ctx.line_words.len();
            if word_count == 0 {
                return;
            }

            let mut line_x = 0.0;
            let space = if align {
                (line_max_width - text_ctx.line_word_space) / (word_count - 1) as f32
            } else {
                line_min_space
            };

            while let Some(word) = text_ctx.line_words.pop_front() {
                render_word(&word, line_x, text_ctx.line_y);
                line_x += word.bounding_box.width() + space;
            }

            text_ctx.line_total_space = 0.0;
            text_ctx.line_word_space = 0.0;
        };

        let mut process_text_ctx = ProcessTextCtx {
            line_y: -ascent,
            line_total_space: 0.0,
            line_word_space: 0.0,
            line_words: VecDeque::new(),
        };

        let mut process_text = |formatted_text: FormattedText| {
            let mut ctx = &mut process_text_ctx;
            match formatted_text {
                FormattedText::End => {
                    layout_line(ctx, false);
                }
                FormattedText::NewLine => {
                    layout_line(ctx, false);
                    ctx.line_y -= line_height
                }
                FormattedText::Word(word) => {
                    let word_layout = context.font.layout_text_block(word);
                    let word_width = word_layout.bounding_box.width();
                    if word_width <= line_max_width - ctx.line_total_space {
                        ctx.line_total_space += word_width + line_min_space;
                        ctx.line_word_space += word_width;
                    } else {
                        layout_line(ctx, true);
                        ctx.line_y -= line_height;
                        ctx.line_total_space = word_width + line_min_space;
                        ctx.line_word_space = word_width;
                    }
                    ctx.line_words.push_back(word_layout);
                }
            };
        };

        let mut format_text = || {
            let mut word_start = None;
            for (index, character) in self.last_text.char_indices() {
                match character {
                    '\n' => {
                        if let Some(start) = word_start {
                            process_text(FormattedText::Word(&self.last_text[start..index]));
                            word_start = None;
                        }
                        process_text(FormattedText::NewLine);
                    }
                    x if x.is_whitespace() => {
                        if let Some(start) = word_start {
                            process_text(FormattedText::Word(&self.last_text[start..index]));
                            word_start = None;
                        }
                    }
                    _ => {
                        if word_start.is_none() {
                            word_start = Some(index);
                        }
                    }
                }
            }

            if let Some(start) = word_start {
                process_text(FormattedText::Word(
                    &self.last_text[start..self.last_text.len()],
                ));
            }

            process_text(FormattedText::End);
        };

        format_text();

        let mut gl_passes = HashMap::<u32, UITextAreaRenderPass>::new();
        let gl_context = &context.context;

        for (id, pass_data) in render_word_ctx.passes {
            let vertex_buffer = VertexBuffer::immutable(gl_context, pass_data.vertices.as_slice())
                .expect("Cannot create vertex buffer for text_area");

            let index_buffer = IndexBuffer::immutable(
                gl_context,
                PrimitiveType::TrianglesList,
                pass_data.indices.as_slice(),
            )
            .expect("Cannot create index buffer for text_area");

            gl_passes.insert(
                id,
                UITextAreaRenderPass {
                    vertex_buffer,
                    index_buffer,
                },
            );
        }

        self.passes = gl_passes;
    }

    pub fn render_styled(
        &self,
        frame: &mut Frame,
        layout: UILayout,
        style: UITextAreaStyle,
        screen: UISize,
    ) {
        let pos = [
            layout.left + self.offset.left + self.drag_offset.left,
            layout.top + layout.height + self.offset.top + self.drag_offset.top,
        ];
        let screen = [screen.width, screen.height];
        let context = self.context.borrow_mut();
        let shadow_size = context.font.get_shadow_size();
        let font_size = context.font.get_font_size();
        let sharpness = self.style.sharpness
            / f32::from(shadow_size)
            / (style.text_size * self.zoom / f32::from(font_size));

        for (texture_id, pass_data) in &self.passes {
            if let Some(texture) = context.get_texture(*texture_id) {
                frame
                    .draw(
                        &pass_data.vertex_buffer,
                        &pass_data.index_buffer,
                        &context.program,
                        &uniform! {
                            uTexture: texture,
                            uInnerDist: 1.0 - style.inner_dist,
                            uOuterDist: 1.0 - style.outer_dist,
                            uSharpness: sharpness,
                            uFontSize: style.text_size * self.zoom,
                            uPosition: pos,
                            uScreen: screen,
                            uColor: style.text_color,
                            uShadowColor: style.shadow_color,
                            uShadowPos: style.shadow_pos,
                            uShadowSize: style.shadow_size,
                            uShadowAlpha: style.shadow_alpha,
                            uTextureVisibility: style.texture_visibility,
                            uMouse: [self.mouse_x, self.mouse_y],
                            uAnimation: self.style.animation
                        },
                        &DrawParameters {
                            blend: Blend::alpha_blending(),
                            color_mask: (true, true, true, false),
                            ..Default::default()
                        },
                    )
                    .expect("Cannot draw UITextArea pass");
            }
        }
    }
}

impl UIWidget for UITextArea {
    type Event = ();

    fn render(&self, frame: &mut Frame, layout: UILayout, screen: UISize) {
        self.render_styled(frame, layout, self.style, screen)
    }

    fn update_input(
        &mut self,
        layout: UILayout,
        frame_input: UIFrameInput,
        _events: &mut Vec<Self::Event>,
    ) {
        self.mouse_x = frame_input.mouse_pos.left;
        self.mouse_y = frame_input.mouse_pos.top;

        if (layout.width - self.last_size.width).abs() > f32::EPSILON
            || (layout.height - self.last_size.height).abs() > f32::EPSILON
        {
            self.last_size = UISize {
                width: layout.width,
                height: layout.height,
            };
            self.invalidate();
        }

        let left = frame_input.mouse_pos.left - layout.left;
        let top = frame_input.mouse_pos.top - layout.top - layout.height;

        if let Some(drag_start) = self.drag_start {
            if !frame_input.left_mouse_button_pressed {
                self.drag_start = None;
                self.offset = UIPoint {
                    left: self.offset.left + left - drag_start.left,
                    top: self.offset.top + top - drag_start.top,
                };
                self.drag_offset = UIPoint::zero();
            } else {
                self.drag_offset = UIPoint {
                    left: left - drag_start.left,
                    top: top - drag_start.top,
                };
            }
        } else if layout.is_inside(frame_input.mouse_pos) {
            if frame_input.left_mouse_button_pressed {
                self.drag_start = Some(UIPoint { left, top });
            }

            if let Some(mouse_wheel_delta) = frame_input.mouse_wheel_delta {
                let new_zoom = (self.zoom + mouse_wheel_delta / 100.0 * self.zoom)
                    .max(1.0 / 8.0)
                    .min(128.0);
                let new_offset_left = left - (left - self.offset.left) * (new_zoom / self.zoom);
                let new_offset_top = top - (top - self.offset.top) * (new_zoom / self.zoom);
                self.zoom = new_zoom;
                self.offset = UIPoint {
                    left: new_offset_left,
                    top: new_offset_top,
                };
            }
        }
    }
}