1use blockworx_geom::{Vec2, WorldPx, vec2};
10use blockworx_paint::Font;
11use blockworx_paint::text::{Glyph, Layout, Row};
12
13use crate::metrics::Metrics;
14
15pub(crate) fn rows_of(
18 paragraphs: Vec<Vec<Glyph>>,
19 wrap: WorldPx,
20 metrics: Metrics,
21 font: &Font,
22) -> Layout {
23 let fit = Fit {
24 wrap: wrap.get(),
25 height: metrics.row_height(font),
26 };
27 let last = paragraphs.len().saturating_sub(1);
28 let mut rows: Vec<Row> = Vec::with_capacity(paragraphs.len());
29 for (index, glyphs) in paragraphs.into_iter().enumerate() {
30 let ends = if index == last {
31 Break::Wrap
32 } else {
33 Break::Newline
34 };
35 if glyphs.is_empty() || width_of(&glyphs) <= fit.wrap {
36 rows.push(fit.row(glyphs, ends));
37 continue;
38 }
39 fit.break_rows(&glyphs, &mut rows);
40 if let Some(row) = rows.last_mut() {
41 row.ends_with_newline = ends.closes_a_paragraph();
42 }
43 }
44 stack(rows)
45}
46
47struct Fit {
50 wrap: f32,
51 height: f32,
52}
53
54impl Fit {
55 fn break_rows(&self, glyphs: &[Glyph], out: &mut Vec<Row>) {
56 let mut candidates = Candidates::default();
57 let mut start = 0;
58 let mut start_x = 0.0;
59 for index in 0..glyphs.len() {
60 if self.wrap < right_of(&glyphs[index]) - start_x
61 && let Some(kept) = candidates.pick()
62 {
63 out.push(self.row(shifted(&glyphs[start..=kept], start_x), Break::Wrap));
64 start = kept + 1;
67 start_x = glyphs[start].pos.x;
68 candidates.forget_before(start);
69 }
70 candidates.add(index, &glyphs[index..]);
71 }
72 if start < glyphs.len() {
73 out.push(self.row(shifted(&glyphs[start..], start_x), Break::Wrap));
74 }
75 }
76
77 fn row(&self, glyphs: Vec<Glyph>, ends: Break) -> Row {
78 Row {
79 pos: Vec2::ZERO,
80 height: self.height,
81 ends_with_newline: ends.closes_a_paragraph(),
82 glyphs,
83 }
84 }
85}
86
87#[derive(Clone, Copy, PartialEq)]
89enum Break {
90 Newline,
91 Wrap,
92}
93
94impl Break {
95 fn closes_a_paragraph(self) -> bool {
96 self == Break::Newline
97 }
98}
99
100fn shifted(glyphs: &[Glyph], by: f32) -> Vec<Glyph> {
101 glyphs
102 .iter()
103 .map(|glyph| Glyph {
104 pos: vec2(glyph.pos.x - by, glyph.pos.y),
105 ..*glyph
106 })
107 .collect()
108}
109
110fn stack(mut rows: Vec<Row>) -> Layout {
112 let mut y = 0.0;
113 let mut width: f32 = 0.0;
114 for row in &mut rows {
115 row.pos = vec2(0.0, y);
116 y += row.height;
117 width = width.max(width_of(&row.glyphs));
118 }
119 Layout {
120 size: vec2(width, y),
121 rows,
122 }
123}
124
125fn right_of(glyph: &Glyph) -> f32 {
126 glyph.pos.x + glyph.advance
127}
128
129fn width_of(glyphs: &[Glyph]) -> f32 {
130 glyphs.last().map_or(0.0, right_of)
131}
132
133#[derive(Default)]
137struct Candidates {
138 space: Option<usize>,
139 cjk: Option<usize>,
140 pre_cjk: Option<usize>,
142 dash: Option<usize>,
143 punctuation: Option<usize>,
144 any: Option<usize>,
145}
146
147impl Candidates {
148 fn add(&mut self, index: usize, rest: &[Glyph]) {
149 const NON_BREAKING_SPACE: char = '\u{A0}';
150 let chr = rest[0].chr;
151 if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
152 self.space = Some(index);
153 } else if is_cjk(chr) && (rest.len() == 1 || may_begin_a_row(rest[1].chr)) {
154 self.cjk = Some(index);
155 } else if chr == '-' {
156 self.dash = Some(index);
157 } else if chr.is_ascii_punctuation() {
158 self.punctuation = Some(index);
159 } else if rest.len() > 1 && is_cjk(rest[1].chr) {
160 self.pre_cjk = Some(index);
161 }
162 self.any = Some(index);
163 }
164
165 fn pick(&self) -> Option<usize> {
166 [self.space, self.cjk, self.pre_cjk]
167 .into_iter()
168 .max()
169 .flatten()
170 .or(self.dash)
171 .or(self.punctuation)
172 .or(self.any)
173 }
174
175 fn forget_before(&mut self, index: usize) {
176 for candidate in [
177 &mut self.space,
178 &mut self.cjk,
179 &mut self.pre_cjk,
180 &mut self.dash,
181 &mut self.punctuation,
182 &mut self.any,
183 ] {
184 if candidate.is_some_and(|at| at < index) {
185 *candidate = None;
186 }
187 }
188 }
189}
190
191fn is_cjk(chr: char) -> bool {
194 ('\u{4E00}'..='\u{9FFF}').contains(&chr)
195 || ('\u{3400}'..='\u{4DBF}').contains(&chr)
196 || ('\u{2B740}'..='\u{2B81F}').contains(&chr)
197 || ('\u{3040}'..='\u{309F}').contains(&chr)
198 || ('\u{30A0}'..='\u{30FF}').contains(&chr)
199}
200
201fn may_begin_a_row(chr: char) -> bool {
203 !")]}〕〉》」』】〙〗〟'\"⦆»ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻‐゠–〜?!‼⁇⁈⁉・、:;,。."
204 .contains(chr)
205}