// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Construction + accessors for [`GlesCanvas`]. //! //! The bulk of `GlesCanvas::new` is one big shader-compile + //! uniform-lookup block: each of the shader programs gets compiled //! from [`super::shaders`], its uniform locations are pulled out via //! `get_uniform_location`, and both end up as `Copy` handles on the //! struct so sub-canvases can share them for free. `sub_canvas` is //! the same struct-literal again with the `new`-only bootstrap //! elided (programs, VAO, default font all come from the parent). use std::collections::HashMap; use std::sync::Arc; use fontdue::{ Font, LineMetrics, Metrics }; use glow::HasContext; use crate::theme::{ FontRegistry, FontStyle }; use super::helpers::{ alloc_fbo_tex, bytemuck_cast_slice, compile_program, compile_program_with_attribs }; use super::shaders:: { BACKDROP_BLUR_H_FRAG_SRC, BACKDROP_COMPOSITE_FRAG_SRC, BACKDROP_FAST_BLUR_H_FRAG_SRC, BACKDROP_FAST_COMPOSITE_FRAG_SRC, BLIT_FRAG_SRC, BLIT_VERT_SRC, GLYPH_FRAG_SRC, GLYPH_BATCH_VERT_SRC, GLYPH_BATCH_FRAG_SRC, LINEAR_GRADIENT_FRAG_SRC, RADIAL_GRADIENT_FRAG_SRC, RECT_FRAG_SRC, CLIP_COMPOSITE_FRAG_SRC, SHADOW_INSET_FRAG_SRC, SHADOW_INSET_OVERLAY_FRAG_SRC, SHADOW_OUTER_FRAG_SRC, SUB_BLIT_FRAG_SRC, TEX_FRAG_SRC, VERT_SRC, }; use super::{ GlesCanvas, GlesVersion }; impl GlesCanvas { /// Build a GPU canvas of `width × height` physical px on an already-current /// EGL/GLES context. Compiles every shader program, looks up its uniforms, /// uploads the shared quad geometry and the glyph atlas (format chosen per ES /// profile), then allocates the persistent shadow FBO and makes it the active /// draw target — every draw writes into the FBO, and [`Self::present`] is the /// only call that blits it to the default framebuffer. `dpi_scale` and /// `global_alpha` start at 1.0; the default font comes from the process-wide /// cached handle. Panics if the FBO is incomplete or a program fails to link. pub fn new( gl: Arc, version: GlesVersion, width: u32, height: u32 ) -> Self { let font_handle = crate::system_fonts::default_handle(); let font = font_handle.font.clone(); let font_bytes = font_handle.bytes.clone(); let font_face = font_handle.face; let rect_program = compile_program( &gl, VERT_SRC, RECT_FRAG_SRC ); let tex_program = compile_program( &gl, VERT_SRC, TEX_FRAG_SRC ); let glyph_program = compile_program( &gl, VERT_SRC, GLYPH_FRAG_SRC ); let glyph_batch_program = compile_program_with_attribs( &gl, GLYPH_BATCH_VERT_SRC, GLYPH_BATCH_FRAG_SRC, &[ ( 0, "a_pos" ), ( 1, "a_uv" ) ] ); let blit_program = compile_program( &gl, BLIT_VERT_SRC, BLIT_FRAG_SRC ); let sub_blit_program = compile_program( &gl, VERT_SRC, SUB_BLIT_FRAG_SRC ); let linear_gradient_program = compile_program( &gl, VERT_SRC, LINEAR_GRADIENT_FRAG_SRC ); let radial_gradient_program = compile_program( &gl, VERT_SRC, RADIAL_GRADIENT_FRAG_SRC ); let shadow_outer_program = compile_program( &gl, VERT_SRC, SHADOW_OUTER_FRAG_SRC ); let shadow_inset_program = compile_program( &gl, VERT_SRC, SHADOW_INSET_FRAG_SRC ); let shadow_inset_overlay_program = compile_program( &gl, VERT_SRC, SHADOW_INSET_OVERLAY_FRAG_SRC ); let backdrop_blur_h_program = compile_program( &gl, BLIT_VERT_SRC, BACKDROP_BLUR_H_FRAG_SRC ); let backdrop_composite_program = compile_program( &gl, VERT_SRC, BACKDROP_COMPOSITE_FRAG_SRC ); let backdrop_fast_blur_h_program = compile_program( &gl, BLIT_VERT_SRC, BACKDROP_FAST_BLUR_H_FRAG_SRC ); let backdrop_fast_composite_program = compile_program( &gl, VERT_SRC, BACKDROP_FAST_COMPOSITE_FRAG_SRC ); // SAFETY: every program in scope has just been linked successfully by // `compile_program` (which panics on link failure), so `get_uniform_location` // on these programs is well-defined. Each name argument is a `'static` // ASCII literal — `glow` will not invoke UB on a malformed C string. // `get_uniform_location` does not mutate the GL state machine, so this // block has no interaction with whatever bindings precede it. let ( u_rect_mvp, u_rect_color, u_rect_size, u_rect_radii, u_rect_stroke, u_rect_pad, u_tex_mvp, u_tex_opacity, u_tex_sampler, u_glyph_mvp, u_glyph_color, u_glyph_opacity, u_glyph_sampler, u_glyph_uv_offset, u_glyph_uv_scale, u_blit_sampler, u_subblit_mvp, u_subblit_sampler, u_subblit_opacity, u_subblit_fade_bottom, u_subblit_height_px, u_lingrad_mvp, u_lingrad_lut, u_lingrad_dir, u_lingrad_size, u_lingrad_line_length, u_lingrad_radii, u_lingrad_pad, u_lingrad_lut_domain_min, u_lingrad_lut_domain_span, u_radgrad_mvp, u_radgrad_lut, u_radgrad_center, u_radgrad_radius_frac, u_radgrad_size, u_radgrad_radii, u_radgrad_pad, u_radgrad_lut_domain_min, u_radgrad_lut_domain_span, u_shadow_mvp, u_shadow_size, u_shadow_padding, u_shadow_radii, u_shadow_spread, u_shadow_sigma, u_shadow_color, u_inset_mvp, u_inset_size, u_inset_padding, u_inset_radii, u_inset_spread, u_inset_sigma, u_inset_offset, u_inset_color, u_inset_ov_mvp, u_inset_ov_size, u_inset_ov_padding, u_inset_ov_radii, u_inset_ov_spread, u_inset_ov_sigma, u_inset_ov_offset, u_inset_ov_color, u_inset_ov_snapshot, u_inset_ov_canvas_size, u_bd_h_source, u_bd_h_texel, u_bd_h_canvas_size, u_bd_h_sigma, u_bd_c_mvp, u_bd_c_source, u_bd_c_canvas_size, u_bd_c_texel, u_bd_c_sigma, u_bd_c_size, u_bd_c_padding, u_bd_c_radii, u_bd_c_tint, u_bd_fh_source, u_bd_fh_texel, u_bd_fh_canvas_size, u_bd_fh_sigma, u_bd_fc_mvp, u_bd_fc_source, u_bd_fc_canvas_size, u_bd_fc_texel, u_bd_fc_sigma, u_bd_fc_size, u_bd_fc_padding, u_bd_fc_radii, u_bd_fc_tint, ) = unsafe {( gl.get_uniform_location( rect_program, "u_mvp" ).unwrap(), gl.get_uniform_location( rect_program, "u_color" ).unwrap(), gl.get_uniform_location( rect_program, "u_size" ).unwrap(), gl.get_uniform_location( rect_program, "u_radii" ).unwrap(), gl.get_uniform_location( rect_program, "u_stroke" ).unwrap(), gl.get_uniform_location( rect_program, "u_pad" ).unwrap(), gl.get_uniform_location( tex_program, "u_mvp" ).unwrap(), gl.get_uniform_location( tex_program, "u_opacity" ).unwrap(), gl.get_uniform_location( tex_program, "u_sampler" ).unwrap(), gl.get_uniform_location( glyph_program, "u_mvp" ).unwrap(), gl.get_uniform_location( glyph_program, "u_color" ).unwrap(), gl.get_uniform_location( glyph_program, "u_opacity" ).unwrap(), gl.get_uniform_location( glyph_program, "u_sampler" ).unwrap(), gl.get_uniform_location( glyph_program, "u_uv_offset" ).unwrap(), gl.get_uniform_location( glyph_program, "u_uv_scale" ).unwrap(), gl.get_uniform_location( blit_program, "u_sampler" ).unwrap(), gl.get_uniform_location( sub_blit_program, "u_mvp" ).unwrap(), gl.get_uniform_location( sub_blit_program, "u_sampler" ).unwrap(), gl.get_uniform_location( sub_blit_program, "u_opacity" ).unwrap(), gl.get_uniform_location( sub_blit_program, "u_fade_bottom_px" ).unwrap(), gl.get_uniform_location( sub_blit_program, "u_height_px" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_mvp" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_lut" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_dir" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_size" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_line_length" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_radii" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_pad" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_lut_domain_min" ).unwrap(), gl.get_uniform_location( linear_gradient_program, "u_lut_domain_span" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_mvp" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_lut" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_center" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_radius_frac" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_size" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_radii" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_pad" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_lut_domain_min" ).unwrap(), gl.get_uniform_location( radial_gradient_program, "u_lut_domain_span" ).unwrap(), gl.get_uniform_location( shadow_outer_program, "u_mvp" ).unwrap(), gl.get_uniform_location( shadow_outer_program, "u_size" ).unwrap(), gl.get_uniform_location( shadow_outer_program, "u_padding" ).unwrap(), gl.get_uniform_location( shadow_outer_program, "u_radii" ).unwrap(), gl.get_uniform_location( shadow_outer_program, "u_spread" ).unwrap(), gl.get_uniform_location( shadow_outer_program, "u_sigma" ).unwrap(), gl.get_uniform_location( shadow_outer_program, "u_color" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_mvp" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_size" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_padding" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_radii" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_spread" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_sigma" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_offset" ).unwrap(), gl.get_uniform_location( shadow_inset_program, "u_color" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_mvp" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_size" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_padding" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_radii" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_spread" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_sigma" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_offset" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_color" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_snapshot" ).unwrap(), gl.get_uniform_location( shadow_inset_overlay_program, "u_canvas_size" ).unwrap(), gl.get_uniform_location( backdrop_blur_h_program, "u_source" ).unwrap(), gl.get_uniform_location( backdrop_blur_h_program, "u_texel" ).unwrap(), gl.get_uniform_location( backdrop_blur_h_program, "u_canvas_size" ).unwrap(), gl.get_uniform_location( backdrop_blur_h_program, "u_sigma" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_mvp" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_source" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_canvas_size" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_texel" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_sigma" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_size" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_padding" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_radii" ).unwrap(), gl.get_uniform_location( backdrop_composite_program, "u_tint" ).unwrap(), gl.get_uniform_location( backdrop_fast_blur_h_program, "u_source" ).unwrap(), gl.get_uniform_location( backdrop_fast_blur_h_program, "u_texel" ).unwrap(), gl.get_uniform_location( backdrop_fast_blur_h_program, "u_canvas_size" ).unwrap(), gl.get_uniform_location( backdrop_fast_blur_h_program, "u_sigma" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_mvp" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_source" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_canvas_size" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_texel" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_sigma" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_size" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_padding" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_radii" ).unwrap(), gl.get_uniform_location( backdrop_fast_composite_program, "u_tint" ).unwrap(), )}; let ( u_glyph_batch_color, u_glyph_batch_opacity, u_glyph_batch_sampler ) = unsafe {( gl.get_uniform_location( glyph_batch_program, "u_color" ).unwrap(), gl.get_uniform_location( glyph_batch_program, "u_opacity" ).unwrap(), gl.get_uniform_location( glyph_batch_program, "u_sampler" ).unwrap(), )}; let clip_composite_program = compile_program( &gl, VERT_SRC, CLIP_COMPOSITE_FRAG_SRC ); let ( u_clip_mvp, u_clip_layer, u_clip_mask, u_clip_canvas, u_clip_bbox ) = unsafe {( gl.get_uniform_location( clip_composite_program, "u_mvp" ).unwrap(), gl.get_uniform_location( clip_composite_program, "u_layer" ).unwrap(), gl.get_uniform_location( clip_composite_program, "u_mask" ).unwrap(), gl.get_uniform_location( clip_composite_program, "u_canvas" ).unwrap(), gl.get_uniform_location( clip_composite_program, "u_bbox" ).unwrap(), )}; let ( glyph_batch_vao, glyph_batch_vbo ) = unsafe { let vao = gl.create_vertex_array().unwrap(); let vbo = gl.create_buffer().unwrap(); gl.bind_vertex_array( Some( vao ) ); gl.bind_buffer( glow::ARRAY_BUFFER, Some( vbo ) ); gl.enable_vertex_attrib_array( 0 ); gl.vertex_attrib_pointer_f32( 0, 2, glow::FLOAT, false, 16, 0 ); gl.enable_vertex_attrib_array( 1 ); gl.vertex_attrib_pointer_f32( 1, 2, glow::FLOAT, false, 16, 8 ); gl.bind_vertex_array( None ); ( vao, vbo ) }; let quad_vertices: [f32; 12] = [ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, ]; // SAFETY: the GL context is current (caller contract). We allocate a // VAO + VBO, upload `quad_vertices` (24 bytes, statically known size, // matches `STATIC_DRAW` semantics), and configure attribute 0 to read // 2-float vertices from the VBO at offset 0 with stride 8. The vertex // attrib pointer is bound to `vbo` because `vbo` is the current // `ARRAY_BUFFER` binding when `vertex_attrib_pointer_f32` runs. We // unbind the VAO at the end so we don't strand a binding the rest of // `new` might inherit; the VBO remains attached to the VAO and is not // touched again until `Drop`. let ( quad_vao, quad_vbo ) = unsafe { let vao = gl.create_vertex_array().unwrap(); let vbo = gl.create_buffer().unwrap(); gl.bind_vertex_array( Some( vao ) ); gl.bind_buffer( glow::ARRAY_BUFFER, Some( vbo ) ); gl.buffer_data_u8_slice( glow::ARRAY_BUFFER, bytemuck_cast_slice( &quad_vertices ), glow::STATIC_DRAW, ); gl.enable_vertex_attrib_array( 0 ); gl.vertex_attrib_pointer_f32( 0, 2, glow::FLOAT, false, 8, 0 ); gl.bind_vertex_array( None ); ( vao, vbo ) }; // Build the persistent FBO (shadow canvas) and bind it as the active // draw target. From here on, every draw method writes into the FBO; // `present` is the only call that switches to the default framebuffer. // // SAFETY: the GL context is current. `create_framebuffer` allocates a // fresh FBO name. `alloc_fbo_tex` (an `unsafe fn`) requires the same // invariant and returns a colour-renderable RGBA texture sized to the // caller's width × height — the call is sound because both invariants // are established. The bind + framebuffer_texture_2d pair attach the // texture to COLOR_ATTACHMENT0; `check_framebuffer_status` is the // canonical post-condition check and will assert before we return a // handle to a broken FBO. The trailing GL state (`BLEND`, `blend_func`, // `viewport`, `clear`) is the canvas-wide default state every draw // method assumes — see the comment block beside `blend_func` for why // `(ONE, ONE_MINUS_SRC_ALPHA)` is the correct pair for premultiplied // shaders. let ( fbo, fbo_tex ) = unsafe { let fbo = gl.create_framebuffer().expect( "create_framebuffer" ); let fbo_tex = alloc_fbo_tex( &gl, version, width, height ); gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) ); gl.framebuffer_texture_2d( glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some( fbo_tex ), 0, ); let status = gl.check_framebuffer_status( glow::FRAMEBUFFER ); assert_eq!( status, glow::FRAMEBUFFER_COMPLETE, "FBO incomplete: 0x{status:x}" ); gl.enable( glow::BLEND ); // Premultiplied-alpha "over" composite: `result = src + dst * (1 - src.a)` // applied uniformly to both colour and alpha. All shaders in this // pipeline emit premul colour (see the note above `RECT_FRAG_SRC`), // so `(ONE, ONE_MINUS_SRC_ALPHA)` is correct for both channels. // Premultiplied inputs are also a requirement for the plus-lighter // and overlay blend modes. gl.blend_func( glow::ONE, glow::ONE_MINUS_SRC_ALPHA ); gl.viewport( 0, 0, width as i32, height as i32 ); // Clear the FBO once at startup so the first frame is not garbage. gl.clear_color( 0.0, 0.0, 0.0, 0.0 ); gl.clear( glow::COLOR_BUFFER_BIT ); ( fbo, fbo_tex ) }; // Atlas format picked per ES profile: GL_R8 / GL_RED on ES3 // (GL_LUMINANCE is deprecated in ES3 core and some mobile drivers // — Mali, certain Adreno — return GL_INVALID_ENUM for it), legacy // GL_LUMINANCE on ES2 where the modern single-channel formats do // not exist. The glyph fragment shader samples `.r` and works for // both: GL_RED puts the byte in .r directly, GL_LUMINANCE // replicates it into .r=.g=.b, so reading `.r` gives the same // coverage value either way. let ( atlas_internal, atlas_format ) = match version { GlesVersion::V3 => ( glow::R8 as i32, glow::RED ), GlesVersion::V2 => ( glow::LUMINANCE as i32, glow::LUMINANCE ), }; let atlas_texture = unsafe { let tex = gl.create_texture().unwrap(); gl.bind_texture( glow::TEXTURE_2D, Some( tex ) ); gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 1 ); gl.tex_image_2d( glow::TEXTURE_2D, 0, atlas_internal, super::ATLAS_SIZE as i32, super::ATLAS_SIZE as i32, 0, atlas_format, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice( None ), ); gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 4 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 ); gl.bind_texture( glow::TEXTURE_2D, None ); tex }; Self { gl, version, font, font_bytes, font_face, font_registry: None, dpi_scale: 1.0, layout_viewport: None, global_alpha: 1.0, width, height, rect_program, tex_program, glyph_program, blit_program, sub_blit_program, linear_gradient_program, radial_gradient_program, shadow_outer_program, shadow_inset_program, shadow_inset_overlay_program, quad_vao, _quad_vbo: quad_vbo, u_rect_mvp, u_rect_color, u_rect_size, u_rect_radii, u_rect_stroke, u_rect_pad, u_tex_mvp, u_tex_opacity, u_tex_sampler, u_glyph_mvp, u_glyph_color, u_glyph_opacity, u_glyph_sampler, u_glyph_uv_offset, u_glyph_uv_scale, glyph_batch_program, u_glyph_batch_color, u_glyph_batch_opacity, u_glyph_batch_sampler, glyph_batch_vao, glyph_batch_vbo, u_blit_sampler, u_subblit_mvp, u_subblit_sampler, u_subblit_opacity, u_subblit_fade_bottom, u_subblit_height_px, u_lingrad_mvp, u_lingrad_lut, u_lingrad_dir, u_lingrad_size, u_lingrad_line_length, u_lingrad_radii, u_lingrad_pad, u_lingrad_lut_domain_min, u_lingrad_lut_domain_span, u_radgrad_mvp, u_radgrad_lut, u_radgrad_center, u_radgrad_radius_frac, u_radgrad_size, u_radgrad_radii, u_radgrad_pad, u_radgrad_lut_domain_min, u_radgrad_lut_domain_span, u_shadow_mvp, u_shadow_size, u_shadow_padding, u_shadow_radii, u_shadow_spread, u_shadow_sigma, u_shadow_color, u_inset_mvp, u_inset_size, u_inset_padding, u_inset_radii, u_inset_spread, u_inset_sigma, u_inset_offset, u_inset_color, u_inset_ov_mvp, u_inset_ov_size, u_inset_ov_padding, u_inset_ov_radii, u_inset_ov_spread, u_inset_ov_sigma, u_inset_ov_offset, u_inset_ov_color, u_inset_ov_snapshot, u_inset_ov_canvas_size, backdrop_blur_h_program, u_bd_h_source, u_bd_h_texel, u_bd_h_canvas_size, u_bd_h_sigma, backdrop_composite_program, u_bd_c_mvp, u_bd_c_source, u_bd_c_canvas_size, u_bd_c_texel, u_bd_c_sigma, u_bd_c_size, u_bd_c_padding, u_bd_c_radii, u_bd_c_tint, backdrop_fast_blur_h_program, u_bd_fh_source, u_bd_fh_texel, u_bd_fh_canvas_size, u_bd_fh_sigma, backdrop_fast_composite_program, u_bd_fc_mvp, u_bd_fc_source, u_bd_fc_canvas_size, u_bd_fc_texel, u_bd_fc_sigma, u_bd_fc_size, u_bd_fc_padding, u_bd_fc_radii, u_bd_fc_tint, atlas_texture, atlas_format, atlas_cursor_x: 0, atlas_cursor_y: 0, atlas_row_height: 0, glyph_cache: HashMap::new(), image_cache: HashMap::new(), gradient_lut_cache: HashMap::new(), clip_scissor: None, clip_layer: None, clip_layer_active: false, clip_mask_tex: None, clip_bbox: crate::types::Rect::default(), clip_composite_program, u_clip_mvp, u_clip_layer, u_clip_mask, u_clip_canvas, u_clip_bbox, fbo, fbo_tex, aux_a: None, aux_b: None, } } /// Build a sub-canvas: a separate render target sharing this canvas's GL /// context, font, shader programs, geometry, and uniform locations, but /// with its own FBO + color texture sized to `width × height`. Used to /// render content into an off-screen target that can then be composited /// back via [`Self::blit`]. /// /// The returned canvas inherits the parent's `dpi_scale` and `global_alpha` /// (so glyphs render at the same pixel size). Its glyph cache starts empty /// — re-rasterising on first use is the cost of not sharing GL textures /// across canvases. pub fn sub_canvas( &self, width: u32, height: u32 ) -> GlesCanvas { let gl = Arc::clone( &self.gl ); // SAFETY: `self.gl` is the same context held by `self`; if `self` was // constructed soundly its context is current on this thread. We // allocate a new FBO + colour texture and attach them, then assert // completeness. We deliberately leave `gl` with the new FBO bound // instead of restoring the parent's binding — every draw method goes // through `activate_target`, which re-binds the canvas's own FBO // before issuing any draw call, so the transient binding cannot be // observed by other code on this thread. let ( fbo, fbo_tex ) = unsafe { let fbo = gl.create_framebuffer().expect( "create_framebuffer" ); let fbo_tex = alloc_fbo_tex( &gl, self.version, width, height ); gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) ); gl.framebuffer_texture_2d( glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some( fbo_tex ), 0, ); let status = gl.check_framebuffer_status( glow::FRAMEBUFFER ); assert_eq!( status, glow::FRAMEBUFFER_COMPLETE, "sub-FBO incomplete: 0x{status:x}" ); ( fbo, fbo_tex ) }; let atlas_format = self.atlas_format; let atlas_internal = match self.version { GlesVersion::V3 => glow::R8 as i32, GlesVersion::V2 => glow::LUMINANCE as i32, }; let atlas_texture = unsafe { let tex = gl.create_texture().unwrap(); gl.bind_texture( glow::TEXTURE_2D, Some( tex ) ); gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 1 ); gl.tex_image_2d( glow::TEXTURE_2D, 0, atlas_internal, super::ATLAS_SIZE as i32, super::ATLAS_SIZE as i32, 0, atlas_format, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice( None ), ); gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 4 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 ); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 ); gl.bind_texture( glow::TEXTURE_2D, None ); tex }; GlesCanvas { gl, version: self.version, font: Arc::clone( &self.font ), font_bytes: Arc::clone( &self.font_bytes ), font_face: self.font_face, font_registry: self.font_registry.as_ref().map( Arc::clone ), dpi_scale: self.dpi_scale, layout_viewport: Some( self.layout_viewport.unwrap_or( ( self.width as f32, self.height as f32 ) ) ), global_alpha: self.global_alpha, width, height, rect_program: self.rect_program, tex_program: self.tex_program, glyph_program: self.glyph_program, blit_program: self.blit_program, sub_blit_program: self.sub_blit_program, linear_gradient_program: self.linear_gradient_program, radial_gradient_program: self.radial_gradient_program, shadow_outer_program: self.shadow_outer_program, shadow_inset_program: self.shadow_inset_program, shadow_inset_overlay_program: self.shadow_inset_overlay_program, backdrop_blur_h_program: self.backdrop_blur_h_program, backdrop_composite_program: self.backdrop_composite_program, backdrop_fast_blur_h_program: self.backdrop_fast_blur_h_program, backdrop_fast_composite_program: self.backdrop_fast_composite_program, quad_vao: self.quad_vao, _quad_vbo: self._quad_vbo, u_rect_mvp: self.u_rect_mvp, u_rect_color: self.u_rect_color, u_rect_size: self.u_rect_size, u_rect_radii: self.u_rect_radii, u_rect_stroke: self.u_rect_stroke, u_rect_pad: self.u_rect_pad, u_tex_mvp: self.u_tex_mvp, u_tex_opacity: self.u_tex_opacity, u_tex_sampler: self.u_tex_sampler, u_glyph_mvp: self.u_glyph_mvp, u_glyph_color: self.u_glyph_color, u_glyph_opacity: self.u_glyph_opacity, u_glyph_sampler: self.u_glyph_sampler, u_glyph_uv_offset: self.u_glyph_uv_offset, u_glyph_uv_scale: self.u_glyph_uv_scale, glyph_batch_program: self.glyph_batch_program, u_glyph_batch_color: self.u_glyph_batch_color, u_glyph_batch_opacity: self.u_glyph_batch_opacity, u_glyph_batch_sampler: self.u_glyph_batch_sampler, glyph_batch_vao: self.glyph_batch_vao, glyph_batch_vbo: self.glyph_batch_vbo, u_blit_sampler: self.u_blit_sampler, u_subblit_mvp: self.u_subblit_mvp, u_subblit_sampler: self.u_subblit_sampler, u_subblit_opacity: self.u_subblit_opacity, u_subblit_fade_bottom: self.u_subblit_fade_bottom, u_subblit_height_px: self.u_subblit_height_px, u_lingrad_mvp: self.u_lingrad_mvp, u_lingrad_lut: self.u_lingrad_lut, u_lingrad_dir: self.u_lingrad_dir, u_lingrad_size: self.u_lingrad_size, u_lingrad_line_length: self.u_lingrad_line_length, u_lingrad_radii: self.u_lingrad_radii, u_lingrad_pad: self.u_lingrad_pad, u_lingrad_lut_domain_min: self.u_lingrad_lut_domain_min, u_lingrad_lut_domain_span: self.u_lingrad_lut_domain_span, u_radgrad_mvp: self.u_radgrad_mvp, u_radgrad_lut: self.u_radgrad_lut, u_radgrad_center: self.u_radgrad_center, u_radgrad_radius_frac: self.u_radgrad_radius_frac, u_radgrad_size: self.u_radgrad_size, u_radgrad_radii: self.u_radgrad_radii, u_radgrad_pad: self.u_radgrad_pad, u_radgrad_lut_domain_min: self.u_radgrad_lut_domain_min, u_radgrad_lut_domain_span: self.u_radgrad_lut_domain_span, u_shadow_mvp: self.u_shadow_mvp, u_shadow_size: self.u_shadow_size, u_shadow_padding: self.u_shadow_padding, u_shadow_radii: self.u_shadow_radii, u_shadow_spread: self.u_shadow_spread, u_shadow_sigma: self.u_shadow_sigma, u_shadow_color: self.u_shadow_color, u_inset_mvp: self.u_inset_mvp, u_inset_size: self.u_inset_size, u_inset_padding: self.u_inset_padding, u_inset_radii: self.u_inset_radii, u_inset_spread: self.u_inset_spread, u_inset_sigma: self.u_inset_sigma, u_inset_offset: self.u_inset_offset, u_inset_color: self.u_inset_color, u_inset_ov_mvp: self.u_inset_ov_mvp, u_inset_ov_size: self.u_inset_ov_size, u_inset_ov_padding: self.u_inset_ov_padding, u_inset_ov_radii: self.u_inset_ov_radii, u_inset_ov_spread: self.u_inset_ov_spread, u_inset_ov_sigma: self.u_inset_ov_sigma, u_inset_ov_offset: self.u_inset_ov_offset, u_inset_ov_color: self.u_inset_ov_color, u_inset_ov_snapshot: self.u_inset_ov_snapshot, u_inset_ov_canvas_size: self.u_inset_ov_canvas_size, u_bd_h_source: self.u_bd_h_source, u_bd_h_texel: self.u_bd_h_texel, u_bd_h_canvas_size: self.u_bd_h_canvas_size, u_bd_h_sigma: self.u_bd_h_sigma, u_bd_c_mvp: self.u_bd_c_mvp, u_bd_c_source: self.u_bd_c_source, u_bd_c_canvas_size: self.u_bd_c_canvas_size, u_bd_c_texel: self.u_bd_c_texel, u_bd_c_sigma: self.u_bd_c_sigma, u_bd_c_size: self.u_bd_c_size, u_bd_c_padding: self.u_bd_c_padding, u_bd_c_radii: self.u_bd_c_radii, u_bd_c_tint: self.u_bd_c_tint, u_bd_fh_source: self.u_bd_fh_source, u_bd_fh_texel: self.u_bd_fh_texel, u_bd_fh_canvas_size: self.u_bd_fh_canvas_size, u_bd_fh_sigma: self.u_bd_fh_sigma, u_bd_fc_mvp: self.u_bd_fc_mvp, u_bd_fc_source: self.u_bd_fc_source, u_bd_fc_canvas_size: self.u_bd_fc_canvas_size, u_bd_fc_texel: self.u_bd_fc_texel, u_bd_fc_sigma: self.u_bd_fc_sigma, u_bd_fc_size: self.u_bd_fc_size, u_bd_fc_padding: self.u_bd_fc_padding, u_bd_fc_radii: self.u_bd_fc_radii, u_bd_fc_tint: self.u_bd_fc_tint, atlas_texture, atlas_format, atlas_cursor_x: 0, atlas_cursor_y: 0, atlas_row_height: 0, glyph_cache: HashMap::new(), image_cache: HashMap::new(), gradient_lut_cache: HashMap::new(), clip_scissor: None, clip_layer: None, clip_layer_active: false, clip_mask_tex: None, clip_bbox: crate::types::Rect::default(), clip_composite_program: self.clip_composite_program, u_clip_mvp: self.u_clip_mvp, u_clip_layer: self.u_clip_layer, u_clip_mask: self.u_clip_mask, u_clip_canvas: self.u_clip_canvas, u_clip_bbox: self.u_clip_bbox, fbo, fbo_tex, aux_a: None, aux_b: None, } } /// `( width, height )` of the FBO in physical px. pub fn size( &self ) -> ( u32, u32 ) { ( self.width, self.height ) } /// Discard all cached gradient LUT textures. Call after a theme change /// so stale LUTs for old palette colours are freed and rebuilt fresh. pub fn clear_gradient_cache( &mut self ) { // SAFETY: GL context is current (canvas invariant). Each texture in // `gradient_lut_cache` was created through this same context in // `gradient.rs::ensure_lut`, so deleting them through the same context // is well-defined. `drain` consumes the entries so the same name is // never deleted twice. unsafe { for ( _, tex ) in self.gradient_lut_cache.drain() { self.gl.delete_texture( tex ); } } } /// DPI scale factor applied to font sizes before rasterisation. pub fn dpi_scale( &self ) -> f32 { self.dpi_scale } /// Set the DPI scale factor applied to font sizes. pub fn set_dpi_scale( &mut self, s: f32 ) { self.dpi_scale = s; } /// Global alpha multiplier applied to every draw (0.0 transparent, 1.0 opaque). pub fn global_alpha( &self ) -> f32 { self.global_alpha } /// Set the global alpha multiplier applied to every draw. pub fn set_global_alpha( &mut self, a: f32 ) { self.global_alpha = a; } /// The canvas default font, used when no specific face is resolved. pub fn font( &self ) -> &Font { &self.font } /// Install a theme font registry so [`Self::font_for`] can resolve /// family+weight+style triples declared by the theme's `fonts` block. pub fn set_font_registry( &mut self, registry: Arc ) { self.font_registry = Some( registry ); } /// Resolve a specific font from the theme registry, falling back to the /// canvas' default [`Self::font`] when no registry is installed or the /// triple cannot be satisfied. pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc { self.font_registry .as_ref() .and_then( |r| r.resolve( family, weight, style ) ) .unwrap_or_else( || Arc::clone( &self.font ) ) } /// Pick the right font for `ch`. Tries the primary [`Self::font`] /// first; on a miss, delegates to the crate-private system-fonts /// fallback chain (lazy load of the relevant Noto pack). Falls /// back to the primary (which paints a `.notdef` box) when no /// installed fallback covers the codepoint. pub fn font_for_char( &self, ch: char ) -> Arc { if self.font.lookup_glyph_index( ch ) != 0 { return Arc::clone( &self.font ); } crate::system_fonts::lookup( ch ).unwrap_or_else( || Arc::clone( &self.font ) ) } /// Glyph metrics for `ch` at `size` logical px, resolved through the fallback /// chain and pre-scaled by `dpi_scale`. pub fn font_metrics( &self, ch: char, size: f32 ) -> Metrics { self.font_for_char( ch ).metrics( ch, size * self.dpi_scale ) } /// Horizontal line metrics of the default font at `size` px (`None` if the /// font lacks them). Not pre-scaled by `dpi_scale`. pub fn font_line_metrics( &self, size: f32 ) -> Option { self.font.horizontal_line_metrics( size ) } /// Resize the FBO and viewport. The previous color attachment is freed and /// a fresh one of the new size is attached — frame-N pixels are dropped, so /// the caller should expect to do a full redraw immediately after a resize. pub fn resize( &mut self, width: u32, height: u32 ) { if width == self.width && height == self.height { return; } self.width = width; self.height = height; // SAFETY: GL context is current (canvas invariant). Sequence: // release the old colour attachment (created via this context in // `new`/`sub_canvas`/last `resize`), allocate a fresh one of the // new size, swap it in for `COLOR_ATTACHMENT0` of the existing FBO, // resize the viewport to match, and clear so the first frame after // resize is well-defined RGBA. `self.fbo` is unchanged and remains // valid; only its colour attachment is replaced. unsafe { self.gl.delete_texture( self.fbo_tex ); self.fbo_tex = alloc_fbo_tex( &self.gl, self.version, width, height ); self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) ); self.gl.framebuffer_texture_2d( glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some( self.fbo_tex ), 0, ); // The clip layer was sized for the old dimensions — free it so the next // path clip re-allocates at the new size; any in-flight clip is dropped. if let Some( ( fbo, tex ) ) = self.clip_layer.take() { self.gl.delete_framebuffer( fbo ); self.gl.delete_texture( tex ); } if let Some( tex ) = self.clip_mask_tex.take() { self.gl.delete_texture( tex ); } self.gl.viewport( 0, 0, width as i32, height as i32 ); self.gl.clear_color( 0.0, 0.0, 0.0, 0.0 ); self.gl.clear( glow::COLOR_BUFFER_BIT ); } self.clip_layer_active = false; // Auxiliary textures were sized for the old dimensions — drop them so // the next effect that needs them re-allocates at the new size. self.invalidate_aux(); } }