1use neco_textview::{LineIndex, Selection, TextViewError};
7use neco_wrap::{
8 LayoutMode, LineLayoutPolicy, VisualLayoutSpace, WidthPolicy, WrapMap, WrapPolicy,
9};
10use std::fmt;
11
12#[derive(Debug, Clone, Copy)]
14pub struct ViewportMetrics {
15 pub line_height: f64,
16 pub char_width: f64,
17 pub cjk_char_width: f64,
18 pub tab_width: u32,
19}
20
21#[derive(Debug, Clone, Copy)]
23pub struct ViewportLayout {
24 pub gutter_width: f64,
25 pub content_left: f64,
26}
27
28#[derive(Debug, Clone, Copy)]
30pub struct Rect {
31 pub x: f64,
32 pub y: f64,
33 pub width: f64,
34 pub height: f64,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct VisualLineFrame {
39 layout: VisualLayoutSpace,
40}
41
42impl VisualLineFrame {
43 pub const fn logical_line(&self) -> u32 {
44 self.layout.logical_line()
45 }
46
47 pub const fn visual_line(&self) -> u32 {
48 self.layout.visual_line()
49 }
50
51 pub const fn inline_advance(&self) -> u32 {
52 self.layout.inline_advance()
53 }
54
55 pub const fn block_advance(&self) -> u32 {
56 self.layout.block_advance()
57 }
58
59 pub const fn layout_mode(&self) -> LayoutMode {
60 self.layout.layout_mode()
61 }
62}
63
64#[non_exhaustive]
66#[derive(Debug)]
67pub enum ViewportError {
68 TextView(TextViewError),
69}
70
71impl fmt::Display for ViewportError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::TextView(e) => write!(f, "text view error: {e}"),
75 }
76 }
77}
78
79impl std::error::Error for ViewportError {
80 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81 match self {
82 Self::TextView(e) => Some(e),
83 }
84 }
85}
86
87impl From<TextViewError> for ViewportError {
88 fn from(e: TextViewError) -> Self {
89 Self::TextView(e)
90 }
91}
92
93fn text_width(
99 text: &str,
100 start_byte: usize,
101 end_byte: usize,
102 metrics: &ViewportMetrics,
103 width_policy: &WidthPolicy,
104) -> f64 {
105 let slice = &text[start_byte..end_byte];
106 slice
107 .chars()
108 .map(|ch| char_pixel_width(ch, metrics, width_policy))
109 .sum()
110}
111
112fn char_pixel_width(ch: char, metrics: &ViewportMetrics, width_policy: &WidthPolicy) -> f64 {
113 let advance = width_policy.advance_of(ch);
114 if ch == '\t' {
115 f64::from(advance) * metrics.char_width
116 } else if advance >= 2 {
117 metrics.cjk_char_width
118 } else {
119 metrics.char_width
120 }
121}
122
123fn u32_to_usize(v: u32) -> usize {
124 usize::try_from(v).expect("u32 exceeds usize::MAX")
125}
126
127pub fn visible_line_range(
135 scroll_top: f64,
136 container_height: f64,
137 wrap_map: &WrapMap,
138 metrics: &ViewportMetrics,
139) -> (u32, u32) {
140 let total = wrap_map.total_visual_lines();
141 if total == 0 {
142 return (0, 0);
143 }
144 let max_line = total - 1;
145
146 let first_f = scroll_top / metrics.line_height;
147 let first = if first_f < 0.0 {
148 0u32
149 } else {
150 let v = first_f as u64;
151 u32::try_from(v.min(u64::from(max_line))).expect("clamped value fits u32")
152 };
153
154 let last_f = (scroll_top + container_height) / metrics.line_height;
155 let last_raw = if last_f < 0.0 {
156 0u64
157 } else {
158 let ceil = last_f.ceil() as u64;
162 if ceil == 0 {
163 0
164 } else {
165 ceil - 1
166 }
167 };
168 let last = u32::try_from(last_raw.min(u64::from(max_line))).expect("clamped value fits u32");
169
170 (first, last)
171}
172
173pub fn caret_rect(
175 text: &str,
176 offset: usize,
177 line_index: &LineIndex,
178 wrap_map: &WrapMap,
179 metrics: &ViewportMetrics,
180 layout: &ViewportLayout,
181) -> Result<Rect, ViewportError> {
182 caret_rect_with_width_policy(
183 text,
184 offset,
185 line_index,
186 wrap_map,
187 metrics,
188 layout,
189 &WidthPolicy::cjk_grid(metrics.tab_width),
190 )
191}
192
193pub fn caret_rect_with_width_policy(
194 text: &str,
195 offset: usize,
196 line_index: &LineIndex,
197 wrap_map: &WrapMap,
198 metrics: &ViewportMetrics,
199 layout: &ViewportLayout,
200 width_policy: &WidthPolicy,
201) -> Result<Rect, ViewportError> {
202 let line = line_index.line_of_offset(offset)?;
203 let line_range = line_index.line_range(line)?;
204 let byte_in_line = offset - line_range.start();
205 let byte_in_line_u32 =
206 u32::try_from(byte_in_line).expect("byte offset in line exceeds u32::MAX");
207
208 let visual_line = wrap_map.to_visual_line(line, byte_in_line_u32);
209
210 let (_, vl_start_in_line) = wrap_map.from_visual_line(visual_line);
212 let vl_start_abs = line_range.start() + u32_to_usize(vl_start_in_line);
213
214 let x = layout.content_left + text_width(text, vl_start_abs, offset, metrics, width_policy);
215 let y = f64::from(visual_line) * metrics.line_height;
216
217 Ok(Rect {
218 x,
219 y,
220 width: 2.0,
221 height: metrics.line_height,
222 })
223}
224
225pub fn selection_rects(
227 text: &str,
228 selection: &Selection,
229 line_index: &LineIndex,
230 wrap_map: &WrapMap,
231 metrics: &ViewportMetrics,
232 layout: &ViewportLayout,
233) -> Result<Vec<Rect>, ViewportError> {
234 selection_rects_with_width_policy(
235 text,
236 selection,
237 line_index,
238 wrap_map,
239 metrics,
240 layout,
241 &WidthPolicy::cjk_grid(metrics.tab_width),
242 )
243}
244
245pub fn selection_rects_with_width_policy(
246 text: &str,
247 selection: &Selection,
248 line_index: &LineIndex,
249 wrap_map: &WrapMap,
250 metrics: &ViewportMetrics,
251 layout: &ViewportLayout,
252 width_policy: &WidthPolicy,
253) -> Result<Vec<Rect>, ViewportError> {
254 let range = selection.range();
255 if range.is_empty() {
256 return Ok(Vec::new());
257 }
258
259 let start_offset = range.start();
260 let end_offset = range.end();
261
262 let start_line = line_index.line_of_offset(start_offset)?;
263 let start_line_range = line_index.line_range(start_line)?;
264 let start_byte_in_line = start_offset - start_line_range.start();
265 let start_byte_u32 =
266 u32::try_from(start_byte_in_line).expect("byte offset in line exceeds u32::MAX");
267 let first_vl = wrap_map.to_visual_line(start_line, start_byte_u32);
268
269 let end_line = line_index.line_of_offset(end_offset)?;
270 let end_line_range = line_index.line_range(end_line)?;
271 let end_byte_in_line = end_offset - end_line_range.start();
272 let end_byte_u32 =
273 u32::try_from(end_byte_in_line).expect("byte offset in line exceeds u32::MAX");
274 let last_vl = wrap_map.to_visual_line(end_line, end_byte_u32);
275
276 let mut rects = Vec::new();
277
278 for vl in first_vl..=last_vl {
279 let (log_line, vl_start_in_line) = wrap_map.from_visual_line(vl);
280 let lr = line_index.line_range(log_line)?;
281 let vl_start_abs = lr.start() + u32_to_usize(vl_start_in_line);
282
283 let total_vl = wrap_map.total_visual_lines();
285 let vl_end_abs = if vl + 1 < total_vl {
286 let (next_log, next_start_in_line) = wrap_map.from_visual_line(vl + 1);
287 if next_log == log_line {
288 lr.start() + u32_to_usize(next_start_in_line)
289 } else {
290 lr.end()
291 }
292 } else {
293 lr.end()
294 };
295
296 let sel_start = start_offset.max(vl_start_abs);
298 let sel_end = end_offset.min(vl_end_abs);
299
300 if sel_start >= sel_end {
301 continue;
302 }
303
304 let x =
305 layout.content_left + text_width(text, vl_start_abs, sel_start, metrics, width_policy);
306 let w = text_width(text, sel_start, sel_end, metrics, width_policy);
307 let y = f64::from(vl) * metrics.line_height;
308
309 rects.push(Rect {
310 x,
311 y,
312 width: w,
313 height: metrics.line_height,
314 });
315 }
316
317 Ok(rects)
318}
319
320#[allow(clippy::too_many_arguments)]
322pub fn hit_test(
323 x: f64,
324 y: f64,
325 scroll_top: f64,
326 text: &str,
327 line_index: &LineIndex,
328 wrap_map: &WrapMap,
329 metrics: &ViewportMetrics,
330 layout: &ViewportLayout,
331) -> usize {
332 hit_test_with_width_policy(
333 x,
334 y,
335 scroll_top,
336 text,
337 line_index,
338 wrap_map,
339 metrics,
340 layout,
341 &WidthPolicy::cjk_grid(metrics.tab_width),
342 )
343}
344
345#[allow(clippy::too_many_arguments)]
346pub fn hit_test_with_width_policy(
347 x: f64,
348 y: f64,
349 scroll_top: f64,
350 text: &str,
351 line_index: &LineIndex,
352 wrap_map: &WrapMap,
353 metrics: &ViewportMetrics,
354 layout: &ViewportLayout,
355 width_policy: &WidthPolicy,
356) -> usize {
357 let total_vl = wrap_map.total_visual_lines();
358 if total_vl == 0 {
359 return 0;
360 }
361
362 let vl_f = (y + scroll_top) / metrics.line_height;
364 let vl_raw = if vl_f < 0.0 { 0u64 } else { vl_f as u64 };
365 let vl = u32::try_from(vl_raw.min(u64::from(total_vl - 1))).expect("clamped value fits u32");
366
367 let (log_line, vl_start_in_line) = wrap_map.from_visual_line(vl);
368 let lr = match line_index.line_range(log_line) {
369 Ok(r) => r,
370 Err(_) => return text.len(),
371 };
372 let vl_start_abs = lr.start() + u32_to_usize(vl_start_in_line);
373
374 let vl_end_abs = if vl + 1 < total_vl {
376 let (next_log, next_start_in_line) = wrap_map.from_visual_line(vl + 1);
377 if next_log == log_line {
378 lr.start() + u32_to_usize(next_start_in_line)
379 } else {
380 lr.end()
381 }
382 } else {
383 lr.end()
384 };
385
386 let rel_x = (x - layout.content_left).max(0.0);
388
389 let slice = &text[vl_start_abs..vl_end_abs];
391 let mut accum = 0.0;
392 for (i, ch) in slice.char_indices() {
393 let cw = char_pixel_width(ch, metrics, width_policy);
394 if rel_x < accum + cw * 0.5 {
396 return vl_start_abs + i;
397 }
398 accum += cw;
399 }
400
401 vl_end_abs
403}
404
405pub fn gutter_width(total_lines: u32, metrics: &ViewportMetrics) -> f64 {
407 let digit_count = total_lines.max(1).ilog10() + 1;
408 f64::from(digit_count) * metrics.char_width + metrics.char_width
409}
410
411pub fn line_top(visual_line: u32, metrics: &ViewportMetrics) -> f64 {
413 f64::from(visual_line) * metrics.line_height
414}
415
416#[allow(clippy::too_many_arguments)]
417pub fn visual_line_frame(
418 text: &str,
419 visual_line: u32,
420 line_index: &LineIndex,
421 wrap_map: &WrapMap,
422 _metrics: &ViewportMetrics,
423 _layout: &ViewportLayout,
424 width_policy: &WidthPolicy,
425 line_layout_policy: &LineLayoutPolicy,
426) -> Result<VisualLineFrame, ViewportError> {
427 let (logical_line, vl_start_in_line) = wrap_map.from_visual_line(visual_line);
428 let line_range = line_index.line_range(logical_line)?;
429 let total_vl = wrap_map.total_visual_lines();
430 let vl_end_in_line = if visual_line + 1 < total_vl {
431 let (next_logical_line, next_start_in_line) = wrap_map.from_visual_line(visual_line + 1);
432 if next_logical_line == logical_line {
433 next_start_in_line
434 } else {
435 u32::try_from(line_range.end() - line_range.start()).expect("line length fits u32")
436 }
437 } else {
438 u32::try_from(line_range.end() - line_range.start()).expect("line length fits u32")
439 };
440 let line_text = &text[line_range.start()..line_range.end()];
441 let local_visual_line = visual_line - wrap_map.to_visual_line(logical_line, 0);
442 let wrap_policy = WrapPolicy::code_with_width_policy(*width_policy);
443 let visual_layout = wrap_map.visual_layout_space(
444 logical_line,
445 local_visual_line,
446 line_text,
447 &wrap_policy,
448 line_layout_policy,
449 );
450 debug_assert_eq!(
451 visual_layout.inline_advance(),
452 line_layout_policy.redistributed_inline_width(
453 width_policy.text_width(
454 &line_text[u32_to_usize(vl_start_in_line)..u32_to_usize(vl_end_in_line)]
455 ),
456 wrap_map.max_width(),
457 )
458 );
459 Ok(VisualLineFrame {
460 layout: visual_layout,
461 })
462}
463
464pub fn scroll_to_reveal(
466 _text: &str,
467 offset: usize,
468 scroll_top: f64,
469 container_height: f64,
470 line_index: &LineIndex,
471 wrap_map: &WrapMap,
472 metrics: &ViewportMetrics,
473) -> Result<Option<f64>, ViewportError> {
474 let line = line_index.line_of_offset(offset)?;
475 let line_range = line_index.line_range(line)?;
476 let byte_in_line = offset - line_range.start();
477 let byte_in_line_u32 =
478 u32::try_from(byte_in_line).expect("byte offset in line exceeds u32::MAX");
479 let vl = wrap_map.to_visual_line(line, byte_in_line_u32);
480
481 let top = f64::from(vl) * metrics.line_height;
482 let bottom = top + metrics.line_height;
483
484 if top < scroll_top {
485 Ok(Some(top))
487 } else if bottom > scroll_top + container_height {
488 Ok(Some(bottom - container_height))
490 } else {
491 Ok(None)
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use neco_wrap::{LineLayoutPolicy, WidthPolicy, WrapMap, WrapPolicy};
499
500 fn default_metrics() -> ViewportMetrics {
501 ViewportMetrics {
502 line_height: 20.0,
503 char_width: 8.0,
504 cjk_char_width: 14.0,
505 tab_width: 4,
506 }
507 }
508
509 fn default_width_policy() -> WidthPolicy {
510 WidthPolicy::cjk_grid(4)
511 }
512
513 fn default_line_layout_policy() -> LineLayoutPolicy {
514 LineLayoutPolicy::horizontal_ltr()
515 }
516
517 fn default_layout() -> ViewportLayout {
518 ViewportLayout {
519 gutter_width: 40.0,
520 content_left: 48.0,
521 }
522 }
523
524 fn make_wrap_map(text: &str, max_width: u32) -> WrapMap {
525 let lines: Vec<&str> = text.split('\n').collect();
526 WrapMap::new(lines.iter().copied(), max_width, &WrapPolicy::code())
527 }
528
529 #[test]
534 fn visible_line_range_single_line() {
535 let text = "hello";
536 let wm = make_wrap_map(text, 80);
537 let metrics = default_metrics();
538 let (first, last) = visible_line_range(0.0, 100.0, &wm, &metrics);
539 assert_eq!(first, 0);
540 assert_eq!(last, 0);
541 }
542
543 #[test]
544 fn visible_line_range_multi_line() {
545 let text = "aaa\nbbb\nccc\nddd\neee";
546 let wm = make_wrap_map(text, 80);
547 let metrics = default_metrics();
548 let (first, last) = visible_line_range(0.0, 60.0, &wm, &metrics);
550 assert_eq!(first, 0);
551 assert_eq!(last, 2);
552 }
553
554 #[test]
555 fn visible_line_range_scrolled() {
556 let text = "aaa\nbbb\nccc\nddd\neee";
557 let wm = make_wrap_map(text, 80);
558 let metrics = default_metrics();
559 let (first, last) = visible_line_range(40.0, 40.0, &wm, &metrics);
561 assert_eq!(first, 2);
562 assert_eq!(last, 3);
563 }
564
565 #[test]
566 fn visible_line_range_with_wrapping() {
567 let text = "ab cd ef";
569 let wm = make_wrap_map(text, 4);
570 let metrics = default_metrics();
571 let total = wm.total_visual_lines();
572 assert_eq!(total, 3);
573 let (first, last) = visible_line_range(0.0, 60.0, &wm, &metrics);
574 assert_eq!(first, 0);
575 assert_eq!(last, 2);
576 }
577
578 #[test]
579 fn visible_line_range_clamps_past_end() {
580 let text = "aaa\nbbb";
581 let wm = make_wrap_map(text, 80);
582 let metrics = default_metrics();
583 let (first, last) = visible_line_range(0.0, 1000.0, &wm, &metrics);
584 assert_eq!(first, 0);
585 assert_eq!(last, 1);
586 }
587
588 #[test]
593 fn caret_rect_line_start() {
594 let text = "hello\nworld";
595 let li = LineIndex::new(text);
596 let wm = make_wrap_map(text, 80);
597 let metrics = default_metrics();
598 let layout = default_layout();
599 let width_policy = default_width_policy();
600
601 let r = caret_rect_with_width_policy(text, 0, &li, &wm, &metrics, &layout, &width_policy)
602 .unwrap();
603 assert!((r.x - layout.content_left).abs() < f64::EPSILON);
604 assert!((r.y - 0.0).abs() < f64::EPSILON);
605 assert!((r.height - 20.0).abs() < f64::EPSILON);
606 }
607
608 #[test]
609 fn caret_rect_line_middle() {
610 let text = "hello\nworld";
611 let li = LineIndex::new(text);
612 let wm = make_wrap_map(text, 80);
613 let metrics = default_metrics();
614 let layout = default_layout();
615 let width_policy = default_width_policy();
616
617 let r = caret_rect_with_width_policy(text, 3, &li, &wm, &metrics, &layout, &width_policy)
619 .unwrap();
620 let expected_x = layout.content_left + 3.0 * metrics.char_width;
621 assert!((r.x - expected_x).abs() < f64::EPSILON);
622 assert!((r.y - 0.0).abs() < f64::EPSILON);
623 }
624
625 #[test]
626 fn caret_rect_second_line() {
627 let text = "hello\nworld";
628 let li = LineIndex::new(text);
629 let wm = make_wrap_map(text, 80);
630 let metrics = default_metrics();
631 let layout = default_layout();
632 let width_policy = default_width_policy();
633
634 let r = caret_rect_with_width_policy(text, 6, &li, &wm, &metrics, &layout, &width_policy)
636 .unwrap();
637 assert!((r.x - layout.content_left).abs() < f64::EPSILON);
638 assert!((r.y - 20.0).abs() < f64::EPSILON);
639 }
640
641 #[test]
642 fn caret_rect_wrapped_line() {
643 let text = "ab cd ef";
646 let li = LineIndex::new(text);
647 let wm = make_wrap_map(text, 4);
648 let metrics = default_metrics();
649 let layout = default_layout();
650 let width_policy = default_width_policy();
651
652 let r = caret_rect_with_width_policy(text, 4, &li, &wm, &metrics, &layout, &width_policy)
654 .unwrap();
655 let expected_x = layout.content_left + 1.0 * metrics.char_width;
656 assert!((r.x - expected_x).abs() < f64::EPSILON);
657 assert!((r.y - 20.0).abs() < f64::EPSILON);
658 }
659
660 #[test]
665 fn hit_test_basic() {
666 let text = "hello\nworld";
667 let li = LineIndex::new(text);
668 let wm = make_wrap_map(text, 80);
669 let metrics = default_metrics();
670 let layout = default_layout();
671 let width_policy = default_width_policy();
672
673 let offset = hit_test_with_width_policy(
675 layout.content_left,
676 0.0,
677 0.0,
678 text,
679 &li,
680 &wm,
681 &metrics,
682 &layout,
683 &width_policy,
684 );
685 assert_eq!(offset, 0);
686 }
687
688 #[test]
689 fn hit_test_middle_of_line() {
690 let text = "hello";
691 let li = LineIndex::new(text);
692 let wm = make_wrap_map(text, 80);
693 let metrics = default_metrics();
694 let layout = default_layout();
695 let width_policy = default_width_policy();
696
697 let x = layout.content_left + 2.5 * metrics.char_width;
699 let offset = hit_test_with_width_policy(
700 x,
701 0.0,
702 0.0,
703 text,
704 &li,
705 &wm,
706 &metrics,
707 &layout,
708 &width_policy,
709 );
710 assert_eq!(offset, 3);
711 }
712
713 #[test]
714 fn hit_test_gutter_area_clamps_to_line_start() {
715 let text = "hello\nworld";
716 let li = LineIndex::new(text);
717 let wm = make_wrap_map(text, 80);
718 let metrics = default_metrics();
719 let layout = default_layout();
720 let width_policy = default_width_policy();
721
722 let offset = hit_test_with_width_policy(
724 0.0,
725 25.0,
726 0.0,
727 text,
728 &li,
729 &wm,
730 &metrics,
731 &layout,
732 &width_policy,
733 );
734 assert_eq!(offset, 6); }
736
737 #[test]
738 fn hit_test_wrapped_line() {
739 let text = "ab cd ef";
740 let li = LineIndex::new(text);
741 let wm = make_wrap_map(text, 4);
742 let metrics = default_metrics();
743 let layout = default_layout();
744 let width_policy = default_width_policy();
745
746 let offset = hit_test_with_width_policy(
748 layout.content_left,
749 45.0,
750 0.0,
751 text,
752 &li,
753 &wm,
754 &metrics,
755 &layout,
756 &width_policy,
757 );
758 assert_eq!(offset, 6);
759 }
760
761 #[test]
762 fn hit_test_past_end_of_line() {
763 let text = "hi";
764 let li = LineIndex::new(text);
765 let wm = make_wrap_map(text, 80);
766 let metrics = default_metrics();
767 let layout = default_layout();
768 let width_policy = default_width_policy();
769
770 let offset = hit_test_with_width_policy(
772 layout.content_left + 500.0,
773 0.0,
774 0.0,
775 text,
776 &li,
777 &wm,
778 &metrics,
779 &layout,
780 &width_policy,
781 );
782 assert_eq!(offset, 2);
783 }
784
785 #[test]
790 fn gutter_width_1_line() {
791 let metrics = default_metrics();
792 let w = gutter_width(1, &metrics);
793 assert!((w - 16.0).abs() < f64::EPSILON);
795 }
796
797 #[test]
798 fn gutter_width_10_lines() {
799 let metrics = default_metrics();
800 let w = gutter_width(10, &metrics);
801 assert!((w - 24.0).abs() < f64::EPSILON);
803 }
804
805 #[test]
806 fn gutter_width_100_lines() {
807 let metrics = default_metrics();
808 let w = gutter_width(100, &metrics);
809 assert!((w - 32.0).abs() < f64::EPSILON);
811 }
812
813 #[test]
814 fn gutter_width_1000_lines() {
815 let metrics = default_metrics();
816 let w = gutter_width(1000, &metrics);
817 assert!((w - 40.0).abs() < f64::EPSILON);
819 }
820
821 #[test]
826 fn line_top_zero() {
827 let metrics = default_metrics();
828 assert!((line_top(0, &metrics) - 0.0).abs() < f64::EPSILON);
829 }
830
831 #[test]
832 fn line_top_five() {
833 let metrics = default_metrics();
834 assert!((line_top(5, &metrics) - 100.0).abs() < f64::EPSILON);
835 }
836
837 #[test]
838 fn line_top_ten() {
839 let metrics = default_metrics();
840 assert!((line_top(10, &metrics) - 200.0).abs() < f64::EPSILON);
841 }
842
843 #[test]
848 fn scroll_to_reveal_already_visible() {
849 let text = "aaa\nbbb\nccc\nddd\neee";
850 let li = LineIndex::new(text);
851 let wm = make_wrap_map(text, 80);
852 let metrics = default_metrics();
853
854 let result = scroll_to_reveal(text, 4, 0.0, 100.0, &li, &wm, &metrics).unwrap();
856 assert!(result.is_none());
857 }
858
859 #[test]
860 fn scroll_to_reveal_above_viewport() {
861 let text = "aaa\nbbb\nccc\nddd\neee";
862 let li = LineIndex::new(text);
863 let wm = make_wrap_map(text, 80);
864 let metrics = default_metrics();
865
866 let result = scroll_to_reveal(text, 0, 40.0, 40.0, &li, &wm, &metrics).unwrap();
868 assert_eq!(result, Some(0.0));
869 }
870
871 #[test]
872 fn scroll_to_reveal_below_viewport() {
873 let text = "aaa\nbbb\nccc\nddd\neee";
874 let li = LineIndex::new(text);
875 let wm = make_wrap_map(text, 80);
876 let metrics = default_metrics();
877
878 let result = scroll_to_reveal(text, 16, 0.0, 40.0, &li, &wm, &metrics).unwrap();
881 assert_eq!(result, Some(60.0));
883 }
884
885 #[test]
890 fn selection_rects_empty_selection() {
891 let text = "hello";
892 let li = LineIndex::new(text);
893 let wm = make_wrap_map(text, 80);
894 let metrics = default_metrics();
895 let layout = default_layout();
896 let width_policy = default_width_policy();
897
898 let sel = Selection::cursor(2);
899 let rects = selection_rects_with_width_policy(
900 text,
901 &sel,
902 &li,
903 &wm,
904 &metrics,
905 &layout,
906 &width_policy,
907 )
908 .unwrap();
909 assert!(rects.is_empty());
910 }
911
912 #[test]
913 fn selection_rects_single_line() {
914 let text = "hello";
915 let li = LineIndex::new(text);
916 let wm = make_wrap_map(text, 80);
917 let metrics = default_metrics();
918 let layout = default_layout();
919 let width_policy = default_width_policy();
920
921 let sel = Selection::new(1, 4); let rects = selection_rects_with_width_policy(
923 text,
924 &sel,
925 &li,
926 &wm,
927 &metrics,
928 &layout,
929 &width_policy,
930 )
931 .unwrap();
932 assert_eq!(rects.len(), 1);
933 let r = &rects[0];
934 let expected_x = layout.content_left + 1.0 * metrics.char_width;
935 assert!((r.x - expected_x).abs() < f64::EPSILON);
936 assert!((r.width - 3.0 * metrics.char_width).abs() < f64::EPSILON);
937 }
938
939 #[test]
940 fn selection_rects_multi_line() {
941 let text = "aaa\nbbb\nccc";
942 let li = LineIndex::new(text);
943 let wm = make_wrap_map(text, 80);
944 let metrics = default_metrics();
945 let layout = default_layout();
946 let width_policy = default_width_policy();
947
948 let sel = Selection::new(1, 9); let rects = selection_rects_with_width_policy(
951 text,
952 &sel,
953 &li,
954 &wm,
955 &metrics,
956 &layout,
957 &width_policy,
958 )
959 .unwrap();
960 assert_eq!(rects.len(), 3);
961 }
962
963 #[test]
968 fn roundtrip_caret_hit_test() {
969 let text = "hello\nworld";
970 let li = LineIndex::new(text);
971 let wm = make_wrap_map(text, 80);
972 let metrics = default_metrics();
973 let layout = default_layout();
974 let width_policy = default_width_policy();
975
976 for offset in [0, 3, 5, 6, 9, 11] {
977 let r = caret_rect_with_width_policy(
978 text,
979 offset,
980 &li,
981 &wm,
982 &metrics,
983 &layout,
984 &width_policy,
985 )
986 .unwrap();
987 let got = hit_test_with_width_policy(
989 r.x,
990 r.y,
991 0.0,
992 text,
993 &li,
994 &wm,
995 &metrics,
996 &layout,
997 &width_policy,
998 );
999 assert_eq!(got, offset, "roundtrip failed at offset {offset}");
1000 }
1001 }
1002
1003 #[test]
1004 fn roundtrip_caret_hit_test_wrapped() {
1005 let text = "ab cd ef";
1006 let li = LineIndex::new(text);
1007 let wm = make_wrap_map(text, 4);
1008 let metrics = default_metrics();
1009 let layout = default_layout();
1010 let width_policy = default_width_policy();
1011
1012 for offset in [0, 3, 6] {
1013 let r = caret_rect_with_width_policy(
1014 text,
1015 offset,
1016 &li,
1017 &wm,
1018 &metrics,
1019 &layout,
1020 &width_policy,
1021 )
1022 .unwrap();
1023 let got = hit_test_with_width_policy(
1024 r.x,
1025 r.y,
1026 0.0,
1027 text,
1028 &li,
1029 &wm,
1030 &metrics,
1031 &layout,
1032 &width_policy,
1033 );
1034 assert_eq!(got, offset, "roundtrip failed at offset {offset}");
1035 }
1036 }
1037
1038 #[test]
1043 fn text_width_with_tabs() {
1044 let metrics = default_metrics();
1045 let width_policy = default_width_policy();
1046 let text = "a\tb";
1047 let w = text_width(text, 0, text.len(), &metrics, &width_policy);
1049 assert!((w - 48.0).abs() < f64::EPSILON);
1050 }
1051
1052 #[test]
1053 fn text_width_empty() {
1054 let metrics = default_metrics();
1055 let width_policy = default_width_policy();
1056 let w = text_width("hello", 2, 2, &metrics, &width_policy);
1057 assert!((w - 0.0).abs() < f64::EPSILON);
1058 }
1059
1060 #[test]
1061 fn text_width_uses_measured_cjk_width_without_changing_tabs() {
1062 let metrics = default_metrics();
1063 let width_policy = default_width_policy();
1064 let text = "aあ\tb";
1065
1066 let w = text_width(text, 0, text.len(), &metrics, &width_policy);
1067
1068 assert!((w - 62.0).abs() < f64::EPSILON);
1069 }
1070
1071 #[test]
1072 fn caret_rect_uses_measured_cjk_width() {
1073 let text = "aあ";
1074 let li = LineIndex::new(text);
1075 let wm = make_wrap_map(text, 80);
1076 let metrics = default_metrics();
1077 let layout = default_layout();
1078 let width_policy = WidthPolicy::cjk_grid(4);
1079
1080 let r = caret_rect_with_width_policy(
1081 text,
1082 text.len(),
1083 &li,
1084 &wm,
1085 &metrics,
1086 &layout,
1087 &width_policy,
1088 )
1089 .unwrap();
1090
1091 let expected_x = layout.content_left + 22.0;
1092 assert!((r.x - expected_x).abs() < f64::EPSILON);
1093 }
1094
1095 #[test]
1096 fn hit_test_uses_measured_cjk_width() {
1097 let text = "aあb";
1098 let li = LineIndex::new(text);
1099 let wm = make_wrap_map(text, 80);
1100 let metrics = default_metrics();
1101 let layout = default_layout();
1102 let width_policy = WidthPolicy::cjk_grid(4);
1103
1104 let got = hit_test_with_width_policy(
1105 layout.content_left + 23.0,
1106 0.0,
1107 0.0,
1108 text,
1109 &li,
1110 &wm,
1111 &metrics,
1112 &layout,
1113 &width_policy,
1114 );
1115
1116 assert_eq!(got, "aあ".len());
1117 }
1118
1119 #[test]
1120 fn caret_rect_uses_measured_width_for_cjk_grid() {
1121 let text = "aあ";
1122 let li = LineIndex::new(text);
1123 let wm = make_wrap_map(text, 80);
1124 let metrics = default_metrics();
1125 let layout = default_layout();
1126 let width_policy = WidthPolicy::cjk_grid(4);
1127
1128 let r = caret_rect_with_width_policy(
1129 text,
1130 "aあ".len(),
1131 &li,
1132 &wm,
1133 &metrics,
1134 &layout,
1135 &width_policy,
1136 )
1137 .unwrap();
1138
1139 let expected_x = layout.content_left + metrics.char_width + metrics.cjk_char_width;
1140 assert!((r.x - expected_x).abs() < f64::EPSILON);
1141 }
1142
1143 #[test]
1144 fn visual_line_frame_exposes_visual_layout_space() {
1145 let text = "ab cd";
1146 let li = LineIndex::new(text);
1147 let wm = make_wrap_map(text, 3);
1148 let metrics = default_metrics();
1149 let layout = default_layout();
1150 let width_policy = default_width_policy();
1151
1152 let frame = visual_line_frame(
1153 text,
1154 1,
1155 &li,
1156 &wm,
1157 &metrics,
1158 &layout,
1159 &width_policy,
1160 &default_line_layout_policy(),
1161 )
1162 .unwrap();
1163
1164 assert_eq!(frame.logical_line(), 0);
1165 assert_eq!(frame.visual_line(), 1);
1166 assert_eq!(frame.inline_advance(), 2);
1167 assert_eq!(frame.block_advance(), 1);
1168 assert_eq!(frame.layout_mode(), LayoutMode::HorizontalLtr);
1169 }
1170
1171 #[test]
1172 fn visual_line_frame_uses_line_layout_policy_width_redistribution() {
1173 fn justify_to_max(_line_width: u32, max_width: u32) -> u32 {
1174 max_width
1175 }
1176
1177 let text = "ab";
1178 let li = LineIndex::new(text);
1179 let wm = make_wrap_map(text, 6);
1180 let metrics = default_metrics();
1181 let layout = default_layout();
1182 let width_policy = default_width_policy();
1183 let line_layout_policy = LineLayoutPolicy::new(LayoutMode::HorizontalLtr, justify_to_max);
1184
1185 let frame = visual_line_frame(
1186 text,
1187 0,
1188 &li,
1189 &wm,
1190 &metrics,
1191 &layout,
1192 &width_policy,
1193 &line_layout_policy,
1194 )
1195 .unwrap();
1196
1197 assert_eq!(frame.inline_advance(), 6);
1198 assert_eq!(frame.layout_mode(), LayoutMode::HorizontalLtr);
1199 }
1200}