mcsdf-font-tech-demo 1.0.1

Multi-channel signed distance fields font tech demo
use crate::ui::widget::{UILayout, 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::{
    implement_vertex, program, uniform, Blend, Frame, IndexBuffer, Program, Rect as GLRect,
    Surface, VertexBuffer,
};
use mcsdf::font::{Font, GlyphLayout, TextureRenderBatch};
use mcsdf::geometry::Rect;
use mcsdf::texture::Texture;
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

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

impl UILabelContext {
    #[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;

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

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

            in vec2 vCoord;
            out vec4 color;

            uniform sampler2D uTexture;
            uniform float uSharpness;
            uniform vec4 uColor;
            uniform vec4 uShadowColor;
            uniform float uOpacity;

            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);
                float alpha = smoothstep(0.6, 0.3, d);
                color = mix(uColor, uShadowColor, alpha);
                color.a = color.a * smoothstep(0.45 - uSharpness, 0.45 + uSharpness, d) * uOpacity;
            }
        "#,
        })
        .expect("Cannot create program for label");

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

    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)]
struct UILabelGlyphVertex {
    pos: [f32; 2],
    coord: [f32; 2],
}

implement_vertex!(UILabelGlyphVertex, pos, coord);

impl UILabelGlyphVertex {
    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 UILabelRenderPass {
    vertex_buffer: VertexBuffer<UILabelGlyphVertex>,
    index_buffer: IndexBuffer<u16>,
}

#[derive(Copy, Clone)]
pub enum UILabelAlignment {
    Left,
    Right,
    Center,
}

#[derive(Copy, Clone)]
pub struct UILabelStyle {
    pub align: UILabelAlignment,
    pub size: f32,
    pub color: [f32; 4],
    pub shadow_color: [f32; 4],
    pub opacity: f32,
}

pub struct UILabel {
    style: UILabelStyle,
    text: String,
    bounding_box: Rect<f32>,
    passes: HashMap<u32, UILabelRenderPass>,
    context: Rc<RefCell<UILabelContext>>,
}

impl UILabel {
    pub fn new(context: Rc<RefCell<UILabelContext>>, text: &str, style: UILabelStyle) -> Self {
        let mut label = Self {
            context,
            text: String::new(),
            bounding_box: Rect::new(0.0, 0.0, 0.0, 0.0),
            passes: HashMap::new(),
            style,
        };

        label.set_text(text);
        label
    }

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

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

    pub fn get_bounding_box(&self, style: UILabelStyle) -> Rect<f32> {
        let bb = self.bounding_box;
        let size = style.size;
        Rect::new(
            bb.min.x * size,
            bb.min.y * size,
            bb.max.x * size,
            bb.max.y * size,
        )
    }

    pub fn set_color(&mut self, color: [f32; 4]) {
        self.style.color = color;
    }

    pub fn set_shadow_color(&mut self, shadow_color: [f32; 4]) {
        self.style.shadow_color = shadow_color;
    }

    pub fn set_size(&mut self, size: f32) {
        self.style.size = size
    }

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

        let mut context = self.context.borrow_mut();
        let text_layout = context.font.layout_text_block(text);
        let gl_context = &context.context;

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

        fn update_pass_data(pass_data: &mut PassData, glyph_layout: &GlyphLayout) {
            let new_index = pass_data.vertices.len();
            let scr = glyph_layout.screen_coord;
            let tex = glyph_layout.texture_coord;

            let tl = UILabelGlyphVertex::new(scr.min.x, scr.max.y, tex.min.x, tex.max.y);
            let tr = UILabelGlyphVertex::new(scr.max.x, scr.max.y, tex.max.x, tex.max.y);
            let bl = UILabelGlyphVertex::new(scr.min.x, scr.min.y, tex.min.x, tex.min.y);
            let br = UILabelGlyphVertex::new(scr.max.x, scr.min.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 passes = HashMap::<u32, PassData>::new();
        for glyph_layout in &text_layout.glyph_layouts {
            let pass_data = passes.entry(glyph_layout.texture_id).or_insert(PassData {
                vertices: Vec::new(),
                indices: Vec::new(),
            });
            update_pass_data(pass_data, glyph_layout);
        }

        let mut gl_passes = HashMap::<u32, UILabelRenderPass>::new();
        for (id, pass_data) in passes {
            let vertex_buffer = VertexBuffer::immutable(gl_context, pass_data.vertices.as_slice())
                .expect("Cannot create vertex buffer for label");

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

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

        self.passes = gl_passes;
        self.bounding_box = text_layout.bounding_box;
    }

    pub fn render_styled(
        &self,
        frame: &mut Frame,
        layout: UILayout,
        style: UILabelStyle,
        screen: UISize,
    ) {
        let mut pos = [layout.left, layout.top];
        let size = [layout.width, layout.height];
        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 font_sharpness = 0.4;
        let sharpness =
            font_sharpness / f32::from(shadow_size) / (style.size / f32::from(font_size));

        let bb = self.get_bounding_box(style);
        pos[1] -= (bb.height() - size[1]) / 2.0;
        match style.align {
            UILabelAlignment::Left => {}
            UILabelAlignment::Right => {
                pos[0] += size[0] - bb.width();
            }
            UILabelAlignment::Center => {
                pos[0] += (size[0] - bb.width()) / 2.0;
            }
        };

        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,
                            uSharpness: sharpness,
                            uFontSize: style.size,
                            uPosition: pos,
                            uScreen: screen,
                            uColor: style.color,
                            uOpacity: style.opacity,
                            uShadowColor: style.shadow_color
                        },
                        &DrawParameters {
                            blend: Blend::alpha_blending(),
                            color_mask: (true, true, true, false),
                            ..Default::default()
                        },
                    )
                    .expect("Cannot draw UILabel pass");
            }
        }
    }
}

impl UIWidget for UILabel {
    type Event = ();

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