1use {
2 crate::*,
3 minimad::*,
4 unicode_width::{
5 UnicodeWidthChar,
6 UnicodeWidthStr,
7 },
8};
9
10pub static ELLIPSIS: &str = "…";
11
12#[derive(Debug, Clone, Copy)]
16pub struct Fitter {
17 mid_token_ellision: bool,
20
21 mid_compound_ellision: bool,
23
24 align: Alignment,
25}
26
27impl Default for Fitter {
28 fn default() -> Self {
29 Self {
30 mid_token_ellision: true,
31 mid_compound_ellision: true,
32 align: Alignment::Unspecified,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy)]
38struct CharInfo {
39 byte_idx: usize,
40 width: usize, }
42fn str_char_infos(s: &str) -> Vec<CharInfo> {
43 s.char_indices()
44 .map(|(byte_idx, char)| CharInfo {
45 byte_idx,
46 width: char.width().unwrap_or(0),
47 })
48 .collect()
49}
50
51#[derive(Debug, Clone)]
52struct Zone {
53 compound_idx: usize,
54 byte_start_idx: usize,
55 char_infos: Vec<CharInfo>,
56 removable_width: usize, }
58impl Zone {
59 fn token(compounds: &[Compound], min_removable_width: usize) -> Vec<Zone> {
60 let mut zones = Vec::new();
61 for (compound_idx, compound) in compounds.iter().enumerate() {
62 let s = compound.src;
63 if s.len() < min_removable_width + 2 {
64 continue;
65 }
66 let mut byte_start: Option<usize> = None;
67 for (byte_idx, char) in s.char_indices() {
68 if char.is_whitespace() {
69 if let Some(byte_start_idx) = byte_start {
70 if byte_idx - byte_start_idx >= min_removable_width + 2 {
71 let zs = &s[byte_start_idx..byte_idx];
72 let removable_width = zs.width();
73 if removable_width >= min_removable_width {
74 let char_infos = str_char_infos(zs);
75 zones.push(Zone {
76 compound_idx,
77 byte_start_idx,
78 char_infos,
79 removable_width,
80 });
81 }
82 }
83 byte_start = None;
84 }
85 } else if byte_start.is_none() {
86 byte_start = Some(byte_idx);
87 }
88 }
89 if let Some(byte_start_idx) = byte_start {
90 let byte_end_idx = s.len();
91 if byte_end_idx - byte_start_idx >= min_removable_width + 2 {
92 let zs = &s[byte_start_idx..];
93 let removable_width = zs.width();
94 if removable_width >= min_removable_width {
95 let char_infos = str_char_infos(zs);
96 zones.push(Zone {
97 compound_idx,
98 byte_start_idx,
99 char_infos,
100 removable_width,
101 });
102 }
103 }
104 }
105 }
106 zones
107 }
108 fn biggest_token(compounds: &[Compound], min_removable_width: usize) -> Option<Zone> {
109 Zone::token(compounds, min_removable_width)
110 .drain(..)
111 .max_by_key(|z| z.removable_width)
112 }
113 fn compounds(compounds: &[Compound], min_removable_width: usize) -> Vec<Zone> {
115 compounds
116 .iter()
117 .enumerate()
118 .filter_map(|(compound_idx, compound)| {
119 let char_infos = str_char_infos(compound.src);
120 if char_infos.len() < 2 + min_removable_width {
121 return None;
122 }
123 let removable = &compound.src
124 [char_infos[1].byte_idx..char_infos[char_infos.len() - 1].byte_idx];
125 let removable_width = removable.width();
126 if removable_width < min_removable_width {
127 None
128 } else {
129 Some(Zone {
130 compound_idx,
131 byte_start_idx: 0,
132 char_infos,
133 removable_width,
134 })
135 }
136 })
137 .collect()
138 }
139 fn biggest_compound(compounds: &[Compound], min_removable_width: usize) -> Option<Zone> {
140 Zone::compounds(compounds, min_removable_width)
141 .drain(..)
142 .max_by_key(|z| z.removable_width)
143 }
144 fn cut(&self, compounds: &mut Vec<Compound>, to_remove: usize) -> usize {
146 if self.removable_width < 2 {
147 return 0;
148 }
149 let compound = &compounds[self.compound_idx];
150 let len = self.char_infos.len();
151 let mut start_char_idx = len / 2;
152 let mut end_char_idx = start_char_idx;
153 let mut removed_width = 0;
154 loop {
155 if (end_char_idx - start_char_idx) % 2 == 0 {
157 if end_char_idx + 1 >= len {
158 break;
159 }
160 end_char_idx += 1;
161 } else {
162 if start_char_idx <= 1 {
163 break;
164 }
165 start_char_idx -= 1;
166 }
167 let start_byte_idx = self.byte_start_idx + self.char_infos[start_char_idx].byte_idx;
168 let end_byte_idx = self.byte_start_idx + self.char_infos[end_char_idx].byte_idx;
169 removed_width = (compound.src[start_byte_idx..end_byte_idx]).width();
170 if removed_width >= to_remove {
171 break;
172 }
173 }
174 let start_byte_idx = self.byte_start_idx + self.char_infos[start_char_idx].byte_idx;
175 let end_byte_idx = self.byte_start_idx + self.char_infos[end_char_idx].byte_idx;
176 let head = compound.sub(0, start_byte_idx);
177 let tail = compound.tail(end_byte_idx);
178 compounds[self.compound_idx] = head;
179 compounds.insert(self.compound_idx + 1, Compound::raw_str(ELLIPSIS));
180 compounds.insert(self.compound_idx + 2, tail);
181
182 removed_width - 1
183 }
184}
185
186impl Fitter {
187 pub fn for_align(align: Alignment) -> Self {
192 let internal_ellision = align == Alignment::Unspecified;
193 Self {
194 mid_token_ellision: internal_ellision,
195 mid_compound_ellision: internal_ellision,
196 align,
197 }
198 }
199
200 pub fn fit(self, fc: &mut FmtComposite<'_>, max_width: usize, skin: &MadSkin) {
203 if fc.visible_length <= max_width {
205 return;
206 } else if max_width == 0 {
207 fc.compounds.clear();
208 fc.visible_length = 0;
209 return;
210 } else if max_width == 1 {
211 fc.compounds.clear();
212 fc.compounds.push(Compound::raw_str(ELLIPSIS));
213 fc.visible_length = 1;
214 return;
215 }
216
217 let mut excess = fc.visible_length - max_width;
218
219 if self.mid_token_ellision {
223 while excess > 0 {
225 let mut gain = 0;
226 if let Some(zone) = Zone::biggest_token(&fc.compounds, 3) {
227 gain = zone.cut(&mut fc.compounds, excess + 1);
228 }
229 if gain == 0 {
230 break;
231 }
232 excess -= gain.min(excess);
233 }
234 }
235
236 if self.mid_compound_ellision {
237 while excess > 0 {
239 let mut gain = 0;
240 if let Some(zone) = Zone::biggest_compound(&fc.compounds, 2) {
243 gain = zone.cut(&mut fc.compounds, excess + 1);
244 }
245 if gain == 0 {
246 break;
247 }
248 excess -= gain.min(excess);
249 }
250 }
251
252 if excess == 0 {
253 fc.recompute_width(skin);
254 return;
255 }
256
257 let compounds = &mut fc.compounds;
258 let (mut excess_left, mut excess_right) = match self.align {
261 Alignment::Right => (excess + 1, 0),
262 Alignment::Left | Alignment::Unspecified => (0, excess + 1),
263 Alignment::Center => {
264 let left = excess / 2;
265 let right = excess - left;
266 if left > 0 {
267 (left + 1, right + 1)
268 } else {
269 (0, right + 1)
270 }
271 }
272 };
273
274 if excess_left > 0 {
275 while excess_left > 0 && !compounds.is_empty() {
277 let compound = &mut compounds[0];
278 let char_infos = str_char_infos(compound.src);
279 let mut last_removed_char_idx = 0;
280 let mut removed_width = 0;
281 loop {
282 removed_width += char_infos[last_removed_char_idx].width;
283 if removed_width >= excess_left || last_removed_char_idx + 1 == char_infos.len()
284 {
285 break;
286 }
287 last_removed_char_idx += 1;
288 }
289 if last_removed_char_idx + 1 == char_infos.len() {
290 compounds.remove(0);
292 excess_left -= removed_width.min(excess_left);
293 } else {
294 compound.src = &compound.src[char_infos[last_removed_char_idx + 1].byte_idx..];
296 excess_left = 0;
297 }
298 }
299 compounds.insert(0, Compound::raw_str(ELLIPSIS));
300 }
301
302 if excess_right > 0 {
303 while excess_right > 0 && !compounds.is_empty() {
305 let last_idx = compounds.len() - 1;
306 let compound = &mut compounds[last_idx];
307 let char_infos = str_char_infos(compound.src);
308 let mut removed_width = 0;
309 let mut end_byte_idx = compound.src.len();
310 for ci in char_infos.iter().rev() {
311 end_byte_idx = ci.byte_idx;
312 removed_width += ci.width;
313 if removed_width >= excess_right {
314 break;
315 }
316 }
317 if end_byte_idx == 0 {
318 compounds.pop();
320 excess_right -= removed_width.min(excess_right);
321 } else {
322 compound.src = &compound.src[..end_byte_idx];
324 excess_right = 0;
325 }
326 }
327 compounds.push(Compound::raw_str(ELLIPSIS));
328 }
329
330 fc.recompute_width(skin);
331 }
332}
333
334#[cfg(test)]
341mod fit_tests {
342
343 use {
344 crate::{
345 Fitter,
346 FmtComposite,
347 },
348 minimad::{
349 Alignment,
350 Composite,
351 },
352 };
353
354 fn check_fit_align(src: &str, target_width: usize, align: Alignment) {
355 dbg!((target_width, align));
356 let skin = crate::get_default_skin();
357 let mut fc = FmtComposite::from(Composite::from_inline(src), skin);
358 let fitter = Fitter::for_align(align);
359 fitter.fit(&mut fc, target_width, skin);
360 dbg!(&fc);
361 assert!(fc.visible_length <= target_width); }
363
364 fn check_fit(src: &str, target_width: usize) {
365 check_fit_align(src, target_width, Alignment::Right);
366 check_fit_align(src, target_width, Alignment::Left);
367 check_fit_align(src, target_width, Alignment::Center);
368 check_fit_align(src, target_width, Alignment::Unspecified);
369 }
370
371 #[test]
372 fn test_fit() {
373 let sentence = "This sentence has **short** and **much longer** parts, and some Korean: *一曰道,二曰天*.";
374 check_fit(sentence, 60);
375 check_fit(sentence, 40);
376
377 let five_issues = "一曰道,二曰天,三曰地,四曰將,五曰法。";
378 check_fit(five_issues, 15);
379 check_fit(five_issues, 8);
380
381 let status = "ab *cd* `12345 123456789`";
382 check_fit(status, 17);
383 check_fit(status, 2);
384 }
385}