use std::fmt::Write; use glow::HasContext; use blurry::{latin1, ttf_parser::Face, FontAssetBuilder, Glyph, GlyphRequest}; static DISPLAY_FONT_SIZE: f32 = 30.0; const PADDING_RATIO: f32 = 0.3; #[derive(Clone, Copy, Debug)] struct AdvanceWidth(f32); fn update_font( gl: &glow::Context, texture: glow::Texture, ttf_data: &[u8], ) -> Result>, &'static str> { let face = Face::parse(ttf_data, 0).map_err(|_| "failed to parse font file")?; let height = f32::from(face.units_per_em()); let mut asset = FontAssetBuilder::with_font_size(30.0) .with_padding_ratio(PADDING_RATIO) .build(latin1().map_while(|codepoint| { let advance_width: f32 = face .glyph_index(codepoint) .and_then(|glyph_id| face.glyph_hor_advance(glyph_id)) .unwrap_or(0) .into(); Some(GlyphRequest { user_data: AdvanceWidth(advance_width / height), face: &face, codepoint, }) })) .map_err(|err| match err { blurry::Error::MissingGlyph(_) => "the font file didn't contain all the characters", blurry::Error::PackingAtlasFailed => { "we failed to pack the glyphs into a single texture" } _ => "an unspecified error occurred", })?; let space_width = face .glyph_index(' ') .and_then(|idx| face.glyph_hor_advance(idx)) .map(|w| f32::from(w) / height) .unwrap_or(0.25); let mut space_glyph = asset.metadata[0]; space_glyph.user_data = AdvanceWidth(space_width); space_glyph.codepoint = ' '; unsafe { gl.bind_texture(glow::TEXTURE_2D, Some(texture)); gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1); gl.tex_image_2d( glow::TEXTURE_2D, 0, glow::RED.try_into().unwrap(), asset.width.into(), asset.height.into(), 0, glow::RED, glow::UNSIGNED_BYTE, Some(&asset.data), ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as _, ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as _, ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as _, ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as _, ); } asset.metadata.push(space_glyph); Ok(asset.metadata) } static FIRST_FONT: &[u8] = include_bytes!("roboto/Roboto-Regular.ttf"); #[derive(Clone, Copy, Debug)] struct Offset(f32, f32); #[derive(Clone, Copy, Debug)] struct Color(f32, f32, f32, f32); #[derive(Clone, Copy, Debug)] struct Config(f32, f32, f32); #[derive(Clone, Copy, Debug)] struct EffectPreset { name: &'static str, background_color: Color, effects: &'static [(Offset, Color, Config)], } static EFFECTS: &[EffectPreset] = &[ EffectPreset { name: "white on grey", background_color: Color(0.314, 0.314, 0.314, 1.0), effects: &[( Offset(0.0, 0.0), Color(0.867, 0.867, 0.867, 1.0), Config(0.5, 2.0, 0.0), )], }, EffectPreset { name: "basic drop shadow", background_color: Color(0.6, 0.5, 0.7, 1.0), effects: &[ ( Offset(0.06, -0.06), Color(0.0, 0.0, 0.0, 0.7), Config(0.5, 2.0, 0.075), ), ( Offset(0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), Config(0.5, 2.0, 0.0), ), ], }, EffectPreset { name: "clouds", background_color: Color(0.0, 0.656, 1.0, 1.0), effects: &[ ( Offset(0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), Config(0.1, 2.0, 0.0), ), ( Offset(0.0, 0.0), Color(0.0, 0.656, 1.0, 1.0), Config(0.45, 2.0, 0.0), ), ], }, EffectPreset { name: "over desktop", background_color: Color(0.0, 0.0, 0.0, 0.0), effects: &[ ( Offset(0.0, 0.0), Color(0.0, 0.0, 0.0, 1.0), Config(0.4, 2.0, 0.0), ), ( Offset(0.0, 0.0), Color(1.0, 1.0, 1.0, 1.0), Config(0.5, 2.0, 0.0), ), ], }, ]; fn main() { unsafe { let event_loop = glutin::event_loop::EventLoop::new(); let window_builder = glutin::window::WindowBuilder::new() .with_title("Hello triangle!") .with_transparent(true) .with_inner_size(glutin::dpi::LogicalSize::new(500.0, 500.0)); let window = glutin::ContextBuilder::new() .with_vsync(true) .build_windowed(window_builder, &event_loop) .unwrap() .make_current() .unwrap(); let gl = glow::Context::from_loader_function(|s| window.get_proc_address(s) as *const _); let vao = gl.create_vertex_array().unwrap(); gl.bind_vertex_array(Some(vao)); let program = setup_shader_program(&gl); gl.use_program(Some(program)); let vbo = gl.create_buffer().unwrap(); gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); let texture = gl.create_texture().unwrap(); let mut glyphs = update_font(&gl, texture, FIRST_FONT).unwrap(); let default_text = "You can type to change this text. Click inside the window for the next style. Drag and drop a font file on the window to change the font."; let mut typed_text = String::new(); let mut current_effect = 0; println!("switched to effect '{}'", EFFECTS[current_effect].name); let logical_size: glutin::dpi::LogicalSize = window .window() .inner_size() .to_logical(window.window().scale_factor()); let mut font_mul_x = 2.0 * DISPLAY_FONT_SIZE / logical_size.width; let mut font_mul_y = 2.0 * DISPLAY_FONT_SIZE / logical_size.height; event_loop.run(move |event, _, control_flow| { use glutin::{ event::{ElementState, Event, MouseButton, WindowEvent}, event_loop::ControlFlow, }; *control_flow = ControlFlow::Wait; match event { Event::RedrawRequested(_) => { let effect_preset = EFFECTS[current_effect]; let Color(r, g, b, a) = effect_preset.background_color; gl.clear_color(r, g, b, a); gl.clear(glow::COLOR_BUFFER_BIT); gl.enable(glow::BLEND); gl.blend_func_separate( glow::ONE, glow::ONE_MINUS_SRC_ALPHA, glow::ONE, glow::ONE, ); let text = if typed_text.is_empty() { default_text } else { &typed_text }; let mut data = Vec::new(); let mut cursor_x = -1.0; let mut cursor_y = 1.0 - font_mul_y; for ch in text.chars() { if let Some(glyph) = glyphs.iter().find(|glyph| glyph.codepoint == ch) { let AdvanceWidth(advance) = glyph.user_data; if !ch.is_whitespace() { if (cursor_x + (advance * font_mul_x)) > 1.0 { cursor_x = -1.0; cursor_y -= font_mul_y; } push_glyph( &mut data, cursor_x, cursor_y, font_mul_x, font_mul_y, glyph, ); } cursor_x += advance * font_mul_x; } } let item_size = std::mem::size_of::<[f32; 4]>(); let raw_buffer = std::slice::from_raw_parts(data.as_ptr().cast(), data.len() * item_size); gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, raw_buffer, glow::DYNAMIC_DRAW); gl.enable_vertex_attrib_array(0); gl.vertex_attrib_pointer_f32(0, 4, glow::FLOAT, false, 0, 0); let offset_uniform = gl.get_uniform_location(program, "offset"); let sdf_uniform = gl.get_uniform_location(program, "sdf"); let color_uniform = gl.get_uniform_location(program, "color"); let config_uniform = gl.get_uniform_location(program, "config"); gl.uniform_1_i32(sdf_uniform.as_ref(), 0); gl.active_texture(glow::TEXTURE0); gl.bind_texture(glow::TEXTURE_2D, Some(texture)); for (Offset(x, y), Color(r, g, b, a), Config(start, end, smoothing)) in effect_preset.effects.iter().copied() { let smoothing = if smoothing == 0.0 { let distance_range_in_px = DISPLAY_FONT_SIZE * (2.0 * PADDING_RATIO); 1.0 / distance_range_in_px } else { smoothing }; gl.uniform_2_f32(offset_uniform.as_ref(), x * font_mul_x, y * font_mul_y); gl.uniform_4_f32(color_uniform.as_ref(), r, g, b, a); gl.uniform_3_f32(config_uniform.as_ref(), start, end, smoothing); gl.draw_arrays(glow::TRIANGLES, 0, data.len() as _); } window.swap_buffers().unwrap(); } Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; } WindowEvent::Resized(size) => { window.resize(size); gl.viewport(0, 0, size.width as _, size.height as _); let logical_size: glutin::dpi::LogicalSize = window .window() .inner_size() .to_logical(window.window().scale_factor()); font_mul_x = 2.0 * DISPLAY_FONT_SIZE / logical_size.width; font_mul_y = 2.0 * DISPLAY_FONT_SIZE / logical_size.height; window.window().request_redraw(); } WindowEvent::DroppedFile(path) => { match std::fs::read(path) { Ok(ttf_data) => match update_font(&gl, texture, &ttf_data) { Ok(new_glyphs) => { glyphs = new_glyphs; } Err(err) => { eprintln!("{}", err); typed_text.replace_range(.., err); } }, Err(err) => { typed_text.clear(); let _ = write!(typed_text, "couldn't read dropped file: {}", err); eprintln!("{}", typed_text); } } window.window().request_redraw(); } WindowEvent::ReceivedCharacter(ch) => { if ch == '\u{8}' { typed_text.pop(); } else if ch == ' ' || latin1().any(|c| c == ch) { typed_text.push(ch); } window.window().request_redraw(); } WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => { current_effect = (current_effect + 1) % EFFECTS.len(); println!("switched to effect '{}'", EFFECTS[current_effect].name); window.window().request_redraw(); } _ => {} }, _ => {} } }); } } unsafe fn setup_shader_program(gl: &glow::Context) -> glow::Program { let program = gl.create_program().unwrap(); let vertex_shader = gl.create_shader(glow::VERTEX_SHADER).unwrap(); let fragment_shader = gl.create_shader(glow::FRAGMENT_SHADER).unwrap(); gl.shader_source( vertex_shader, "#version 410 uniform vec2 offset; layout(location = 0) in vec4 vert_input; out vec2 uv; void main() { uv = vert_input.zw; gl_Position = vec4(offset + vert_input.xy, 0.0, 1.0); } ", ); gl.shader_source( fragment_shader, "#version 410 uniform sampler2D sdf; uniform vec4 color; uniform vec3 config; in vec2 uv; out vec4 out_color; void main() { float dist = texture2D(sdf, uv.xy).r; float start = 0.5 + (dist - config.x) / config.z; float end = 0.5 + (config.y - dist) / config.z; float inside = clamp(min(start, end), 0.0, 1.0); out_color = color * inside; } ", ); gl.compile_shader(vertex_shader); if !gl.get_shader_compile_status(vertex_shader) { panic!("{}", gl.get_shader_info_log(vertex_shader)); } gl.attach_shader(program, vertex_shader); gl.compile_shader(fragment_shader); if !gl.get_shader_compile_status(fragment_shader) { panic!("{}", gl.get_shader_info_log(fragment_shader)); } gl.attach_shader(program, fragment_shader); gl.link_program(program); if !gl.get_program_link_status(program) { panic!("{}", gl.get_program_info_log(program)); } gl.detach_shader(program, vertex_shader); gl.detach_shader(program, fragment_shader); gl.delete_shader(vertex_shader); gl.delete_shader(fragment_shader); program } fn push_glyph( data: &mut Vec<[f32; 4]>, offset_x: f32, offset_y: f32, font_mul_x: f32, font_mul_y: f32, glyph: &Glyph, ) { // first triangle data.push([ offset_x + glyph.left * font_mul_x, offset_y + glyph.bottom * font_mul_y, glyph.tex_left, glyph.tex_bottom, ]); data.push([ offset_x + glyph.right * font_mul_x, offset_y + glyph.bottom * font_mul_y, glyph.tex_right, glyph.tex_bottom, ]); data.push([ offset_x + glyph.left * font_mul_x, offset_y + glyph.top * font_mul_y, glyph.tex_left, glyph.tex_top, ]); // second triangle data.push([ offset_x + glyph.right * font_mul_x, offset_y + glyph.bottom * font_mul_y, glyph.tex_right, glyph.tex_bottom, ]); data.push([ offset_x + glyph.right * font_mul_x, offset_y + glyph.top * font_mul_y, glyph.tex_right, glyph.tex_top, ]); data.push([ offset_x + glyph.left * font_mul_x, offset_y + glyph.top * font_mul_y, glyph.tex_left, glyph.tex_top, ]); }