event_loop, a11y, text_shaping: AccessKit AT-SPI2 bridge, cross-app clipboard, xdg-activation, HarfBuzz shaping, multi-touch hooks
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Five orthogonal capabilities land together because they share the same `try_run` plumbing: an optional global is bound at startup, a piece of state is added to `AppData`, the run-loop iteration drains an inbox / pushes a frame snapshot, and the public surface gains a small set of opt-in `App` hooks. Nothing here breaks an existing app — every new path degrades to a no-op when the compositor does not advertise the relevant global or when the platform adapter cannot start.
AT-SPI2 accessibility via AccessKit. A new `src/a11y/` module owns the platform adapter and the inbound `ActionRequest` channel. `A11yState::try_new` constructs an `accesskit_unix::Adapter`; when the AT-SPI2 daemon is not on the session bus (headless CI, locked-down compositors) the constructor returns `None` and the rest of the pipeline runs unchanged. After every successful `draw_frame`, the run loop builds a fresh `accesskit::TreeUpdate` from `widget_rects` and pushes it through the adapter — main surface plus every visible overlay, each translated to global coordinates via `surface_offset_for` so screen readers report positions in the same frame the user sees. Buttons / toggles / checkboxes / radios / list items / sliders / text edits map to the matching `Role`s; `Click` and `Focus` actions are advertised on every interactive node; inbound action requests are drained at the top of each iteration and translated into a synthetic press / focus on the matching widget. The integration is documented as best-effort in `docs/architecture.md` under "Known gaps and non-goals": hierarchical nesting, per-widget accessible names, live regions and `Action::SetValue` are listed as the natural follow-ups that the foundation now supports but does not yet wire.
Cross-application clipboard via `wl_data_device_manager`. A new `src/event_loop/data_device.rs` bridges the existing process-local `clipboard: String` to the Wayland selection. Outbound (Ctrl+C / Cut): after the local clipboard is populated, `publish_clipboard_selection` creates a `CopyPasteSource` offering `text/plain;charset=utf-8` and installs it as the seat's selection; `DataSourceHandler::send` writes the cached string into the fd the peer hands us. Inbound (Ctrl+V from another app): `DataDeviceHandler::selection` asks for the offered text via `WlDataOffer::receive`, spawns a tiny worker thread to drain the read pipe with a 16 MiB cap to prevent paste-bomb DoS, and posts the result back through an `mpsc::Sender` that the run loop drains each iteration into `data.clipboard`. The `clipboard:` field's doc-comment is updated to reflect the new behaviour: process-local when the compositor does not advertise the global, synchronised with the seat selection otherwise.
External drag-and-drop reception. The same `data_device` module handles `DragOffer` enter / motion / leave / drop_performed: `on_drop_motion( x, y )` fires while the drag hovers over the surface, `on_drop_leave()` when it withdraws without dropping, and `on_drop_received( x, y, mime, text )` when an external payload (`text/uri-list`, `text/plain`, …) is released on top of an ltk window. The receive path reuses the same worker-thread / channel pattern as the clipboard so the run loop never blocks on the read fd. Three new `App` hooks expose the events with no-op defaults; apps that ignore them get the previous behaviour.
`xdg-activation-v1`. The global is bound optionally; when it is present, `try_run` reads `$XDG_ACTIVATION_TOKEN` from the environment, removes it immediately (single-use; preventing leaks into child processes) and stashes it on `AppData::activation_token_pending`. After the first successful configure of the main surface — the earliest point at which `xdg_activation_v1.activate` is meaningful — the token is consumed once and the surface raised to focus. Compositors without the global leave `activation_state` as `None` and the inbound path silently degrades. An `App::request_activation_token` outbound path is reserved on the trait but not yet exercised here.
HarfBuzz shaping. A new `src/text_shaping.rs::shape_line` drives both renderers: the logical-order string is run through `unicode-bidi`, split into per-font sub-runs, and shaped through `rustybuzz`. Each `PositionedGlyph` carries the per-font `glyph_id`, the visual advance and the ink offsets — exactly what `fontdue::Font::rasterize_indexed` needs to render Arabic connected forms, Devanagari clusters and CJK shaped glyphs correctly. The GLES atlas is re-keyed on `(glyph_id, size_bits, font_id)` so glyphs from different fonts at the same size no longer collide, and the atlas format is selected per ES profile (`GL_R8` / `GL_RED` on ES3, `GL_LUMINANCE` on ES2) — the fragment shader samples `.r` for both, since `GL_LUMINANCE` replicates the coverage byte into `.r=.g=.b`. Software path follows the same key. New `Cargo.toml` deps: `unicode-bidi = "0.3"`, `rustybuzz = "0.14"`.
Multi-touch hooks. `App::on_touch_down / on_touch_move / on_touch_up( id, x, y )` expose the raw `wl_touch.id` of every secondary finger. The first finger to land remains the *primary slot* and is fed through the regular gesture machine (`on_pointer_*`, swipe, scroll, long-press, drag-and-drop). Every additional finger fires the new callbacks instead, leaving the existing single-slot behaviour untouched for apps that do not override them. This is the substrate for app-defined pinch-zoom / two-finger pan; the toolkit itself does not yet ship a built-in pinch gesture (called out in the same "Known gaps" doc section).
`event_loop::frame` extracted from `draw/mod.rs`. The `draw_frame` orchestrator and its per-format SHM helper (`pick_shm_format`) move into `src/event_loop/frame.rs`, leaving `draw/` strictly responsible for per-surface paint primitives. The import in `event_loop/run.rs` is rewritten accordingly; `draw/mod.rs` shrinks from 192-line orchestrator to a thin module index.
Overlay teardown safety. `AppData::discard_overlay( id )` synchronously removes a destroyed overlay from the map and rewrites every per-device focus that pointed at it (pointer, keyboard, every touch slot), migrating an in-flight long-press drag to the main surface the same way `reconcile_overlays` does. Used by the compositor-driven destruction paths (`PopupHandler::done`, `LayerShellHandler::closed`) where waiting for the next reconcile would leave a window in which `surface()` / `surface_mut()` panic. The non-panicking siblings `try_surface` / `try_surface_mut` are added for callers on async dispatch paths (IME `Done`, tooltip arm) that may race a teardown.
Miscellaneous. CI: `master` → `main` to match the actual default branch. `Makefile` adds `cargo run --example dialog` to the examples target. `src/lib.rs` re-exports `widget::scroll::ScrollAxis` so apps can configure a `scroll()` axis without reaching into a `pub(crate)` module. `Cargo.toml` adds `accesskit = "0.17"` and `accesskit_unix = "0.13"`. `docs/architecture.md` gains the "Known gaps and non-goals" section that enumerates the new capabilities, what still ships flat, and what is deferred (per-widget a11y labels, primary selection, intra-process multi-touch gestures, `wp_fractional_scale_v1`).
This commit is contained in:
2026-05-16 22:09:59 +02:00
parent 4aa3480b64
commit 4a80165428
48 changed files with 3088 additions and 645 deletions

