/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use crate::{ gfx::{DebugTag, DrawInstruction, DrawMesh, Point, Rectangle, RectangleUnion, Renderer}, mesh::{Color, MeshBuilder, COLOR_WHITE}, }; use super::{ atlas::{Atlas, RenderedAtlas, RunIdx}, TextLayout, }; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct DebugRenderOptions(u32); impl DebugRenderOptions { pub const OFF: DebugRenderOptions = DebugRenderOptions(0b00); pub const GLYPH: DebugRenderOptions = DebugRenderOptions(0b01); pub const BASELINE: DebugRenderOptions = DebugRenderOptions(0b10); pub fn has(self, other: Self) -> bool { (self.0 & other.0) == other.0 } } impl std::ops::BitOr for DebugRenderOptions { type Output = Self; fn bitor(self, rhs: Self) -> Self { Self(self.0 | rhs.0) } } impl std::ops::BitOrAssign for DebugRenderOptions { fn bitor_assign(&mut self, rhs: Self) { self.0 |= rhs.0; } } pub fn render_layout( layout: &TextLayout, renderer: &Renderer, tag: DebugTag, ) -> Vec { render_layout_with_opts(layout, DebugRenderOptions::OFF, renderer, tag) } /// Render a raw parley layout that was built with the given scale. Used /// by editors that own their layout internally (e.g. `PlainEditor`). #[cfg(not(target_os = "android"))] pub fn render_raw_layout( layout: &parley::Layout, scale: f32, renderer: &Renderer, tag: DebugTag, ) -> Vec { render_raw_layout_impl(layout, scale, DebugRenderOptions::OFF, renderer, tag).0 } /// Draw a filled (and optionally outlined) background box behind every glyph run /// whose style brush equals `match_brush`. The box tracks the run's horizontal /// advance and the font-metric ascent/descent vertically, so a run that wraps /// across lines gets one box per wrapped line. /// /// Matching by brush (rather than by byte range) is required because a parley /// `GlyphRun` does not expose its own byte range — only its parent font run does, /// and a font run is coarser than the per-color segment (e.g. the nick, body, and /// URL of one line all share a font run). Matching `style().brush` pins the box to /// exactly the color segment, so only the intended runs (here: the URL runs) are /// highlighted. The fill is skipped when `bg_color` alpha is ~0; the outline is /// skipped when `border_size` is ~0 or `border_color` alpha is ~0. pub fn render_backgrounds( layout: &TextLayout, match_brush: Color, bg_color: Color, border_color: Color, border_size: f32, renderer: &Renderer, tag: DebugTag, ) -> Vec { let mut instrs = vec![]; let has_fill = bg_color[3] > 0.; let has_border = border_size > 0. && border_color[3] > 0.; if !has_fill && !has_border { return instrs } let scale = layout.scale(); for line in layout.lines() { for item in line.items() { let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; if glyph_run.style().brush != match_brush { continue } let metrics = glyph_run.run().metrics(); let x = glyph_run.offset(); let y = glyph_run.baseline() - metrics.ascent; let w = glyph_run.advance(); let h = metrics.ascent + metrics.descent; let rect = Rectangle::new(x, y, w, h) / scale; let mut mesh = MeshBuilder::new(tag); if has_fill { mesh.draw_filled_box(&rect, bg_color); } if has_border { mesh.draw_outline(&rect, border_color, border_size); } instrs.push(DrawInstruction::Draw(mesh.alloc(renderer).draw_untextured())); } } instrs } pub fn render_layout_with_opts( layout: &TextLayout, opts: DebugRenderOptions, renderer: &Renderer, tag: DebugTag, ) -> Vec { render_raw_layout_impl(layout, layout.scale(), opts, renderer, tag).0 } /// Layout coordinates are physical (scale is baked in by parley) while /// meshes are consumed in virtual units and scaled up again by the /// renderer's `SetScale`. So every emitted coordinate is divided by /// `scale` here. Glyphs are still rasterized at physical resolution so /// the final on-screen texel mapping stays crisp. fn render_raw_layout_impl( layout: &parley::Layout, scale: f32, opts: DebugRenderOptions, renderer: &Renderer, tag: DebugTag, ) -> (Vec, Rectangle) { // First pass to create atlas let mut scale_ctx = swash::scale::ScaleContext::new(); let mut atlas = Atlas::new(renderer, tag); let mut run_idx = 0; for line in layout.lines() { for item in line.items() { match item { parley::PositionedLayoutItem::GlyphRun(glyph_run) => { push_glyphs(&mut atlas, &glyph_run, run_idx, &mut scale_ctx); run_idx += 1; } parley::PositionedLayoutItem::InlineBox(_) => {} } } } // Render the atlas let atlas = atlas.make(); // Second pass to draw glyphs let mut run_idx = 0; let mut instrs = vec![]; let mut bounds = RectangleUnion::new(); for line in layout.lines() { for item in line.items() { match item { parley::PositionedLayoutItem::GlyphRun(glyph_run) => { let (mesh, run_bounds) = render_glyph_run(&glyph_run, run_idx, opts, &atlas, scale, renderer, tag); bounds.join(run_bounds); instrs.push(DrawInstruction::Draw(mesh)); run_idx += 1; } parley::PositionedLayoutItem::InlineBox(_) => {} } } } (instrs, bounds.get().unwrap_or(Rectangle::zero())) } pub(super) fn push_glyphs( atlas: &mut Atlas, glyph_run: &parley::GlyphRun<'_, Color>, run_idx: RunIdx, scale_ctx: &mut swash::scale::ScaleContext, ) { let run = glyph_run.run(); let font = run.font(); let font_size = run.font_size(); let normalized_coords = run.normalized_coords(); let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap(); let mut scaler = scale_ctx .builder(font_ref) .size(font_size) .hint(true) .normalized_coords(normalized_coords) .build(); for glyph in glyph_run.glyphs() { atlas.push_glyph(glyph.id as u16, run_idx, &mut scaler); } } fn render_glyph_run( glyph_run: &parley::GlyphRun<'_, Color>, run_idx: usize, opts: DebugRenderOptions, atlas: &RenderedAtlas, scale: f32, renderer: &Renderer, tag: DebugTag, ) -> (DrawMesh, RectangleUnion) { let mut run_x = glyph_run.offset(); let run_y = glyph_run.baseline(); let style = glyph_run.style(); let color = style.brush; //trace!(target: "text::render", "render_glyph_run run_idx={run_idx} baseline={run_y}"); let mut mesh = MeshBuilder::new(tag); let mut bounds = RectangleUnion::new(); if let Some(underline) = &style.underline { render_underline(underline, glyph_run, scale, &mut mesh); } for glyph in glyph_run.glyphs() { let glyph_inf = atlas.fetch_uv(glyph.id as u16, run_idx).expect("missing glyph UV rect"); let glyph_x = run_x + glyph.x; let glyph_y = run_y - glyph.y; run_x += glyph.advance; let glyph_rect = Rectangle::new( (glyph_x + glyph_inf.place.left as f32) / scale, (glyph_y - glyph_inf.place.top as f32) / scale, glyph_inf.place.width as f32 / scale, glyph_inf.place.height as f32 / scale, ); bounds.add(glyph_rect); if opts.has(DebugRenderOptions::GLYPH) { mesh.draw_outline(&glyph_rect, [0., 1., 0., 0.7], 1.); } let color = if glyph_inf.is_color { COLOR_WHITE } else { color }; mesh.draw_box(&glyph_rect, color, &glyph_inf.uv_rect); } if opts.has(DebugRenderOptions::BASELINE) { let rect = Rectangle::new(glyph_run.offset(), glyph_run.baseline(), glyph_run.advance(), 1.) / scale; mesh.draw_filled_box(&rect, [0., 0., 1., 0.7]); } (mesh.alloc(renderer).draw_with_textures(vec![atlas.texture.clone()]), bounds) } fn render_underline( underline: &parley::layout::Decoration, glyph_run: &parley::GlyphRun<'_, Color>, scale: f32, mesh: &mut MeshBuilder, ) { let color = underline.brush; let run_metrics = glyph_run.run().metrics(); let offset = match underline.offset { Some(offset) => offset, None => run_metrics.underline_offset, }; let width = match underline.size { Some(size) => size, None => run_metrics.underline_size, }; // The `offset` is the distance from the baseline to the top of the underline // so we move the line down by half the width // Remember that we are using a y-down coordinate system // If there's a custom width, because this is an underline, we want the custom // width to go down from the default expectation let y = (glyph_run.baseline() - offset + width / 2.) / scale; let start_x = glyph_run.offset() / scale; let end_x = start_x + glyph_run.advance() / scale; let start = Point::new(start_x, y); let end = Point::new(end_x, y); mesh.draw_line(start, end, color, width / scale); }