mod.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. //! GPU text rendering pipeline.
  19. //!
  20. //! Text is drawn as textured quads on the GPU; no platform text API is
  21. //! involved. The pipeline has three layers:
  22. //!
  23. //! * Layout (`make_layout`): parley shapes a string into glyph runs
  24. //! using the font stack (IBM Plex Mono, Noto Color Emoji, DarkIRC
  25. //! Emoji) registered in `GLOBAL_FONT_CTX`. Coordinates are physical
  26. //! pixels (window scale is baked in by parley); `TextLayout` remembers
  27. //! the scale, and consumers divide by it exactly once to get the
  28. //! virtual units the renderer expects. Rasterization still happens at
  29. //! physical resolution so text stays crisp while the renderer
  30. //! re-applies the scale via `SetScale`.
  31. //!
  32. //! * Rendering (`render`): two passes over a layout. First every glyph
  33. //! of every run is rasterized with swash and packed into an `Atlas`:
  34. //! one RGBA8 texture per call, color glyphs stored as RGBA and mask
  35. //! glyphs as alpha. Then each run becomes a `DrawMesh` of quads whose
  36. //! UVs reference that texture, so a whole run draws in one call. This
  37. //! atlas is transient (one per layout): the right trade-off for
  38. //! arbitrary dynamic text such as chat messages and labels, where
  39. //! layouts are short-lived and each draws from its own texture.
  40. //!
  41. //! * Batching for fixed sets (`string_atlas`): the opposite trade-off.
  42. //! `make_string_atlas` lays out many fixed strings up front and packs
  43. //! all their glyphs into a single shared atlas: one texture and one
  44. //! raster per glyph for the entire set, returning per-string quad
  45. //! geometry to the caller. Used by the emoji picker, where rendering
  46. //! each icon through the dynamic path allocated one texture plus a
  47. //! vertex/index buffer pair per emoji (~574 icons, ~2s of generation)
  48. //! for glyphs that only ever needed to reference a shared sheet.
  49. //!
  50. //! The packer itself lives in `atlas`: sprites are separated by a 2px
  51. //! gap on all sides to prevent UV bleed, and can optionally wrap into
  52. //! rows capped at `MAX_TEXTURE_DIMENSION` so large fixed sets stay
  53. //! within GPU texture size limits (GLES3/WebGL2 only guarantee 2048),
  54. //! failing loudly at build time instead of corrupting at draw time.
  55. use parley::fontique::{Collection, CollectionOptions, SourceCache, SourceCacheOptions};
  56. use std::{
  57. cell::RefCell,
  58. ops::Range,
  59. sync::{Arc, LazyLock},
  60. };
  61. use crate::mesh::Color;
  62. pub mod atlas;
  63. mod editor;
  64. pub use editor::Editor;
  65. mod render;
  66. #[cfg(not(target_os = "android"))]
  67. pub use render::render_raw_layout;
  68. pub use render::{
  69. render_backgrounds, render_layout, render_layout_shadowed, render_layout_with_opts,
  70. DebugRenderOptions, Shadow,
  71. };
  72. mod string_atlas;
  73. pub use string_atlas::make_string_atlas;
  74. pub static GLOBAL_FONT_CTX: LazyLock<parley::FontContext> = LazyLock::new(|| {
  75. let mut font_ctx = parley::FontContext {
  76. collection: Collection::new(CollectionOptions { shared: true, system_fonts: true }),
  77. source_cache: SourceCache::new(SourceCacheOptions { shared: true }),
  78. };
  79. let font_data = include_bytes!("../../data/font/ibm-plex-mono-regular.otf") as &[u8];
  80. font_ctx.collection.register_fonts(peniko::Blob::new(Arc::new(font_data)), None);
  81. let font_data = include_bytes!("../../data/font/NotoColorEmoji.ttf") as &[u8];
  82. font_ctx.collection.register_fonts(peniko::Blob::new(Arc::new(font_data)), None);
  83. let font_data = include_bytes!("../../data/font/darkfi-custom-emoji.ttf") as &[u8];
  84. font_ctx.collection.register_fonts(peniko::Blob::new(Arc::new(font_data)), None);
  85. font_ctx
  86. });
  87. thread_local! {
  88. pub static THREAD_LAYOUT_CTX: RefCell<parley::LayoutContext<Color>> =
  89. RefCell::new(parley::LayoutContext::new());
  90. }
  91. const FONT_STACK: &[parley::FontFamilyName<'_>] = &[
  92. parley::FontFamilyName::named("IBM Plex Mono"),
  93. parley::FontFamilyName::named("Noto Color Emoji"),
  94. parley::FontFamilyName::named("DarkIRC Emoji"),
  95. ];
  96. /// A parley layout paired with the window scale it was built with.
  97. ///
  98. /// Parley bakes the builder scale into every coordinate (font size,
  99. /// advances, glyph positions), so the underlying layout is in physical
  100. /// pixels. The renderer applies the window scale again via `SetScale`,
  101. /// so geometry consumed in virtual units must be divided by the scale
  102. /// exactly once. The accessors below do that division. Rendering code
  103. /// in `render.rs` divides when emitting meshes. Glyph rasterization
  104. /// still happens at physical resolution so text stays crisp.
  105. #[derive(Clone)]
  106. pub struct TextLayout {
  107. layout: parley::Layout<Color>,
  108. /// Scale parley baked into `layout`. Divide physical coords by this
  109. /// to get virtual units.
  110. scale: f32,
  111. }
  112. impl std::ops::Deref for TextLayout {
  113. type Target = parley::Layout<Color>;
  114. fn deref(&self) -> &Self::Target {
  115. &self.layout
  116. }
  117. }
  118. impl std::ops::DerefMut for TextLayout {
  119. fn deref_mut(&mut self) -> &mut Self::Target {
  120. &mut self.layout
  121. }
  122. }
  123. impl Default for TextLayout {
  124. fn default() -> Self {
  125. Self { layout: parley::Layout::default(), scale: 1. }
  126. }
  127. }
  128. impl TextLayout {
  129. pub fn scale(&self) -> f32 {
  130. self.scale
  131. }
  132. /// Height in virtual units
  133. pub fn height(&self) -> f32 {
  134. self.layout.height() / self.scale
  135. }
  136. /// Width in virtual units
  137. pub fn width(&self) -> f32 {
  138. self.layout.width() / self.scale
  139. }
  140. }
  141. pub fn make_layout(
  142. text: &str,
  143. text_color: Color,
  144. font_size: f32,
  145. lineheight: f32,
  146. window_scale: f32,
  147. width: Option<f32>,
  148. underlines: &[Range<usize>],
  149. ) -> TextLayout {
  150. make_layout2(
  151. text,
  152. text_color,
  153. font_size,
  154. lineheight,
  155. window_scale,
  156. width,
  157. underlines,
  158. &[],
  159. parley::Alignment::Start,
  160. parley::OverflowWrap::Normal,
  161. )
  162. }
  163. pub fn make_layout2(
  164. text: &str,
  165. text_color: Color,
  166. font_size: f32,
  167. lineheight: f32,
  168. window_scale: f32,
  169. width: Option<f32>,
  170. underlines: &[Range<usize>],
  171. foreground_colors: &[(Range<usize>, Color)],
  172. text_align: parley::Alignment,
  173. overflow_wrap: parley::OverflowWrap,
  174. ) -> TextLayout {
  175. make_layout_with_indent(
  176. text,
  177. text_color,
  178. font_size,
  179. lineheight,
  180. window_scale,
  181. width,
  182. underlines,
  183. foreground_colors,
  184. text_align,
  185. overflow_wrap,
  186. 0.,
  187. )
  188. }
  189. /// Reserve space before the first line without changing text or style ranges.
  190. pub fn make_layout_with_indent(
  191. text: &str,
  192. text_color: Color,
  193. font_size: f32,
  194. lineheight: f32,
  195. window_scale: f32,
  196. width: Option<f32>,
  197. underlines: &[Range<usize>],
  198. foreground_colors: &[(Range<usize>, Color)],
  199. text_align: parley::Alignment,
  200. overflow_wrap: parley::OverflowWrap,
  201. first_line_indent: f32,
  202. ) -> TextLayout {
  203. THREAD_LAYOUT_CTX.with(|layout_ctx| {
  204. let mut layout_ctx = layout_ctx.borrow_mut();
  205. let mut font_ctx = GLOBAL_FONT_CTX.clone();
  206. let mut builder = layout_ctx.ranged_builder(&mut font_ctx, text, window_scale, false);
  207. builder.push_default(parley::LineHeight::FontSizeRelative(lineheight));
  208. builder.push_default(parley::StyleProperty::FontSize(font_size));
  209. builder.push_default(parley::StyleProperty::from(FONT_STACK));
  210. builder.push_default(parley::StyleProperty::Brush(text_color));
  211. builder.push_default(parley::StyleProperty::OverflowWrap(overflow_wrap));
  212. for underline in underlines {
  213. builder.push(parley::StyleProperty::Underline(true), underline.clone());
  214. }
  215. for (range, color) in foreground_colors {
  216. builder.push(parley::StyleProperty::Brush(*color), range.clone());
  217. }
  218. let mut layout: parley::Layout<Color> = builder.build(text);
  219. // The wrap width is given in virtual units while the layout
  220. // coordinates are physical, so scale it up before breaking.
  221. let width = width.map(|w| w * window_scale);
  222. if first_line_indent > 0. {
  223. let width = width.unwrap_or(f32::MAX);
  224. let indent = first_line_indent * window_scale;
  225. let mut breaker = layout.break_lines();
  226. breaker.state_mut().set_layout_max_advance(width);
  227. breaker.state_mut().set_line_max_advance((width - indent).max(0.));
  228. breaker.state_mut().set_line_x(indent);
  229. breaker.break_next();
  230. breaker.state_mut().set_line_x(0.);
  231. breaker.break_remaining(width);
  232. } else {
  233. layout.break_all_lines(width);
  234. }
  235. layout.align(text_align, parley::AlignmentOptions::default());
  236. TextLayout { layout, scale: window_scale }
  237. })
  238. }
  239. #[cfg(test)]
  240. mod tests {
  241. use super::*;
  242. #[test]
  243. fn first_line_indent_uses_full_width_on_continuations() {
  244. for scale in [1., 1.5, 2.] {
  245. let text = "word ".repeat(30);
  246. let layout = make_layout_with_indent(
  247. &text,
  248. [1.; 4],
  249. 14.,
  250. 20. / 14.,
  251. scale,
  252. Some(200.),
  253. &[],
  254. &[],
  255. parley::Alignment::Start,
  256. parley::OverflowWrap::Normal,
  257. 50.,
  258. );
  259. assert!(layout.lines().count() > 2);
  260. for (i, line) in layout.lines().enumerate() {
  261. let run = line
  262. .items()
  263. .find_map(|item| match item {
  264. parley::PositionedLayoutItem::GlyphRun(run) => Some(run),
  265. _ => None,
  266. })
  267. .unwrap();
  268. let expected = if i == 0 { 50. } else { 0. };
  269. assert!((run.offset() / scale - expected).abs() < 0.01);
  270. }
  271. let old_layout = make_layout(&text, [1.; 4], 14., 20. / 14., scale, Some(150.), &[]);
  272. assert!(layout.height() < old_layout.height());
  273. for line in old_layout.lines() {
  274. assert_eq!(line.metrics().offset, 0.);
  275. }
  276. }
  277. }
  278. #[test]
  279. fn first_line_indent_preserves_empty_and_overflow_metrics() {
  280. for text in ["", "\n", "first\nsecond", "averylongunbrokenword"] {
  281. for width in [0., 30., 80., 200.] {
  282. let layout = make_layout_with_indent(
  283. text,
  284. [1.; 4],
  285. 14.,
  286. 20. / 14.,
  287. 1.,
  288. Some(width),
  289. &[],
  290. &[],
  291. parley::Alignment::Start,
  292. parley::OverflowWrap::Normal,
  293. 50.,
  294. );
  295. let expected_lines = if text.contains('\n') { 2 } else { 1 };
  296. assert_eq!(layout.lines().count(), expected_lines, "{text:?}, width={width}");
  297. assert!((layout.height() - 20. * expected_lines as f32).abs() < 0.01);
  298. for (i, line) in layout.lines().enumerate() {
  299. assert_eq!(line.metrics().offset, if i == 0 { 50. } else { 0. });
  300. }
  301. }
  302. }
  303. }
  304. }