View File

@@ -153,53 +153,14 @@ pub( super ) unsafe fn alloc_fbo_tex( gl: &glow::Context, version: GlesVersion,
}
}
pub( super ) fn upload_alpha_texture( gl: &glow::Context, data: &[u8], w: i32, h: i32 ) -> glow::Texture
{
// SAFETY: caller's GL context is current. Caller is responsible for the
// `data.len() == w * h` invariant — `upload_alpha_texture` is only called
// from the glyph atlas path on bitmaps fontdue produced at the same
// dimensions. UNPACK_ALIGNMENT is set to 1 for the upload (1 byte/pixel)
// and restored to the GL default of 4 immediately after, so subsequent
// uploads are not affected.
unsafe
{
let tex = gl.create_texture().unwrap();
gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
// NEAREST, not LINEAR. Glyph atlases are drawn 1:1 with their bitmap
// (dest size = texture size, integer-aligned position). Mathematically
// LINEAR at an exact texel center collapses to the texel value, but
// mediump precision in the fragment shader (and the `1 - v_uv.y` flip)
// can drift the sample point a fraction of a texel off-center, and the
// LINEAR filter then blends with neighbours — visible as soft, washed
// stems, especially at small sizes. NEAREST snaps to the correct texel
// every time, matching the software path's pixel-perfect bitmap copy.
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_LUMINANCE is 1 byte/pixel; default UNPACK_ALIGNMENT (4) misreads
// any row whose width is not a multiple of 4, scrambling those glyphs.
gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 1 );
gl.tex_image_2d(
glow::TEXTURE_2D, 0, glow::LUMINANCE as i32,
w, h, 0, glow::LUMINANCE, glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice( Some( data ) ),
);
gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 4 );
gl.bind_texture( glow::TEXTURE_2D, None );
tex
}
}
pub( super ) fn compile_program( gl: &glow::Context, vert_src: &str, frag_src: &str ) -> glow::Program
{
// SAFETY: caller's GL context is current. The shaders are compiled and
// linked inside this block; on success `program` is a fresh, fully linked
// GL program object. Vertex / fragment shaders are released with
// `delete_shader` after attach + link — they are flagged for deletion and
// freed when the program is deleted, which is the canonical pattern.
// Compile / link asserts panic with the driver's info log instead of
// returning a half-broken program (callers cannot recover anyway).
compile_program_with_attribs( gl, vert_src, frag_src, &[ ( 0, "a_pos" ) ] )
}
pub( super ) fn compile_program_with_attribs( gl: &glow::Context, vert_src: &str, frag_src: &str, attribs: &[ ( u32, &str ) ] ) -> glow::Program
{
unsafe
{
let program = gl.create_program().unwrap();
@@ -215,7 +176,10 @@ pub( super ) fn compile_program( gl: &glow::Context, vert_src: &str, frag_src: &
gl.attach_shader( program, vs );
gl.attach_shader( program, fs );
gl.bind_attrib_location( program, 0, "a_pos" );
for ( loc, name ) in attribs
{
gl.bind_attrib_location( program, *loc, name );
}
gl.link_program( program );
assert!( gl.get_program_link_status( program ), "Link: {}", gl.get_program_info_log( program ) );

View File

@@ -29,7 +29,7 @@
//! * `primitives` — `GlesCanvas::{fill_rect, fill_linear_gradient_rect,
//! fill_radial_gradient_rect, fill_shadow_outer, fill_shadow_inset,
//! stroke_rect, draw_line}`.
//! * `text` — `GlesCanvas::{draw_text, measure_text, draw_glyph_texture}`.
//! * `text` — `GlesCanvas::{draw_text, measure_text}`.
//! * `image` — `GlesCanvas::draw_image_data`.
//! * `shaders` — GLSL ES 1.00 shader sources (const strings).
//! * `helpers` — free functions: `ortho_rect`, `compile_program`,
@@ -101,15 +101,25 @@ pub struct BorrowedGlesTexture
pub y_inverted: bool,
}
/// Cached glyph: pre-rasterized bitmap uploaded as a GL texture.
/// Cached glyph: rasterized bitmap placed in the shared atlas.
pub ( super ) struct GlyphEntry
{
pub ( super ) texture: glow::Texture,
pub ( super ) metrics: fontdue::Metrics,
pub ( super ) tex_w: i32,
pub ( super ) tex_h: i32,
pub ( super ) atlas_x: u32,
pub ( super ) atlas_y: u32,
}
/// Cache key for the GLES glyph atlas — the GPU-side mirror of the
/// software canvas's `GlyphKey`. `glyph_id` is the per-font index
/// returned by HarfBuzz shaping; `size_bits` is `f32::to_bits` of
/// `size * dpi_scale`; `font_id` is the address of the
/// `Arc<fontdue::Font>` used for rasterisation.
pub( super ) type GlyphAtlasKey = ( u16, u32, usize );
pub( super ) const ATLAS_SIZE: u32 = 2048;
// ─── GlesCanvas ──────────────────────────────────────────────────────────────
/// GPU-accelerated canvas using EGL + GLES2/3.
@@ -127,6 +137,13 @@ pub struct GlesCanvas
/// Kept as a fallback for callers that do not route through the
/// theme registry.
pub font: Arc<Font>,
/// Raw bytes of the default font. Required by rustybuzz for
/// HarfBuzz shaping (see [`crate::text_shaping`]). Kept on the
/// canvas so the shape pipeline has direct access without a
/// global lookup.
pub font_bytes: Arc<Vec<u8>>,
/// TTC sub-face index for the default font (0 for non-`.ttc` files).
pub font_face: u32,
/// Optional theme font registry. Populated by the runtime after
/// theme load; until then it is `None` and [`Self::font_for`]
/// falls back to [`Self::font`].
@@ -167,10 +184,19 @@ pub struct GlesCanvas
u_tex_sampler: glow::UniformLocation,
// Uniform locations for glyph shader
u_glyph_mvp: glow::UniformLocation,
u_glyph_color: glow::UniformLocation,
u_glyph_opacity: glow::UniformLocation,
u_glyph_sampler: glow::UniformLocation,
u_glyph_mvp: glow::UniformLocation,
u_glyph_color: glow::UniformLocation,
u_glyph_opacity: glow::UniformLocation,
u_glyph_sampler: glow::UniformLocation,
u_glyph_uv_offset: glow::UniformLocation,
u_glyph_uv_scale: glow::UniformLocation,
glyph_batch_program: glow::Program,
u_glyph_batch_color: glow::UniformLocation,
u_glyph_batch_opacity: glow::UniformLocation,
u_glyph_batch_sampler: glow::UniformLocation,
pub ( super ) glyph_batch_vao: glow::VertexArray,
pub ( super ) glyph_batch_vbo: glow::Buffer,
// Uniform location for blit shader
u_blit_sampler: glow::UniformLocation,
@@ -289,11 +315,23 @@ pub struct GlesCanvas
u_bd_fc_radii: glow::UniformLocation,
u_bd_fc_tint: glow::UniformLocation,
// Glyph cache: (char, size_key, font_id) → GlyphEntry. The
// `font_id` is the address of the `Arc<Font>` used for the
// rasterisation, so distinct weights / families of the same
// (char, size) keep separate atlas entries.
glyph_cache: HashMap<(char, u32, usize), GlyphEntry>,
atlas_texture: glow::Texture,
/// Upload format of `atlas_texture`. On GLES3 we use the modern
/// single-channel `GL_RED` over `GL_R8`; on GLES2 we keep the
/// legacy `GL_LUMINANCE` because `GL_R8` / `GL_RED` did not exist
/// before ES3. The fragment shader samples `.r` and is identical
/// for both — `GL_LUMINANCE` replicates the single channel into
/// `.r=.g=.b`, so `.r` carries the coverage either way.
pub ( super ) atlas_format: u32,
atlas_cursor_x: u32,
atlas_cursor_y: u32,
atlas_row_height: u32,
// (glyph_id, size_key, font_id) → GlyphEntry. `glyph_id` is the
// per-font glyph index returned by HarfBuzz shaping; the cache
// therefore persists Arabic / Devanagari / CJK shaped forms
// independently of the source codepoints.
glyph_cache: HashMap<GlyphAtlasKey, GlyphEntry>,
// Reusable texture cache for images. Keyed by
// `(width, height, content fingerprint)` rather than the source
@@ -370,10 +408,8 @@ impl Drop for GlesCanvas
self.gl.delete_framebuffer( fbo );
self.gl.delete_texture( tex );
}
for ( _, entry ) in self.glyph_cache.drain()
{
self.gl.delete_texture( entry.texture );
}
self.glyph_cache.clear();
self.gl.delete_texture( self.atlas_texture );
for ( _, ( tex, _, _ ) ) in self.image_cache.drain()
{
self.gl.delete_texture( tex );

View File

@@ -19,13 +19,13 @@ use glow::HasContext;
use crate::theme::{ FontRegistry, FontStyle };
use super::helpers::{ alloc_fbo_tex, bytemuck_cast_slice, compile_program, load_default_font_bytes };
use super::helpers::{ alloc_fbo_tex, bytemuck_cast_slice, compile_program, compile_program_with_attribs, load_default_font_bytes };
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_FRAG_SRC, GLYPH_BATCH_VERT_SRC, GLYPH_BATCH_FRAG_SRC,
LINEAR_GRADIENT_FRAG_SRC, RADIAL_GRADIENT_FRAG_SRC,
RECT_FRAG_SRC,
SHADOW_INSET_FRAG_SRC, SHADOW_INSET_OVERLAY_FRAG_SRC, SHADOW_OUTER_FRAG_SRC,
@@ -39,28 +39,37 @@ use super::{ GlesCanvas, GlesVersion };
/// bring-up. The fallback chain (Noto Sans / CJK / Devanagari / …)
/// is owned by the crate-private system-fonts module and loaded
/// lazily per codepoint, not per canvas.
static DEFAULT_FONT_GLES: OnceLock<Arc<Font>> = OnceLock::new();
static DEFAULT_FONT_GLES: OnceLock<crate::system_fonts::FontHandle> = OnceLock::new();
fn default_font_gles() -> Arc<Font>
fn default_handle_gles() -> crate::system_fonts::FontHandle
{
Arc::clone( DEFAULT_FONT_GLES.get_or_init( ||
DEFAULT_FONT_GLES.get_or_init( ||
{
let bytes = load_default_font_bytes();
let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() )
.expect( "bad font" );
Arc::new( font )
} ) )
crate::system_fonts::FontHandle
{
font: Arc::new( font ),
bytes: Arc::new( bytes ),
face: 0,
}
} ).clone()
}
impl GlesCanvas
{
pub fn new( gl: Arc<glow::Context>, version: GlesVersion, width: u32, height: u32 ) -> Self
{
let font = default_font_gles();
let font_handle = default_handle_gles();
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 );
@@ -83,6 +92,7 @@ impl GlesCanvas
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,
@@ -114,7 +124,9 @@ impl GlesCanvas
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_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(),
@@ -192,6 +204,27 @@ impl GlesCanvas
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 ( 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,
@@ -265,11 +298,46 @@ impl GlesCanvas
( 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,
global_alpha: 1.0,
@@ -300,6 +368,14 @@ impl GlesCanvas
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,
@@ -379,6 +455,11 @@ impl GlesCanvas
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(),
@@ -425,11 +506,39 @@ impl GlesCanvas
( 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,
global_alpha: self.global_alpha,
@@ -460,10 +569,18 @@ impl GlesCanvas
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_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,
@@ -539,6 +656,11 @@ impl GlesCanvas
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(),

View File

@@ -147,24 +147,60 @@ void main()
}
"##;
// Fragment shader for single-channel glyph textures with color tint. Same
// Y-flip rationale as `TEX_FRAG_SRC`. The texture is `GL_LUMINANCE`, which
// replicates the uploaded byte into `.r`, `.g`, `.b` (with `.a = 1`), so the
// coverage value is read from `.r`. We deliberately avoid `GL_ALPHA` here:
// some Mesa GLES3 paths handle the legacy alpha-only format inconsistently
// (sampled `.a` returns near-zero in glyph interiors, leaving only the
// antialias edges visible text appears as thin faded outlines instead of
// solid strokes). `LUMINANCE` is also a legacy format but its mapping to
// `.r=.g=.b=data` is well-supported across ES2/ES3 drivers.
// Fragment shader for single-channel glyph textures with color tint.
//
// Glyphs live in a shared GL_LUMINANCE atlas. `u_uv_scale` and `u_uv_offset`
// map the unit quad's (0,0)(1,1) UV range into the glyph's sub-region:
// tex_uv = vec2(v_uv.x, 1 - v_uv.y) * u_uv_scale + u_uv_offset
// The Y-flip (`1 - v_uv.y`) corrects for fontdue bitmaps being stored
// top-row-first while GL tex coords have V=0 at the bottom.
//
// The atlas is GL_R8 on ES3 and GL_LUMINANCE on ES2 (see
// `gles_render/text.rs` module doc and `GlesCanvas::atlas_format`).
// Both formats put the coverage byte in `.r` when sampled, so this
// shader is identical for both.
pub( super ) const GLYPH_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
uniform vec4 u_color;
uniform float u_opacity;
uniform vec2 u_uv_offset;
uniform vec2 u_uv_scale;
void main()
{
vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y) * u_uv_scale + u_uv_offset;
float coverage = texture2D(u_sampler, uv).r;
float a = u_color.a * coverage * u_opacity;
gl_FragColor = vec4(u_color.rgb * a, a);
}
"##;
// Batched glyph shader. `a_pos` is pre-transformed into NDC by the
// CPU side (we know the surface size) so the shader has no MVP to
// apply — that lets us upload one VBO and draw every glyph of the
// text run in a single `glDrawArrays`. `a_uv` carries the per-vertex
// atlas coordinate ready to sample.
pub( super ) const GLYPH_BATCH_VERT_SRC: &str = r#"
attribute vec2 a_pos;
attribute vec2 a_uv;
varying vec2 v_uv;
void main()
{
v_uv = a_uv;
gl_Position = vec4(a_pos, 0.0, 1.0);
}
"#;
pub( super ) const GLYPH_BATCH_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
uniform vec4 u_color;
uniform float u_opacity;
void main()
{
float coverage = texture2D(u_sampler, vec2(v_uv.x, 1.0 - v_uv.y)).r;
float coverage = texture2D(u_sampler, v_uv).r;
float a = u_color.a * coverage * u_opacity;
gl_FragColor = vec4(u_color.rgb * a, a);
}

View File

@@ -1,27 +1,29 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Text rendering for [`GlesCanvas`]: glyph-atlas cache + per-glyph
//! draw call. Each glyph is rasterised once via fontdue, uploaded as
//! a one-off `GL_LUMINANCE` texture, and stored on the canvas; the
//! per-frame hot path picks positions and issues one draw call per
//! glyph.
//! Text rendering for [`GlesCanvas`]: shelf-packed glyph atlas +
//! per-glyph draw call. The line is shaped through
//! [`crate::text_shaping::shape_line`] (Unicode BiDi + per-sub-run
//! rustybuzz / HarfBuzz) and each shaped glyph is rasterised by
//! glyph index via `fontdue::Font::rasterize_indexed`, so the cache
//! is keyed on `(glyph_id, size_bits, font_id)` and Arabic
//! connected forms / Devanagari clusters / CJK shaped glyphs are
//! cached correctly without colliding with the source codepoints.
//!
//! `GL_LUMINANCE` is deliberate — `GL_ALPHA` has inconsistent
//! handling across Mesa GLES3 drivers (sampled `.a` returns near-zero
//! in glyph interiors, leaving only antialias edges visible), and
//! luminance's `.r = .g = .b = data` mapping is well-supported
//! across ES2/ES3.
//! The atlas format is picked per ES profile
//! ([`GlesCanvas::atlas_format`]): `GL_R8` / `GL_RED` on ES3,
//! `GL_LUMINANCE` on ES2. The fragment shader samples `.r` and is
//! identical for both — `GL_RED` puts the coverage byte in `.r`
//! directly, `GL_LUMINANCE` replicates it into `.r=.g=.b`.
use std::sync::Arc;
use fontdue::Font;
use glow::HasContext;
use crate::types::{ Color, Rect };
use crate::types::Color;
use super::helpers::{ ortho_rect, upload_alpha_texture };
use super::{ GlesCanvas, GlyphEntry };
use super::{ ATLAS_SIZE, GlesCanvas, GlyphEntry };
const GLYPH_CACHE_SOFT_CAP: usize = 8192;
@@ -37,9 +39,6 @@ impl GlesCanvas
self.draw_text_inner( text, x, y, size, color, None );
}
/// Draw `text` using `font` instead of the canvas default + lazy
/// system-font fallback chain. Glyphs from this font live under
/// their own atlas keys.
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
{
self.draw_text_inner( text, x, y, size, color, Some( font ) );
@@ -48,129 +47,330 @@ impl GlesCanvas
fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc<Font>> )
{
self.activate_target();
let scaled = size * self.dpi_scale;
let mut cursor_x = x;
let scaled = size * self.dpi_scale;
let size_key = ( scaled * 10.0 ) as u32;
for ch in text.chars()
unsafe
{
let size_key = (scaled * 10.0) as u32;
let ( id, font_arc ): ( usize, Arc<Font> ) = match font
{
Some( f ) =>
{
if f.lookup_glyph_index( ch ) != 0
{
( font_id( f ), Arc::clone( f ) )
}
else
{
self.font_id_for_char( ch )
}
}
None => self.font_id_for_char( ch ),
};
let key = ( ch, size_key, id );
self.gl.active_texture( glow::TEXTURE0 );
self.gl.bind_texture( glow::TEXTURE_2D, Some( self.atlas_texture ) );
}
if !self.glyph_cache.contains_key( &key )
// Resolve every glyph + its font up front so we can decide
// whether the atlas needs a single reset before we start
// emitting quads. Doing the reset mid-draw would orphan the
// `(atlas_x, atlas_y)` recorded in `glyph_cache` for glyphs
// already pushed earlier in this same call.
let canvas_handle = self.font_handle();
let prefer_handle = font.cloned().map( |f|
{
if Arc::ptr_eq( &f, &canvas_handle.font )
{
if self.glyph_cache.len() >= GLYPH_CACHE_SOFT_CAP
canvas_handle.clone()
}
else
{
// Caller-supplied face without bytes — leave bytes
// empty so the resolve function below knows it
// cannot be shaped through HarfBuzz and must fall
// back to the system chain.
crate::system_fonts::FontHandle
{
self.evict_glyph_cache_half();
}
let ( metrics, bitmap ) = font_arc.rasterize( ch, scaled );
if metrics.width > 0 && metrics.height > 0
{
let tex = upload_alpha_texture( &self.gl, &bitmap, metrics.width as i32, metrics.height as i32 );
self.glyph_cache.insert( key, GlyphEntry
{
texture: tex,
metrics,
tex_w: metrics.width as i32,
tex_h: metrics.height as i32,
} );
} else {
cursor_x += metrics.advance_width;
continue;
font: f,
bytes: Arc::new( Vec::new() ),
face: 0,
}
}
if let Some( entry ) = self.glyph_cache.get( &key )
} ).unwrap_or_else( || canvas_handle.clone() );
let resolve = |ch: char| -> Option<crate::system_fonts::FontHandle>
{
if prefer_handle.font.lookup_glyph_index( ch ) != 0 && !prefer_handle.bytes.is_empty()
{
let gx = ( cursor_x + entry.metrics.xmin as f32 ).round();
let gy = ( y - entry.metrics.height as f32 - entry.metrics.ymin as f32 + 1.0 ).round();
let dest = Rect
return Some( prefer_handle.clone() );
}
crate::system_fonts::lookup_handle( ch ).or_else( ||
if !canvas_handle.bytes.is_empty() { Some( canvas_handle.clone() ) } else { None }
)
};
let shaped = crate::text_shaping::shape_line( text, scaled, resolve );
if shaped.is_empty() { return; }
// Resolve each glyph's `font_id` to an actual `Arc<Font>` for
// rasterization. The `shape_line` step kept the id as the
// `Arc<Font>::as_ptr` of the resolved handle, so we walk
// every codepoint and ask the same resolver — exactly once
// per distinct id thanks to the dedup loop.
let mut fonts: Vec<( usize, Arc<Font> )> = Vec::new();
if !canvas_handle.bytes.is_empty()
{
fonts.push( ( font_id( &canvas_handle.font ), Arc::clone( &canvas_handle.font ) ) );
}
for g in &shaped
{
if fonts.iter().any( |( id, _ )| *id == g.font_id ) { continue; }
let mut found = None;
for ch in text.chars()
{
if let Some( h ) = crate::system_fonts::lookup_handle( ch )
{
x: gx,
y: gy,
width: entry.tex_w as f32,
height: entry.tex_h as f32,
};
self.draw_glyph_texture( entry.texture, dest, color, self.global_alpha );
cursor_x += entry.metrics.advance_width;
if font_id( &h.font ) == g.font_id { found = Some( h.font ); break; }
}
}
if let Some( f ) = found
{
fonts.push( ( g.font_id, f ) );
}
}
// Phase 1 — rasterise every missing glyph into the atlas.
// Permitted to reset the atlas (and re-rasterise everything we
// already inserted in this same pass) at most once: a single
// string longer than the atlas capacity falls back to skipping
// the tail rather than oscillating.
let mut reset_used = false;
let mut i = 0;
while i < shaped.len()
{
let g = &shaped[ i ];
let glyph_id = g.glyph_id as u16;
let key = ( glyph_id, size_key, g.font_id );
if self.glyph_cache.contains_key( &key )
{
i += 1;
continue;
}
let Some( ( _, font_arc ) ) = fonts.iter().find( |( id, _ )| *id == g.font_id ) else
{
i += 1;
continue;
};
let ( metrics, bitmap ) = font_arc.rasterize_indexed( glyph_id, scaled );
if metrics.width == 0 || metrics.height == 0
{
self.glyph_cache.insert( key, GlyphEntry
{
metrics,
tex_w: 0,
tex_h: 0,
atlas_x: 0,
atlas_y: 0,
} );
i += 1;
continue;
}
let ( w, h ) = ( metrics.width as u32, metrics.height as u32 );
if self.glyph_cache.len() >= GLYPH_CACHE_SOFT_CAP && !reset_used
{
self.atlas_reset();
reset_used = true;
i = 0;
continue;
}
let pos = match self.atlas_alloc( w, h )
{
Some( p ) => p,
None if !reset_used =>
{
self.atlas_reset();
reset_used = true;
i = 0;
continue;
}
None =>
{
self.glyph_cache.insert( key, GlyphEntry
{
metrics,
tex_w: 0,
tex_h: 0,
atlas_x: 0,
atlas_y: 0,
} );
i += 1;
continue;
}
};
unsafe
{
self.gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 1 );
self.gl.tex_sub_image_2d(
glow::TEXTURE_2D, 0,
pos.0 as i32, pos.1 as i32,
w as i32, h as i32,
self.atlas_format, glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice( Some( &bitmap ) ),
);
self.gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 4 );
}
self.glyph_cache.insert( key, GlyphEntry
{
metrics,
tex_w: w as i32,
tex_h: h as i32,
atlas_x: pos.0,
atlas_y: pos.1,
} );
i += 1;
}
// Phase 2 — collect every quad's vertices in one buffer and
// flush them with a single draw call. Saves N-1
// program/uniform/draw round-trips per text run.
let atlas_size = ATLAS_SIZE as f32;
let surface_w = self.width as f32;
let surface_h = self.height as f32;
let mut verts: Vec<f32> = Vec::with_capacity( shaped.len() * 24 );
let mut cx = x;
for g in &shaped
{
let glyph_id = g.glyph_id as u16;
let key = ( glyph_id, size_key, g.font_id );
let Some( entry ) = self.glyph_cache.get( &key ) else
{
cx += g.x_advance;
continue;
};
if entry.tex_w == 0 || entry.tex_h == 0
{
cx += g.x_advance;
continue;
}
let pen_x = cx + g.x_offset;
let pen_y = y - g.y_offset;
let gx = ( pen_x + entry.metrics.xmin as f32 ).round();
let gy = ( pen_y - entry.metrics.height as f32 - entry.metrics.ymin as f32 + 1.0 ).round();
let gw = entry.tex_w as f32;
let gh = entry.tex_h as f32;
let x0_ndc = gx * 2.0 / surface_w - 1.0;
let x1_ndc = ( gx + gw ) * 2.0 / surface_w - 1.0;
let y0_ndc = 1.0 - gy * 2.0 / surface_h;
let y1_ndc = 1.0 - ( gy + gh ) * 2.0 / surface_h;
let u0 = entry.atlas_x as f32 / atlas_size;
let v0 = entry.atlas_y as f32 / atlas_size;
let u1 = ( entry.atlas_x as f32 + gw ) / atlas_size;
let v1 = ( entry.atlas_y as f32 + gh ) / atlas_size;
verts.extend_from_slice( &[
x0_ndc, y0_ndc, u0, v0,
x1_ndc, y0_ndc, u1, v0,
x0_ndc, y1_ndc, u0, v1,
x1_ndc, y0_ndc, u1, v0,
x1_ndc, y1_ndc, u1, v1,
x0_ndc, y1_ndc, u0, v1,
] );
cx += g.x_advance;
}
if !verts.is_empty()
{
unsafe
{
self.gl.use_program( Some( self.glyph_batch_program ) );
self.gl.uniform_4_f32( Some( &self.u_glyph_batch_color ), color.r, color.g, color.b, color.a );
self.gl.uniform_1_f32( Some( &self.u_glyph_batch_opacity ), self.global_alpha );
self.gl.uniform_1_i32( Some( &self.u_glyph_batch_sampler ), 0 );
self.gl.bind_vertex_array( Some( self.glyph_batch_vao ) );
self.gl.bind_buffer( glow::ARRAY_BUFFER, Some( self.glyph_batch_vbo ) );
self.gl.buffer_data_u8_slice( glow::ARRAY_BUFFER, super::helpers::bytemuck_cast_slice( &verts ), glow::STREAM_DRAW );
let vertex_count = ( verts.len() / 4 ) as i32;
self.gl.draw_arrays( glow::TRIANGLES, 0, vertex_count );
self.gl.bind_vertex_array( None );
}
}
unsafe { self.gl.bind_texture( glow::TEXTURE_2D, None ); }
}
pub fn measure_text( &self, text: &str, size: f32 ) -> f32
{
text.chars().map( |ch|
{
self.font_for_char( ch ).metrics( ch, size * self.dpi_scale ).advance_width
} ).sum()
self.measure_inner( text, size, None )
}
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
{
text.chars().map( |ch|
self.measure_inner( text, size, Some( font ) )
}
fn measure_inner( &self, text: &str, size: f32, font: Option<&Arc<Font>> ) -> f32
{
let scaled = size * self.dpi_scale;
let canvas_handle = self.font_handle();
let prefer_handle = font.cloned().map( |f|
{
let f = if font.lookup_glyph_index( ch ) != 0
if Arc::ptr_eq( &f, &canvas_handle.font )
{
Arc::clone( font )
canvas_handle.clone()
}
else
{
self.font_for_char( ch )
};
f.metrics( ch, size * self.dpi_scale ).advance_width
} ).sum()
}
fn font_id_for_char( &self, ch: char ) -> ( usize, Arc<Font> )
{
let font = self.font_for_char( ch );
let id = Arc::as_ptr( &font ) as usize;
( id, font )
}
fn evict_glyph_cache_half( &mut self )
{
let drop_n = self.glyph_cache.len() / 2;
let victims: Vec<_> = self.glyph_cache.keys().copied().take( drop_n ).collect();
for key in victims
{
if let Some( entry ) = self.glyph_cache.remove( &key )
{
unsafe { self.gl.delete_texture( entry.texture ); }
crate::system_fonts::FontHandle
{
font: f,
bytes: Arc::new( Vec::new() ),
face: 0,
}
}
} ).unwrap_or_else( || canvas_handle.clone() );
let resolve = |ch: char| -> Option<crate::system_fonts::FontHandle>
{
if prefer_handle.font.lookup_glyph_index( ch ) != 0 && !prefer_handle.bytes.is_empty()
{
return Some( prefer_handle.clone() );
}
crate::system_fonts::lookup_handle( ch ).or_else( ||
if !canvas_handle.bytes.is_empty() { Some( canvas_handle.clone() ) } else { None }
)
};
let shaped = crate::text_shaping::shape_line( text, scaled, resolve );
if shaped.is_empty()
{
return text.chars().map( |ch|
{
let f = font.cloned().unwrap_or_else( || self.font_for_char( ch ) );
f.metrics( ch, scaled ).advance_width
} ).sum();
}
shaped.iter().map( |g| g.x_advance ).sum()
}
fn font_handle( &self ) -> crate::system_fonts::FontHandle
{
crate::system_fonts::FontHandle
{
font: Arc::clone( &self.font ),
bytes: Arc::clone( &self.font_bytes ),
face: self.font_face,
}
}
fn draw_glyph_texture( &self, texture: glow::Texture, dest: Rect, color: Color, opacity: f32 )
fn atlas_alloc( &mut self, w: u32, h: u32 ) -> Option<( u32, u32 )>
{
let mvp = ortho_rect( self.width, self.height, dest );
unsafe
const PAD: u32 = 1;
if w + PAD > ATLAS_SIZE || h + PAD > ATLAS_SIZE { return None; }
if self.atlas_cursor_x + w + PAD > ATLAS_SIZE
{
self.gl.use_program( Some( self.glyph_program ) );
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_glyph_mvp ), false, &mvp );
self.gl.uniform_4_f32( Some( &self.u_glyph_color ), color.r, color.g, color.b, color.a );
self.gl.uniform_1_f32( Some( &self.u_glyph_opacity ), opacity );
self.gl.active_texture( glow::TEXTURE0 );
self.gl.bind_texture( glow::TEXTURE_2D, Some( texture ) );
self.gl.uniform_1_i32( Some( &self.u_glyph_sampler ), 0 );
self.gl.bind_vertex_array( Some( self.quad_vao ) );
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
self.gl.bind_vertex_array( None );
self.gl.bind_texture( glow::TEXTURE_2D, None );
self.atlas_cursor_x = 0;
self.atlas_cursor_y += self.atlas_row_height;
self.atlas_row_height = 0;
}
if self.atlas_cursor_y + h + PAD > ATLAS_SIZE { return None; }
let x = self.atlas_cursor_x;
let y = self.atlas_cursor_y;
self.atlas_cursor_x += w + PAD;
if h + PAD > self.atlas_row_height { self.atlas_row_height = h + PAD; }
Some( ( x, y ) )
}
fn atlas_reset( &mut self )
{
self.glyph_cache.clear();
self.atlas_cursor_x = 0;
self.atlas_cursor_y = 0;
self.atlas_row_height = 0;
}
}