Files
ltk/src/gles_render/setup.rs
Pedro M. de Echanove Pasquin 14572ebfb6
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
render: sub-canvases inherit the root layout viewport for fluid resolution
Content drawn inside a scroll rendered smaller than identical content outside it: the scroll viewport draws its child into a sub-canvas sized to the viewport rect, and Canvas::viewport_layout / viewport_logical — the viewports that geom_px and font_px resolve fluid Lengths against — returned the canvas's own size. Inside a 312 px sub-canvas on a 360 px surface every fluid geometry (icon sizes, row heights, paddings) and font size resolved against 312 instead of 360, shrinking the scroll content by the width ratio, ~13 % in a phone-sized window with 24 px margins. First observed in Eydos Settings' Wi-Fi list, where the connected network row sits outside the scroll and the rest inside: the same full-fan signal icon measured 21x18 px outside and 18x16 px inside.
Add a layout_viewport field to SoftwareCanvas and GlesCanvas, None on root canvases and set by sub_canvas to the parent's effective layout viewport, so nested sub-canvases keep propagating the root surface size. viewport_layout and viewport_logical consult the inherited value before falling back to the canvas size. Root canvases are untouched, and the GLES clip layers, which reuse sub_canvas, get the same correction.
2026-07-30 15:23:52 +02:00

821 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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<glow::Context>, 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<FontRegistry> )
{
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<Font>
{
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<Font>
{
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<LineMetrics>
{
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();
}
}