//! Width-independent word preparation and variable-width line filling. The //! exclusion algorithm owns geometry; this module only consumes text in order. //! graphics 的 Prepare 在这里生成可复用词/字素图像,Flow 每行提供避让后的区间,由 row 按文本顺序消费。 //! 字形资源不随列宽重新生成;Cursor 属于每次 Flow,放不下的内容留待后续区间或下一行。 use crate::{display_list::Color, geometry::Rect, render::image::Image, text::TextStyle}; use std::{collections::HashMap, sync::Arc}; use unicode_segmentation::UnicodeSegmentation; #[derive(Debug)] /// 一段文字的不可变墨迹资源;整词和单字素都用同一结构,graphics::flow 按 Placement 引用它生成图片命令。 pub struct Ink { // Image::text 产生的 RGBA 图片;Arc 允许重复文本和多次 Flow 共享像素,不为每个摆放位置复制。 pub image: Arc, // 图片相对文字原点的逻辑矩形,包含负方向字形留白;不能只按 advance 宽度当作图片框。 pub rect: Rect, // 字体测量的逻辑 advance,排版推进使用它;图片像素宽度还包含 scale 与留白。 pub width: f32, } #[derive(Debug)] /// Unicode 换行机会之间的文字段,不只代表英文词;同时保留整段墨迹和按字素拆分的后备墨迹。 pub struct Word { // 能完整容纳时使用整段图像,保留整段整形的字距与连字效果。 pub ink: Arc, // 按扩展字素簇生成的资源,避免拆开组合音标或 emoji;拆分模式的宽度来自逐字素独立测量。 pub letters: Vec>, // 该段末尾空白的逻辑测量宽度,放完正文后推进 x;换行符不参与这份空白测量。 pub space: f32, // 原段以换行符结尾,消费完该段就结束当前 row,即使本行还有其他可用区间。 pub newline: bool, } #[derive(Debug)] /// 与列宽无关的预排资源;graphics 的 TextResource 持有 Arc,每次 Flow 单独创建游标。 pub struct Prepared { pub words: Vec, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] /// 下一个待消费段及段内字素位置;Default 从 (0,0) 开始,跨一行的多个区间和后续行持续推进。 pub struct Cursor { pub word: usize, // 为零表示尚未拆分/位于段首,非零表示前面字素已放置,不能再回头画整词。 pub letter: usize, } /// row 追加的纯几何结果,graphics::flow 随后转为 PaintCommand;不在排版循环内分配托管命令对象。 pub struct Placement { pub word: usize, // None 指整词墨迹,Some 指该词某一字素;不能把整词位置再当成已经拆分的单字素。 pub letter: Option, // 本次 Flow 坐标空间中的文字逻辑原点;实际图片命令还会叠加 Ink.rect 的留白偏移。 pub x: f32, pub y: f32, } impl Prepared { /// graphics 的 Prepare 分支在校验文本长度、样式和设备比例后调用;这里再限制本份预排图片累计像素。 pub fn new( value: &str, style: TextStyle, scale: f32, color: Color, ) -> Result { // 缓存只在这一份 Prepared 构造期间存在,按文本复用整词/字素资源;同文样式固定,无需额外样式键。 let mut cache: HashMap> = HashMap::new(); // 累计唯一墨迹图片面积,u64 乘法避免在统计前溢出;重复文本命中不重复计费。 let mut pixels = 0_u64; // 整段和字素走同一图片构造闭包,共享资源检查与去重,不另外维护两套缓存。 let mut ink = |text: &str| -> Result, &'static str> { // 重复内容直接克隆 Arc;文本相同的段与字素也能共享同一张图。 if let Some(ink) = cache.get(text) { return Ok(ink.clone()); } // 先生成图像、逻辑留白框及 advance,再核算累计像素;单图自身预算由 Image::text 检查。 let (image, rect, width) = Image::text(text, style, scale, color)?; pixels += u64::from(image.width) * u64::from(image.height); // 超过本份预排的八百万级像素上限即返回错误,不发布部分 Prepared;本次生成图片也计入。 if pixels > 8_388_608 { return Err("prepared text exceeds 8M pixel cache budget"); } // 图片只封装一次,缓存和 Word 分别通过 Arc 保留;闭包的临时 HashMap 退出后资源仍由结果持有。 let ink = Arc::new(Ink { image, rect, width }); cache.insert(text.to_owned(), ink.clone()); Ok(ink) }; let mut words = Vec::new(); // 保留段末换行符,使换行意图不会因分段丢失;空输入不产生任何 Word。 for para in value.split_inclusive('\n') { // Unicode 换行迭代器给出字节结束位置,start 跟随推进以逐段切取 UTF-8 安全文本。 let mut start = 0; // 使用每个换行机会作为可整段放置的单元;机会类型本身未参与判断,显式换行另由 newline 字段保留。 for (end, _) in unicode_linebreak::linebreaks(para) { let part = ¶[start..end]; // 把尾部空白与可见正文分开,正文生成墨迹,末尾空白只作为布局 advance。 let body = part.trim_end_matches(char::is_whitespace); // 去除 CR/LF 后测尾部空白;没有把换行当成一行可绘制的空格宽度。 let space = crate::text::measure(part[body.len()..].trim_matches(['\n', '\r']), style) .width; // 同时预生成整词和各字素;后续列宽或避让区间变化只改摆放,不再次调用字体栅格化。 words.push(Word { ink: ink(body)?, letters: body .graphemes(true) .map(&mut ink) .collect::, _>>()?, space, newline: part.ends_with('\n'), }); start = end; } } Ok(Self { words }) } /// The same cursor crosses left/right fragments. Narrow gaps never discard /// a word; a word wider than every remaining fragment falls back to graphemes. /// graphics::flow 对每条行带调用,ranges 由障碍几何计算并按从左到右给出;y 是整行文字原点纵坐标。 pub fn row( &self, cursor: &mut Cursor, ranges: &[(f32, f32)], y: f32, out: &mut Vec, ) { // 同一游标穿过一行所有碎片,ri 用于查看当前及后续区间原始宽度,防止过早拆词。 for (ri, &(left, right)) in ranges.iter().enumerate() { // 进入新区间从左边界开始,上一碎片的横向剩余空间不累计到这里。 let mut x = left; // 仍有未消费文本才尝试摆放;每个成功分支推进词或字素,放不下则 break 到下一区间。 while let Some(word) = self.words.get(cursor.word) { // 空正文不生成 Placement,也不推进其 space;若是空白换行段则只消费它并结束当前行。 if word.letters.is_empty() { cursor.word += 1; cursor.letter = 0; // 显式换行结束整个 row,不只是当前碎片;下次调用从下一个词继续。 if word.newline { return; } continue; } // 取当前及后续区间的完整最大宽度,不是仅当前剩余 right-x;用来判断整词能否留给稍后碎片/行。 let remaining_max = ranges[ri..].iter().map(|(l, r)| r - l).fold(0., f32::max); // 只有尚未拆分且当前余宽容得下整词才走整词模式,0.001 容差吸收浮点边界误差。 if cursor.letter == 0 && word.ink.width <= right - x + 0.001 { out.push(Placement { word: cursor.word, letter: None, x, y, }); // 推进正文宽度和尾空白,再跳到下一词;尾空白本身不会产生图片命令。 x += word.ink.width + word.space; cursor.word += 1; if word.newline { return; } } else if cursor.letter == 0 && word.ink.width <= remaining_max + 0.001 { // 整词有望放入某个完整剩余区间时先保留它,结束当前碎片尝试,不因当前余宽小就拆成字素。 break; } else { // 词过宽或已经拆分才逐字素继续;这个分支保留跨碎片/跨行的 letter 下标。 let letter = &word.letters[cursor.letter]; // 连一个完整字素也放不下时不丢字,转到后续区间;若所有行都放不下,由外层高度预算结束 Flow。 if letter.width > right - x + 0.001 { break; } out.push(Placement { word: cursor.word, letter: Some(cursor.letter), x, y, }); // 成功放置一个字素后才递增游标,同一段的后续字素从新 x 继续。 x += letter.width; cursor.letter += 1; // 最后字素放完才消费尾空白并推进词;复位 letter,让下一词仍有整词放置机会。 if cursor.letter == word.letters.len() { cursor.word += 1; cursor.letter = 0; x += word.space; if word.newline { return; } } } } } } }