use super::editor::CodeEditor;
use crate::core::{Point, Rect};
use crate::widget::Widget;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct CoordinateMap {
bounds: Rect,
origin_x: i32,
origin_y: i32,
cell: f32,
row_height: f32,
scroll_row: usize,
scroll_column: usize,
clip_bottom: i32,
clip_right: i32,
}
impl CodeEditor {
pub(crate) fn coordinate_map(&self) -> CoordinateMap {
let rect = self.geometry();
CoordinateMap {
bounds: rect,
origin_x: rect.x + self.text_origin_x(),
origin_y: rect.y + self.text_origin_y(),
cell: self.cell_width(),
row_height: self.line_height(),
scroll_row: self.scroll_visual_row,
scroll_column: self.scroll_column,
clip_bottom: rect.y + rect.height as i32 - self.status_bar_height(),
clip_right: rect.x + rect.width as i32 - self.scrollbar_width() - self.minimap_width(),
}
}
}
impl CoordinateMap {
pub(crate) fn row_y(&self, visual_row: usize) -> i32 {
self.origin_y
+ ((visual_row as f32 - self.scroll_row as f32) * self.row_height).round() as i32
}
pub(crate) fn column_x(&self, column: usize) -> i32 {
self.origin_x
+ ((column.saturating_sub(self.scroll_column)) as f32 * self.cell).round() as i32
}
pub(crate) fn row_at_y(&self, y: i32) -> usize {
let local = y - self.origin_y;
if local <= 0 {
return self.scroll_row;
}
let offset = (local as f32 / self.row_height).floor() as usize;
self.scroll_row.saturating_add(offset)
}
pub(crate) fn column_at_x(&self, x: i32) -> usize {
let local = x - self.origin_x;
if local <= 0 {
return self.scroll_column;
}
self.scroll_column + (local as f32 / self.cell).round() as usize
}
pub(crate) fn text_area_contains(&self, point: Point) -> bool {
point.x >= self.origin_x
&& point.y >= self.origin_y
&& point.x < self.clip_right
&& point.y < self.clip_bottom
}
pub(crate) fn row_height(&self) -> f32 {
self.row_height
}
pub(crate) fn bounds(&self) -> Rect {
self.bounds
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
fn editor() -> CodeEditor {
CodeEditor::new(Rect::new(0, 0, 400, 300))
}
#[test]
fn the_row_transform_round_trips() {
let map = editor().coordinate_map();
for row in 0..8 {
let y = map.row_y(row);
assert_eq!(map.row_at_y(y), row, "row {row} at y {y} must round-trip");
}
}
#[test]
fn the_column_transform_round_trips() {
let map = editor().coordinate_map();
for column in 0..6 {
let x = map.column_x(column);
assert_eq!(map.column_at_x(x), column, "column {column} at x {x} must round-trip");
}
}
#[test]
fn scrolling_offsets_both_directions() {
let mut editor = CodeEditor::new(Rect::new(0, 0, 400, 120));
let mut text = String::new();
for i in 0..200 {
text.push_str(&format!("line {i}\n"));
}
editor.set_text(text);
editor.base.set_geometry(Rect::new(0, 0, 400, 120));
editor.refresh_visible_rows();
editor.scroll_by(3);
assert_eq!(editor.scroll_line(), 3, "the document is long enough to scroll");
let map = editor.coordinate_map();
assert_eq!(map.row_y(map.scroll_row), map.origin_y);
assert_eq!(map.row_at_y(map.origin_y), map.scroll_row);
}
#[test]
fn the_text_area_predicate_excludes_chrome() {
let editor = editor();
let map = editor.coordinate_map();
let origin_x = map.column_x(0);
let origin_y = map.row_y(map.scroll_row);
assert!(map.text_area_contains(Point::new(origin_x + 5, origin_y + 5)));
assert!(!map.text_area_contains(Point::new(origin_x - 1, origin_y + 5)));
assert!(!map.text_area_contains(Point::new(origin_x + 5, origin_y - 1)));
}
#[test]
fn the_map_keeps_visual_rows_distinct_from_lines() {
let mut editor = CodeEditor::with_config(
Rect::new(0, 0, 120, 300),
super::super::types::CodeEditorConfig::new().word_wrap(true),
)
.expect("valid config");
editor.set_text("x".repeat(400).to_string());
let map = editor.coordinate_map();
assert_eq!(map.row_at_y(map.row_y(0)), 0);
assert_eq!(map.row_at_y(map.row_y(1)), 1);
assert_eq!(editor.visual_row_to_line(0), editor.visual_row_to_line(1));
}
}