use neco_textview::{LineIndex, Selection, TextViewError};
use neco_wrap::{
LayoutMode, LineLayoutPolicy, VisualLayoutSpace, WidthPolicy, WrapMap, WrapPolicy,
};
use std::fmt;
#[derive(Debug, Clone, Copy)]
pub struct ViewportMetrics {
pub line_height: f64,
pub char_width: f64,
pub cjk_char_width: f64,
pub tab_width: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct ViewportLayout {
pub gutter_width: f64,
pub content_left: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VisualLineFrame {
layout: VisualLayoutSpace,
}
impl VisualLineFrame {
pub const fn logical_line(&self) -> u32 {
self.layout.logical_line()
}
pub const fn visual_line(&self) -> u32 {
self.layout.visual_line()
}
pub const fn inline_advance(&self) -> u32 {
self.layout.inline_advance()
}
pub const fn block_advance(&self) -> u32 {
self.layout.block_advance()
}
pub const fn layout_mode(&self) -> LayoutMode {
self.layout.layout_mode()
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum ViewportError {
TextView(TextViewError),
}
impl fmt::Display for ViewportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TextView(e) => write!(f, "text view error: {e}"),
}
}
}
impl std::error::Error for ViewportError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::TextView(e) => Some(e),
}
}
}
impl From<TextViewError> for ViewportError {
fn from(e: TextViewError) -> Self {
Self::TextView(e)
}
}
fn text_width(
text: &str,
start_byte: usize,
end_byte: usize,
metrics: &ViewportMetrics,
width_policy: &WidthPolicy,
) -> f64 {
let slice = &text[start_byte..end_byte];
slice
.chars()
.map(|ch| char_pixel_width(ch, metrics, width_policy))
.sum()
}
fn char_pixel_width(ch: char, metrics: &ViewportMetrics, width_policy: &WidthPolicy) -> f64 {
let advance = width_policy.advance_of(ch);
if ch == '\t' {
f64::from(advance) * metrics.char_width
} else if advance >= 2 {
metrics.cjk_char_width
} else {
metrics.char_width
}
}
fn u32_to_usize(v: u32) -> usize {
usize::try_from(v).expect("u32 exceeds usize::MAX")
}
pub fn visible_line_range(
scroll_top: f64,
container_height: f64,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
) -> (u32, u32) {
let total = wrap_map.total_visual_lines();
if total == 0 {
return (0, 0);
}
let max_line = total - 1;
let first_f = scroll_top / metrics.line_height;
let first = if first_f < 0.0 {
0u32
} else {
let v = first_f as u64;
u32::try_from(v.min(u64::from(max_line))).expect("clamped value fits u32")
};
let last_f = (scroll_top + container_height) / metrics.line_height;
let last_raw = if last_f < 0.0 {
0u64
} else {
let ceil = last_f.ceil() as u64;
if ceil == 0 {
0
} else {
ceil - 1
}
};
let last = u32::try_from(last_raw.min(u64::from(max_line))).expect("clamped value fits u32");
(first, last)
}
pub fn caret_rect(
text: &str,
offset: usize,
line_index: &LineIndex,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
layout: &ViewportLayout,
) -> Result<Rect, ViewportError> {
caret_rect_with_width_policy(
text,
offset,
line_index,
wrap_map,
metrics,
layout,
&WidthPolicy::cjk_grid(metrics.tab_width),
)
}
pub fn caret_rect_with_width_policy(
text: &str,
offset: usize,
line_index: &LineIndex,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
layout: &ViewportLayout,
width_policy: &WidthPolicy,
) -> Result<Rect, ViewportError> {
let line = line_index.line_of_offset(offset)?;
let line_range = line_index.line_range(line)?;
let byte_in_line = offset - line_range.start();
let byte_in_line_u32 =
u32::try_from(byte_in_line).expect("byte offset in line exceeds u32::MAX");
let visual_line = wrap_map.to_visual_line(line, byte_in_line_u32);
let (_, vl_start_in_line) = wrap_map.from_visual_line(visual_line);
let vl_start_abs = line_range.start() + u32_to_usize(vl_start_in_line);
let x = layout.content_left + text_width(text, vl_start_abs, offset, metrics, width_policy);
let y = f64::from(visual_line) * metrics.line_height;
Ok(Rect {
x,
y,
width: 2.0,
height: metrics.line_height,
})
}
pub fn selection_rects(
text: &str,
selection: &Selection,
line_index: &LineIndex,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
layout: &ViewportLayout,
) -> Result<Vec<Rect>, ViewportError> {
selection_rects_with_width_policy(
text,
selection,
line_index,
wrap_map,
metrics,
layout,
&WidthPolicy::cjk_grid(metrics.tab_width),
)
}
pub fn selection_rects_with_width_policy(
text: &str,
selection: &Selection,
line_index: &LineIndex,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
layout: &ViewportLayout,
width_policy: &WidthPolicy,
) -> Result<Vec<Rect>, ViewportError> {
let range = selection.range();
if range.is_empty() {
return Ok(Vec::new());
}
let start_offset = range.start();
let end_offset = range.end();
let start_line = line_index.line_of_offset(start_offset)?;
let start_line_range = line_index.line_range(start_line)?;
let start_byte_in_line = start_offset - start_line_range.start();
let start_byte_u32 =
u32::try_from(start_byte_in_line).expect("byte offset in line exceeds u32::MAX");
let first_vl = wrap_map.to_visual_line(start_line, start_byte_u32);
let end_line = line_index.line_of_offset(end_offset)?;
let end_line_range = line_index.line_range(end_line)?;
let end_byte_in_line = end_offset - end_line_range.start();
let end_byte_u32 =
u32::try_from(end_byte_in_line).expect("byte offset in line exceeds u32::MAX");
let last_vl = wrap_map.to_visual_line(end_line, end_byte_u32);
let mut rects = Vec::new();
for vl in first_vl..=last_vl {
let (log_line, vl_start_in_line) = wrap_map.from_visual_line(vl);
let lr = line_index.line_range(log_line)?;
let vl_start_abs = lr.start() + u32_to_usize(vl_start_in_line);
let total_vl = wrap_map.total_visual_lines();
let vl_end_abs = if vl + 1 < total_vl {
let (next_log, next_start_in_line) = wrap_map.from_visual_line(vl + 1);
if next_log == log_line {
lr.start() + u32_to_usize(next_start_in_line)
} else {
lr.end()
}
} else {
lr.end()
};
let sel_start = start_offset.max(vl_start_abs);
let sel_end = end_offset.min(vl_end_abs);
if sel_start >= sel_end {
continue;
}
let x =
layout.content_left + text_width(text, vl_start_abs, sel_start, metrics, width_policy);
let w = text_width(text, sel_start, sel_end, metrics, width_policy);
let y = f64::from(vl) * metrics.line_height;
rects.push(Rect {
x,
y,
width: w,
height: metrics.line_height,
});
}
Ok(rects)
}
#[allow(clippy::too_many_arguments)]
pub fn hit_test(
x: f64,
y: f64,
scroll_top: f64,
text: &str,
line_index: &LineIndex,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
layout: &ViewportLayout,
) -> usize {
hit_test_with_width_policy(
x,
y,
scroll_top,
text,
line_index,
wrap_map,
metrics,
layout,
&WidthPolicy::cjk_grid(metrics.tab_width),
)
}
#[allow(clippy::too_many_arguments)]
pub fn hit_test_with_width_policy(
x: f64,
y: f64,
scroll_top: f64,
text: &str,
line_index: &LineIndex,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
layout: &ViewportLayout,
width_policy: &WidthPolicy,
) -> usize {
let total_vl = wrap_map.total_visual_lines();
if total_vl == 0 {
return 0;
}
let vl_f = (y + scroll_top) / metrics.line_height;
let vl_raw = if vl_f < 0.0 { 0u64 } else { vl_f as u64 };
let vl = u32::try_from(vl_raw.min(u64::from(total_vl - 1))).expect("clamped value fits u32");
let (log_line, vl_start_in_line) = wrap_map.from_visual_line(vl);
let lr = match line_index.line_range(log_line) {
Ok(r) => r,
Err(_) => return text.len(),
};
let vl_start_abs = lr.start() + u32_to_usize(vl_start_in_line);
let vl_end_abs = if vl + 1 < total_vl {
let (next_log, next_start_in_line) = wrap_map.from_visual_line(vl + 1);
if next_log == log_line {
lr.start() + u32_to_usize(next_start_in_line)
} else {
lr.end()
}
} else {
lr.end()
};
let rel_x = (x - layout.content_left).max(0.0);
let slice = &text[vl_start_abs..vl_end_abs];
let mut accum = 0.0;
for (i, ch) in slice.char_indices() {
let cw = char_pixel_width(ch, metrics, width_policy);
if rel_x < accum + cw * 0.5 {
return vl_start_abs + i;
}
accum += cw;
}
vl_end_abs
}
pub fn gutter_width(total_lines: u32, metrics: &ViewportMetrics) -> f64 {
let digit_count = total_lines.max(1).ilog10() + 1;
f64::from(digit_count) * metrics.char_width + metrics.char_width
}
pub fn line_top(visual_line: u32, metrics: &ViewportMetrics) -> f64 {
f64::from(visual_line) * metrics.line_height
}
#[allow(clippy::too_many_arguments)]
pub fn visual_line_frame(
text: &str,
visual_line: u32,
line_index: &LineIndex,
wrap_map: &WrapMap,
_metrics: &ViewportMetrics,
_layout: &ViewportLayout,
width_policy: &WidthPolicy,
line_layout_policy: &LineLayoutPolicy,
) -> Result<VisualLineFrame, ViewportError> {
let (logical_line, vl_start_in_line) = wrap_map.from_visual_line(visual_line);
let line_range = line_index.line_range(logical_line)?;
let total_vl = wrap_map.total_visual_lines();
let vl_end_in_line = if visual_line + 1 < total_vl {
let (next_logical_line, next_start_in_line) = wrap_map.from_visual_line(visual_line + 1);
if next_logical_line == logical_line {
next_start_in_line
} else {
u32::try_from(line_range.end() - line_range.start()).expect("line length fits u32")
}
} else {
u32::try_from(line_range.end() - line_range.start()).expect("line length fits u32")
};
let line_text = &text[line_range.start()..line_range.end()];
let local_visual_line = visual_line - wrap_map.to_visual_line(logical_line, 0);
let wrap_policy = WrapPolicy::code_with_width_policy(*width_policy);
let visual_layout = wrap_map.visual_layout_space(
logical_line,
local_visual_line,
line_text,
&wrap_policy,
line_layout_policy,
);
debug_assert_eq!(
visual_layout.inline_advance(),
line_layout_policy.redistributed_inline_width(
width_policy.text_width(
&line_text[u32_to_usize(vl_start_in_line)..u32_to_usize(vl_end_in_line)]
),
wrap_map.max_width(),
)
);
Ok(VisualLineFrame {
layout: visual_layout,
})
}
pub fn scroll_to_reveal(
_text: &str,
offset: usize,
scroll_top: f64,
container_height: f64,
line_index: &LineIndex,
wrap_map: &WrapMap,
metrics: &ViewportMetrics,
) -> Result<Option<f64>, ViewportError> {
let line = line_index.line_of_offset(offset)?;
let line_range = line_index.line_range(line)?;
let byte_in_line = offset - line_range.start();
let byte_in_line_u32 =
u32::try_from(byte_in_line).expect("byte offset in line exceeds u32::MAX");
let vl = wrap_map.to_visual_line(line, byte_in_line_u32);
let top = f64::from(vl) * metrics.line_height;
let bottom = top + metrics.line_height;
if top < scroll_top {
Ok(Some(top))
} else if bottom > scroll_top + container_height {
Ok(Some(bottom - container_height))
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use neco_wrap::{LineLayoutPolicy, WidthPolicy, WrapMap, WrapPolicy};
fn default_metrics() -> ViewportMetrics {
ViewportMetrics {
line_height: 20.0,
char_width: 8.0,
cjk_char_width: 14.0,
tab_width: 4,
}
}
fn default_width_policy() -> WidthPolicy {
WidthPolicy::cjk_grid(4)
}
fn default_line_layout_policy() -> LineLayoutPolicy {
LineLayoutPolicy::horizontal_ltr()
}
fn default_layout() -> ViewportLayout {
ViewportLayout {
gutter_width: 40.0,
content_left: 48.0,
}
}
fn make_wrap_map(text: &str, max_width: u32) -> WrapMap {
let lines: Vec<&str> = text.split('\n').collect();
WrapMap::new(lines.iter().copied(), max_width, &WrapPolicy::code())
}
#[test]
fn visible_line_range_single_line() {
let text = "hello";
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let (first, last) = visible_line_range(0.0, 100.0, &wm, &metrics);
assert_eq!(first, 0);
assert_eq!(last, 0);
}
#[test]
fn visible_line_range_multi_line() {
let text = "aaa\nbbb\nccc\nddd\neee";
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let (first, last) = visible_line_range(0.0, 60.0, &wm, &metrics);
assert_eq!(first, 0);
assert_eq!(last, 2);
}
#[test]
fn visible_line_range_scrolled() {
let text = "aaa\nbbb\nccc\nddd\neee";
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let (first, last) = visible_line_range(40.0, 40.0, &wm, &metrics);
assert_eq!(first, 2);
assert_eq!(last, 3);
}
#[test]
fn visible_line_range_with_wrapping() {
let text = "ab cd ef";
let wm = make_wrap_map(text, 4);
let metrics = default_metrics();
let total = wm.total_visual_lines();
assert_eq!(total, 3);
let (first, last) = visible_line_range(0.0, 60.0, &wm, &metrics);
assert_eq!(first, 0);
assert_eq!(last, 2);
}
#[test]
fn visible_line_range_clamps_past_end() {
let text = "aaa\nbbb";
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let (first, last) = visible_line_range(0.0, 1000.0, &wm, &metrics);
assert_eq!(first, 0);
assert_eq!(last, 1);
}
#[test]
fn caret_rect_line_start() {
let text = "hello\nworld";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let r = caret_rect_with_width_policy(text, 0, &li, &wm, &metrics, &layout, &width_policy)
.unwrap();
assert!((r.x - layout.content_left).abs() < f64::EPSILON);
assert!((r.y - 0.0).abs() < f64::EPSILON);
assert!((r.height - 20.0).abs() < f64::EPSILON);
}
#[test]
fn caret_rect_line_middle() {
let text = "hello\nworld";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let r = caret_rect_with_width_policy(text, 3, &li, &wm, &metrics, &layout, &width_policy)
.unwrap();
let expected_x = layout.content_left + 3.0 * metrics.char_width;
assert!((r.x - expected_x).abs() < f64::EPSILON);
assert!((r.y - 0.0).abs() < f64::EPSILON);
}
#[test]
fn caret_rect_second_line() {
let text = "hello\nworld";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let r = caret_rect_with_width_policy(text, 6, &li, &wm, &metrics, &layout, &width_policy)
.unwrap();
assert!((r.x - layout.content_left).abs() < f64::EPSILON);
assert!((r.y - 20.0).abs() < f64::EPSILON);
}
#[test]
fn caret_rect_wrapped_line() {
let text = "ab cd ef";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 4);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let r = caret_rect_with_width_policy(text, 4, &li, &wm, &metrics, &layout, &width_policy)
.unwrap();
let expected_x = layout.content_left + 1.0 * metrics.char_width;
assert!((r.x - expected_x).abs() < f64::EPSILON);
assert!((r.y - 20.0).abs() < f64::EPSILON);
}
#[test]
fn hit_test_basic() {
let text = "hello\nworld";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let offset = hit_test_with_width_policy(
layout.content_left,
0.0,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(offset, 0);
}
#[test]
fn hit_test_middle_of_line() {
let text = "hello";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let x = layout.content_left + 2.5 * metrics.char_width;
let offset = hit_test_with_width_policy(
x,
0.0,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(offset, 3);
}
#[test]
fn hit_test_gutter_area_clamps_to_line_start() {
let text = "hello\nworld";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let offset = hit_test_with_width_policy(
0.0,
25.0,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(offset, 6); }
#[test]
fn hit_test_wrapped_line() {
let text = "ab cd ef";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 4);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let offset = hit_test_with_width_policy(
layout.content_left,
45.0,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(offset, 6);
}
#[test]
fn hit_test_past_end_of_line() {
let text = "hi";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let offset = hit_test_with_width_policy(
layout.content_left + 500.0,
0.0,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(offset, 2);
}
#[test]
fn gutter_width_1_line() {
let metrics = default_metrics();
let w = gutter_width(1, &metrics);
assert!((w - 16.0).abs() < f64::EPSILON);
}
#[test]
fn gutter_width_10_lines() {
let metrics = default_metrics();
let w = gutter_width(10, &metrics);
assert!((w - 24.0).abs() < f64::EPSILON);
}
#[test]
fn gutter_width_100_lines() {
let metrics = default_metrics();
let w = gutter_width(100, &metrics);
assert!((w - 32.0).abs() < f64::EPSILON);
}
#[test]
fn gutter_width_1000_lines() {
let metrics = default_metrics();
let w = gutter_width(1000, &metrics);
assert!((w - 40.0).abs() < f64::EPSILON);
}
#[test]
fn line_top_zero() {
let metrics = default_metrics();
assert!((line_top(0, &metrics) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn line_top_five() {
let metrics = default_metrics();
assert!((line_top(5, &metrics) - 100.0).abs() < f64::EPSILON);
}
#[test]
fn line_top_ten() {
let metrics = default_metrics();
assert!((line_top(10, &metrics) - 200.0).abs() < f64::EPSILON);
}
#[test]
fn scroll_to_reveal_already_visible() {
let text = "aaa\nbbb\nccc\nddd\neee";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let result = scroll_to_reveal(text, 4, 0.0, 100.0, &li, &wm, &metrics).unwrap();
assert!(result.is_none());
}
#[test]
fn scroll_to_reveal_above_viewport() {
let text = "aaa\nbbb\nccc\nddd\neee";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let result = scroll_to_reveal(text, 0, 40.0, 40.0, &li, &wm, &metrics).unwrap();
assert_eq!(result, Some(0.0));
}
#[test]
fn scroll_to_reveal_below_viewport() {
let text = "aaa\nbbb\nccc\nddd\neee";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let result = scroll_to_reveal(text, 16, 0.0, 40.0, &li, &wm, &metrics).unwrap();
assert_eq!(result, Some(60.0));
}
#[test]
fn selection_rects_empty_selection() {
let text = "hello";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let sel = Selection::cursor(2);
let rects = selection_rects_with_width_policy(
text,
&sel,
&li,
&wm,
&metrics,
&layout,
&width_policy,
)
.unwrap();
assert!(rects.is_empty());
}
#[test]
fn selection_rects_single_line() {
let text = "hello";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let sel = Selection::new(1, 4); let rects = selection_rects_with_width_policy(
text,
&sel,
&li,
&wm,
&metrics,
&layout,
&width_policy,
)
.unwrap();
assert_eq!(rects.len(), 1);
let r = &rects[0];
let expected_x = layout.content_left + 1.0 * metrics.char_width;
assert!((r.x - expected_x).abs() < f64::EPSILON);
assert!((r.width - 3.0 * metrics.char_width).abs() < f64::EPSILON);
}
#[test]
fn selection_rects_multi_line() {
let text = "aaa\nbbb\nccc";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let sel = Selection::new(1, 9); let rects = selection_rects_with_width_policy(
text,
&sel,
&li,
&wm,
&metrics,
&layout,
&width_policy,
)
.unwrap();
assert_eq!(rects.len(), 3);
}
#[test]
fn roundtrip_caret_hit_test() {
let text = "hello\nworld";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
for offset in [0, 3, 5, 6, 9, 11] {
let r = caret_rect_with_width_policy(
text,
offset,
&li,
&wm,
&metrics,
&layout,
&width_policy,
)
.unwrap();
let got = hit_test_with_width_policy(
r.x,
r.y,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(got, offset, "roundtrip failed at offset {offset}");
}
}
#[test]
fn roundtrip_caret_hit_test_wrapped() {
let text = "ab cd ef";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 4);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
for offset in [0, 3, 6] {
let r = caret_rect_with_width_policy(
text,
offset,
&li,
&wm,
&metrics,
&layout,
&width_policy,
)
.unwrap();
let got = hit_test_with_width_policy(
r.x,
r.y,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(got, offset, "roundtrip failed at offset {offset}");
}
}
#[test]
fn text_width_with_tabs() {
let metrics = default_metrics();
let width_policy = default_width_policy();
let text = "a\tb";
let w = text_width(text, 0, text.len(), &metrics, &width_policy);
assert!((w - 48.0).abs() < f64::EPSILON);
}
#[test]
fn text_width_empty() {
let metrics = default_metrics();
let width_policy = default_width_policy();
let w = text_width("hello", 2, 2, &metrics, &width_policy);
assert!((w - 0.0).abs() < f64::EPSILON);
}
#[test]
fn text_width_uses_measured_cjk_width_without_changing_tabs() {
let metrics = default_metrics();
let width_policy = default_width_policy();
let text = "aあ\tb";
let w = text_width(text, 0, text.len(), &metrics, &width_policy);
assert!((w - 62.0).abs() < f64::EPSILON);
}
#[test]
fn caret_rect_uses_measured_cjk_width() {
let text = "aあ";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = WidthPolicy::cjk_grid(4);
let r = caret_rect_with_width_policy(
text,
text.len(),
&li,
&wm,
&metrics,
&layout,
&width_policy,
)
.unwrap();
let expected_x = layout.content_left + 22.0;
assert!((r.x - expected_x).abs() < f64::EPSILON);
}
#[test]
fn hit_test_uses_measured_cjk_width() {
let text = "aあb";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = WidthPolicy::cjk_grid(4);
let got = hit_test_with_width_policy(
layout.content_left + 23.0,
0.0,
0.0,
text,
&li,
&wm,
&metrics,
&layout,
&width_policy,
);
assert_eq!(got, "aあ".len());
}
#[test]
fn caret_rect_uses_measured_width_for_cjk_grid() {
let text = "aあ";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 80);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = WidthPolicy::cjk_grid(4);
let r = caret_rect_with_width_policy(
text,
"aあ".len(),
&li,
&wm,
&metrics,
&layout,
&width_policy,
)
.unwrap();
let expected_x = layout.content_left + metrics.char_width + metrics.cjk_char_width;
assert!((r.x - expected_x).abs() < f64::EPSILON);
}
#[test]
fn visual_line_frame_exposes_visual_layout_space() {
let text = "ab cd";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 3);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let frame = visual_line_frame(
text,
1,
&li,
&wm,
&metrics,
&layout,
&width_policy,
&default_line_layout_policy(),
)
.unwrap();
assert_eq!(frame.logical_line(), 0);
assert_eq!(frame.visual_line(), 1);
assert_eq!(frame.inline_advance(), 2);
assert_eq!(frame.block_advance(), 1);
assert_eq!(frame.layout_mode(), LayoutMode::HorizontalLtr);
}
#[test]
fn visual_line_frame_uses_line_layout_policy_width_redistribution() {
fn justify_to_max(_line_width: u32, max_width: u32) -> u32 {
max_width
}
let text = "ab";
let li = LineIndex::new(text);
let wm = make_wrap_map(text, 6);
let metrics = default_metrics();
let layout = default_layout();
let width_policy = default_width_policy();
let line_layout_policy = LineLayoutPolicy::new(LayoutMode::HorizontalLtr, justify_to_max);
let frame = visual_line_frame(
text,
0,
&li,
&wm,
&metrics,
&layout,
&width_policy,
&line_layout_policy,
)
.unwrap();
assert_eq!(frame.inline_advance(), 6);
assert_eq!(frame.layout_mode(), LayoutMode::HorizontalLtr);
}
}