1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum BreakOpportunity {
5 Allowed,
6 Forbidden,
7 Mandatory,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LayoutMode {
12 HorizontalLtr,
13 VerticalRl,
14 VerticalLr,
15}
16
17#[derive(Debug, Clone, Copy)]
18pub struct LineLayoutPolicy {
19 layout_mode: LayoutMode,
20 redistribute_inline_width: fn(u32, u32) -> u32,
21}
22
23impl LineLayoutPolicy {
24 pub fn new(layout_mode: LayoutMode, redistribute_inline_width: fn(u32, u32) -> u32) -> Self {
25 Self {
26 layout_mode,
27 redistribute_inline_width,
28 }
29 }
30
31 pub fn horizontal_ltr() -> Self {
32 Self::new(LayoutMode::HorizontalLtr, preserve_inline_width)
33 }
34
35 pub fn layout_mode(&self) -> LayoutMode {
36 self.layout_mode
37 }
38
39 pub fn redistributed_inline_width(&self, line_width: u32, max_width: u32) -> u32 {
40 (self.redistribute_inline_width)(line_width, max_width)
41 }
42}
43
44#[derive(Debug, Clone, Copy)]
45pub struct WidthPolicy {
46 char_width: fn(char) -> u32,
47 tab_width: Option<u32>,
48}
49
50impl WidthPolicy {
51 pub fn new(char_width: fn(char) -> u32) -> Self {
52 Self {
53 char_width,
54 tab_width: None,
55 }
56 }
57
58 pub fn monospace_ascii(tab_width: u32) -> Self {
59 Self::with_tab_width(monospace_ascii_width, tab_width)
60 }
61
62 pub fn cjk_grid(tab_width: u32) -> Self {
63 Self::with_tab_width(cjk_grid_width, tab_width)
64 }
65
66 pub fn with_tab_width(char_width: fn(char) -> u32, tab_width: u32) -> Self {
67 Self {
68 char_width,
69 tab_width: Some(tab_width),
70 }
71 }
72
73 pub fn tab_width(&self) -> Option<u32> {
74 self.tab_width
75 }
76
77 pub fn char_width(&self) -> fn(char) -> u32 {
78 self.char_width
79 }
80
81 pub fn advance_of(&self, ch: char) -> u32 {
82 if ch == '\t' {
83 self.tab_width.unwrap_or_else(|| (self.char_width)(ch))
84 } else {
85 (self.char_width)(ch)
86 }
87 }
88
89 pub fn text_width(&self, text: &str) -> u32 {
90 text.chars().map(|ch| self.advance_of(ch)).sum()
91 }
92}
93
94#[derive(Debug, Clone, Copy)]
95pub struct WrapPolicy {
96 width_policy: WidthPolicy,
97 break_opportunity: fn(&str, usize) -> BreakOpportunity,
98}
99
100impl WrapPolicy {
101 pub fn new(
102 char_width: fn(char) -> u32,
103 break_opportunity: fn(&str, usize) -> BreakOpportunity,
104 ) -> Self {
105 Self::with_width_policy(WidthPolicy::new(char_width), break_opportunity)
106 }
107
108 pub fn with_width_policy(
109 width_policy: WidthPolicy,
110 break_opportunity: fn(&str, usize) -> BreakOpportunity,
111 ) -> Self {
112 Self {
113 width_policy,
114 break_opportunity,
115 }
116 }
117
118 pub fn width_policy(&self) -> WidthPolicy {
119 self.width_policy
120 }
121
122 pub fn char_width(&self) -> fn(char) -> u32 {
123 self.width_policy.char_width()
124 }
125
126 pub fn break_opportunity(&self) -> fn(&str, usize) -> BreakOpportunity {
127 self.break_opportunity
128 }
129
130 pub fn code() -> Self {
131 Self::code_with_width_policy(WidthPolicy::cjk_grid(4))
132 }
133
134 pub fn code_with_width_policy(width_policy: WidthPolicy) -> Self {
135 Self::with_width_policy(width_policy, code_break_opportunity)
136 }
137
138 pub fn japanese_basic() -> Self {
139 Self::with_width_policy(WidthPolicy::cjk_grid(4), japanese_break_opportunity)
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct WrapPoint {
145 byte_offset: u32,
146 visual_width: u32,
147}
148
149impl WrapPoint {
150 pub const fn byte_offset(&self) -> u32 {
151 self.byte_offset
152 }
153
154 pub const fn visual_width(&self) -> u32 {
155 self.visual_width
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub struct VisualLine {
161 start: u32,
162 end: u32,
163}
164
165impl VisualLine {
166 pub const fn start(&self) -> u32 {
167 self.start
168 }
169
170 pub const fn end(&self) -> u32 {
171 self.end
172 }
173
174 pub const fn len(&self) -> u32 {
175 self.end - self.start
176 }
177
178 pub const fn is_empty(&self) -> bool {
179 self.start == self.end
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct VisualLayoutSpace {
185 logical_line: u32,
186 visual_line: u32,
187 inline_advance: u32,
188 block_advance: u32,
189 layout_mode: LayoutMode,
190}
191
192impl VisualLayoutSpace {
193 pub const fn logical_line(&self) -> u32 {
194 self.logical_line
195 }
196
197 pub const fn visual_line(&self) -> u32 {
198 self.visual_line
199 }
200
201 pub const fn inline_advance(&self) -> u32 {
202 self.inline_advance
203 }
204
205 pub const fn block_advance(&self) -> u32 {
206 self.block_advance
207 }
208
209 pub const fn layout_mode(&self) -> LayoutMode {
210 self.layout_mode
211 }
212}
213
214#[derive(Debug, Clone)]
215pub struct WrapMap {
216 line_wraps: Vec<Vec<WrapPoint>>,
217 max_width: u32,
218}
219
220impl WrapMap {
221 pub fn new<'a>(
222 lines: impl Iterator<Item = &'a str>,
223 max_width: u32,
224 policy: &WrapPolicy,
225 ) -> Self {
226 let line_wraps = lines
227 .map(|line| wrap_line(line, max_width, policy))
228 .collect::<Vec<_>>();
229 Self {
230 line_wraps,
231 max_width,
232 }
233 }
234
235 pub const fn max_width(&self) -> u32 {
236 self.max_width
237 }
238
239 pub fn line_count(&self) -> u32 {
240 usize_to_u32(self.line_wraps.len(), "line count")
241 }
242
243 pub fn visual_line_count(&self, line: u32) -> u32 {
244 let index = u32_to_usize(line, "line");
245 let wraps = &self.line_wraps[index];
246 usize_to_u32(wraps.len(), "visual line count") + 1
247 }
248
249 pub fn total_visual_lines(&self) -> u32 {
250 self.line_wraps.iter().fold(0u32, |acc, wraps| {
251 acc + usize_to_u32(wraps.len(), "visual line count") + 1
252 })
253 }
254
255 pub fn wrap_points(&self, line: u32) -> &[WrapPoint] {
256 let index = u32_to_usize(line, "line");
257 &self.line_wraps[index]
258 }
259
260 pub fn visual_lines(&self, line: u32, line_len: u32) -> Vec<VisualLine> {
261 let mut visual_lines = Vec::new();
262 let mut start = 0u32;
263 for wrap in self.wrap_points(line) {
264 visual_lines.push(VisualLine {
265 start,
266 end: wrap.byte_offset(),
267 });
268 start = wrap.byte_offset();
269 }
270 visual_lines.push(VisualLine {
271 start,
272 end: line_len,
273 });
274 visual_lines
275 }
276
277 pub fn visual_layout_space(
278 &self,
279 line: u32,
280 local_visual_line: u32,
281 line_text: &str,
282 policy: &WrapPolicy,
283 line_layout_policy: &LineLayoutPolicy,
284 ) -> VisualLayoutSpace {
285 let line_len = usize_to_u32(line_text.len(), "line len");
286 let visual_lines = self.visual_lines(line, line_len);
287 let index = u32_to_usize(local_visual_line, "local visual line");
288 let visual_line = visual_lines[index];
289 let inline_advance = line_layout_policy.redistributed_inline_width(
290 policy.width_policy().text_width(
291 &line_text[u32_to_usize(visual_line.start, "start")
292 ..u32_to_usize(visual_line.end, "end")],
293 ),
294 self.max_width,
295 );
296
297 VisualLayoutSpace {
298 logical_line: line,
299 visual_line: local_visual_line,
300 inline_advance,
301 block_advance: local_visual_line,
302 layout_mode: line_layout_policy.layout_mode(),
303 }
304 }
305
306 pub fn to_visual_line(&self, line: u32, byte_offset_in_line: u32) -> u32 {
307 let prior = (0..line)
308 .map(|current| self.visual_line_count(current))
309 .sum::<u32>();
310 let local = self
311 .wrap_points(line)
312 .iter()
313 .take_while(|wrap| wrap.byte_offset() <= byte_offset_in_line)
314 .count();
315 prior + usize_to_u32(local, "visual line index")
316 }
317
318 pub fn from_visual_line(&self, visual_line: u32) -> (u32, u32) {
319 let mut remaining = visual_line;
320 for (line_index, wraps) in self.line_wraps.iter().enumerate() {
321 let count = usize_to_u32(wraps.len(), "visual line count") + 1;
322 if remaining < count {
323 let start = if remaining == 0 {
324 0
325 } else {
326 let wrap_index = u32_to_usize(remaining - 1, "wrap index");
327 wraps[wrap_index].byte_offset()
328 };
329 return (usize_to_u32(line_index, "line"), start);
330 }
331 remaining -= count;
332 }
333 panic!("visual line {visual_line} out of bounds");
334 }
335
336 pub fn rewrap_line(&mut self, line: u32, line_text: &str, policy: &WrapPolicy) {
337 let index = u32_to_usize(line, "line");
338 self.line_wraps[index] = wrap_line(line_text, self.max_width, policy);
339 }
340
341 pub fn set_max_width<'a>(
342 &mut self,
343 max_width: u32,
344 lines: impl Iterator<Item = &'a str>,
345 policy: &WrapPolicy,
346 ) {
347 self.max_width = max_width;
348 self.line_wraps = lines
349 .map(|line| wrap_line(line, max_width, policy))
350 .collect::<Vec<_>>();
351 }
352
353 pub fn splice_lines<'a>(
354 &mut self,
355 start_line: u32,
356 removed_count: u32,
357 new_lines: impl Iterator<Item = &'a str>,
358 policy: &WrapPolicy,
359 ) {
360 let start = u32_to_usize(start_line, "start line");
361 let end = start + u32_to_usize(removed_count, "removed line count");
362 let replacement = new_lines
363 .map(|line| wrap_line(line, self.max_width, policy))
364 .collect::<Vec<_>>();
365 self.line_wraps.splice(start..end, replacement);
366 }
367}
368
369pub fn wrap_line(line_text: &str, max_width: u32, policy: &WrapPolicy) -> Vec<WrapPoint> {
370 if max_width == 0 {
371 return Vec::new();
372 }
373
374 let width_policy = policy.width_policy();
375 let break_opportunity = policy.break_opportunity();
376 let mut wraps = Vec::new();
377 let mut total_width = 0u32;
378 let mut segment_start_offset = 0u32;
379 let mut segment_start_width = 0u32;
380 let mut last_allowed = None::<WrapPoint>;
381
382 for (byte_offset, ch) in line_text.char_indices() {
383 total_width += width_policy.advance_of(ch);
384 let next_offset = byte_offset + ch.len_utf8();
385 let next_offset_u32 = usize_to_u32(next_offset, "byte offset");
386 let wrap_point = WrapPoint {
387 byte_offset: next_offset_u32,
388 visual_width: total_width,
389 };
390
391 match break_opportunity(line_text, next_offset) {
392 BreakOpportunity::Allowed => {
393 last_allowed = Some(wrap_point);
394 }
395 BreakOpportunity::Forbidden => {}
396 BreakOpportunity::Mandatory => {
397 if next_offset < line_text.len() && next_offset_u32 > segment_start_offset {
398 wraps.push(wrap_point);
399 segment_start_offset = next_offset_u32;
400 segment_start_width = total_width;
401 }
402 last_allowed = None;
403 continue;
404 }
405 }
406
407 if total_width.saturating_sub(segment_start_width) > max_width {
408 if let Some(candidate) = last_allowed {
409 if candidate.byte_offset() > segment_start_offset {
410 wraps.push(candidate);
411 segment_start_offset = candidate.byte_offset();
412 segment_start_width = candidate.visual_width();
413 }
414 }
415 last_allowed = None;
416 }
417 }
418
419 wraps
420}
421
422fn monospace_ascii_width(ch: char) -> u32 {
423 let _ = ch;
424 1
425}
426
427fn cjk_grid_width(ch: char) -> u32 {
428 if ch.is_ascii() {
429 1
430 } else {
431 east_asian_width(ch)
432 }
433}
434
435fn preserve_inline_width(line_width: u32, _max_width: u32) -> u32 {
436 line_width
437}
438
439fn code_break_opportunity(line_text: &str, byte_offset: usize) -> BreakOpportunity {
440 if byte_offset == 0 || byte_offset >= line_text.len() {
441 return BreakOpportunity::Forbidden;
442 }
443
444 match prev_char(line_text, byte_offset) {
445 Some(ch) if ch.is_whitespace() || is_code_operator(ch) => BreakOpportunity::Allowed,
446 _ => BreakOpportunity::Forbidden,
447 }
448}
449
450fn japanese_break_opportunity(line_text: &str, byte_offset: usize) -> BreakOpportunity {
451 if byte_offset == 0 || byte_offset >= line_text.len() {
452 return BreakOpportunity::Forbidden;
453 }
454
455 let prev = match prev_char(line_text, byte_offset) {
456 Some(ch) => ch,
457 None => return BreakOpportunity::Forbidden,
458 };
459 let next = match next_char(line_text, byte_offset) {
460 Some(ch) => ch,
461 None => return BreakOpportunity::Forbidden,
462 };
463
464 if is_line_start_kinsoku(next) || is_line_end_kinsoku(prev) {
465 return BreakOpportunity::Forbidden;
466 }
467
468 if is_japanese_wrap_char(prev) && is_japanese_wrap_char(next) {
469 BreakOpportunity::Allowed
470 } else {
471 BreakOpportunity::Forbidden
472 }
473}
474
475fn prev_char(line_text: &str, byte_offset: usize) -> Option<char> {
476 line_text[..byte_offset].chars().next_back()
477}
478
479fn next_char(line_text: &str, byte_offset: usize) -> Option<char> {
480 line_text[byte_offset..].chars().next()
481}
482
483fn is_code_operator(ch: char) -> bool {
484 matches!(
485 ch,
486 '+' | '-'
487 | '*'
488 | '/'
489 | '%'
490 | '='
491 | '!'
492 | '?'
493 | '&'
494 | '|'
495 | '^'
496 | '~'
497 | ':'
498 | ';'
499 | ','
500 | '.'
501 )
502}
503
504fn is_line_start_kinsoku(ch: char) -> bool {
505 "。、.,:;?!)」』】〉》〕}―…".contains(ch)
506}
507
508fn is_line_end_kinsoku(ch: char) -> bool {
509 "(「『【〈《〔{".contains(ch)
510}
511
512fn is_japanese_wrap_char(ch: char) -> bool {
513 east_asian_width(ch) == 2
514}
515
516fn east_asian_width(ch: char) -> u32 {
517 if matches!(
518 ch as u32,
519 0x1100..=0x115F
520 | 0x2329..=0x232A
521 | 0x2E80..=0x303E
522 | 0x3040..=0x30FF
523 | 0x3100..=0x312F
524 | 0x3130..=0x318F
525 | 0x3190..=0x31EF
526 | 0x31F0..=0x31FF
527 | 0x3200..=0xA4CF
528 | 0xAC00..=0xD7A3
529 | 0xF900..=0xFAFF
530 | 0xFE10..=0xFE19
531 | 0xFE30..=0xFE6F
532 | 0xFF01..=0xFF60
533 | 0xFFE0..=0xFFE6
534 | 0x1F300..=0x1FAFF
535 | 0x20000..=0x3FFFD
536 ) {
537 2
538 } else {
539 1
540 }
541}
542
543fn usize_to_u32(value: usize, what: &str) -> u32 {
544 u32::try_from(value).unwrap_or_else(|_| panic!("{what} exceeds u32::MAX"))
545}
546
547fn u32_to_usize(value: u32, what: &str) -> usize {
548 usize::try_from(value).unwrap_or_else(|_| panic!("{what} exceeds usize::MAX"))
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn wrap_line_basic_wrapping() {
557 let wraps = wrap_line("ab cd ef", 4, &WrapPolicy::code());
558 assert_eq!(
559 wraps,
560 vec![
561 WrapPoint {
562 byte_offset: 3,
563 visual_width: 3,
564 },
565 WrapPoint {
566 byte_offset: 6,
567 visual_width: 6,
568 },
569 ]
570 );
571 }
572
573 #[test]
574 fn wrap_line_no_wrap_needed() {
575 let wraps = wrap_line("abc", 10, &WrapPolicy::code());
576 assert!(wraps.is_empty());
577 }
578
579 #[test]
580 fn wrap_line_zero_width_disables_wrapping() {
581 let wraps = wrap_line("ab cd", 0, &WrapPolicy::code());
582 assert!(wraps.is_empty());
583 }
584
585 #[test]
586 fn wrap_line_cjk_width_is_two() {
587 let wraps = wrap_line("あい うえ", 4, &WrapPolicy::code());
588 assert_eq!(wraps.len(), 1);
589 assert_eq!(wraps[0].byte_offset(), usize_to_u32("あい ".len(), "len"));
590 assert_eq!(wraps[0].visual_width(), 5);
591 }
592
593 #[test]
594 fn width_policy_can_customize_tab_advance() {
595 let width_policy = WidthPolicy::cjk_grid(2);
596 assert_eq!(width_policy.text_width("a\tb"), 4);
597
598 let wraps = wrap_line(
599 "a\tb c",
600 4,
601 &WrapPolicy::code_with_width_policy(width_policy),
602 );
603 assert_eq!(wraps.len(), 1);
604 assert_eq!(wraps[0].byte_offset(), usize_to_u32("a\tb ".len(), "len"));
605 }
606
607 #[test]
608 fn legacy_wrap_policy_new_keeps_tab_width_in_char_width_callback() {
609 fn legacy_width(ch: char) -> u32 {
610 if ch == '\t' {
611 7
612 } else {
613 1
614 }
615 }
616
617 let policy = WrapPolicy::new(legacy_width, code_break_opportunity);
618 assert_eq!(policy.char_width()('\t'), 7);
619 assert_eq!(policy.width_policy().advance_of('\t'), 7);
620 }
621
622 #[test]
623 fn visual_layout_space_tracks_inline_and_block_advances() {
624 let text = "ab cd";
625 let map = WrapMap::new([text].iter().copied(), 3, &WrapPolicy::code());
626 let layout = map.visual_layout_space(
627 0,
628 1,
629 text,
630 &WrapPolicy::code(),
631 &LineLayoutPolicy::horizontal_ltr(),
632 );
633
634 assert_eq!(layout.logical_line(), 0);
635 assert_eq!(layout.visual_line(), 1);
636 assert_eq!(layout.inline_advance(), 2);
637 assert_eq!(layout.block_advance(), 1);
638 assert_eq!(layout.layout_mode(), LayoutMode::HorizontalLtr);
639 }
640
641 #[test]
642 fn line_layout_policy_can_redistribute_inline_width() {
643 fn justify_to_max(_line_width: u32, max_width: u32) -> u32 {
644 max_width
645 }
646
647 let text = "ab cd";
648 let map = WrapMap::new([text].iter().copied(), 6, &WrapPolicy::code());
649 let layout = map.visual_layout_space(
650 0,
651 0,
652 text,
653 &WrapPolicy::code(),
654 &LineLayoutPolicy::new(LayoutMode::HorizontalLtr, justify_to_max),
655 );
656
657 assert_eq!(layout.inline_advance(), 6);
658 assert_eq!(layout.layout_mode(), LayoutMode::HorizontalLtr);
659 }
660
661 #[test]
662 fn code_policy_breaks_after_space_and_operator() {
663 let policy = WrapPolicy::code();
664 let break_opportunity = policy.break_opportunity();
665
666 assert_eq!(break_opportunity("a + b", 2), BreakOpportunity::Allowed);
667 assert_eq!(break_opportunity("a+b", 2), BreakOpportunity::Allowed);
668 assert_eq!(break_opportunity("ab", 1), BreakOpportunity::Forbidden);
669 assert_eq!(break_opportunity("ab", 0), BreakOpportunity::Forbidden);
670 }
671
672 #[test]
673 fn japanese_basic_applies_kinsoku() {
674 let policy = WrapPolicy::japanese_basic();
675 let break_opportunity = policy.break_opportunity();
676
677 assert_eq!(
678 break_opportunity("あい", "あ".len()),
679 BreakOpportunity::Allowed
680 );
681 assert_eq!(
682 break_opportunity("あ。", "あ".len()),
683 BreakOpportunity::Forbidden
684 );
685 assert_eq!(
686 break_opportunity("(あ", "(".len()),
687 BreakOpportunity::Forbidden
688 );
689 }
690
691 #[test]
692 fn wrap_map_construction_and_round_trip() {
693 let lines = ["ab cd ef", "xyz"];
694 let map = WrapMap::new(lines.iter().copied(), 4, &WrapPolicy::code());
695
696 assert_eq!(map.max_width(), 4);
697 assert_eq!(map.line_count(), 2);
698 assert_eq!(map.visual_line_count(0), 3);
699 assert_eq!(map.visual_line_count(1), 1);
700 assert_eq!(map.total_visual_lines(), 4);
701 assert_eq!(map.to_visual_line(0, 0), 0);
702 assert_eq!(map.to_visual_line(0, 3), 1);
703 assert_eq!(map.to_visual_line(1, 0), 3);
704 assert_eq!(map.from_visual_line(0), (0, 0));
705 assert_eq!(map.from_visual_line(1), (0, 3));
706 assert_eq!(map.from_visual_line(3), (1, 0));
707 assert_eq!(
708 map.visual_lines(0, usize_to_u32(lines[0].len(), "line len")),
709 vec![
710 VisualLine { start: 0, end: 3 },
711 VisualLine { start: 3, end: 6 },
712 VisualLine {
713 start: 6,
714 end: usize_to_u32(lines[0].len(), "line len"),
715 },
716 ]
717 );
718 }
719
720 #[test]
721 fn wrap_map_rewrap_line_updates_points() {
722 let lines = ["ab cd", "xy z"];
723 let mut map = WrapMap::new(lines.iter().copied(), 3, &WrapPolicy::code());
724 assert_eq!(map.visual_line_count(0), 2);
725
726 map.rewrap_line(0, "abcd", &WrapPolicy::code());
727 assert_eq!(map.wrap_points(0), &[]);
728 assert_eq!(map.visual_line_count(0), 1);
729 }
730
731 #[test]
732 fn wrap_map_splice_lines_replaces_range() {
733 let lines = ["ab cd", "xy z"];
734 let mut map = WrapMap::new(lines.iter().copied(), 3, &WrapPolicy::code());
735
736 map.splice_lines(1, 1, ["12 34", "p q"].iter().copied(), &WrapPolicy::code());
737
738 assert_eq!(map.line_count(), 3);
739 assert_eq!(map.visual_line_count(1), 2);
740 assert_eq!(map.visual_line_count(2), 1);
741 }
742
743 #[test]
744 fn wrap_map_set_max_width_recomputes_all_lines() {
745 let lines = ["ab cd", "xy z"];
746 let mut map = WrapMap::new(lines.iter().copied(), 10, &WrapPolicy::code());
747 assert_eq!(map.total_visual_lines(), 2);
748
749 map.set_max_width(3, lines.iter().copied(), &WrapPolicy::code());
750 assert_eq!(map.max_width(), 3);
751 assert_eq!(map.total_visual_lines(), 4);
752 }
753}