ailloli_ui_editor/input/
caret.rs

1//! Caret rectangle construction from shaped run geometry.
2
3use ailloli_ui_core::Rect;
4
5use crate::layout::{first_layout_baseline, layout_visual_height, EditorTextRun};
6use crate::{EditorStyle, EditorViewport};
7
8/// Minimum caret width in logical pixels.
9///
10/// # Examples
11///
12/// ```
13/// use ailloli_ui_editor::input::caret::EDITOR_CARET_WIDTH;
14/// assert_eq!(EDITOR_CARET_WIDTH, 1.0);
15/// ```
16pub const EDITOR_CARET_WIDTH: f32 = 1.0;
17
18/// Resolves one run-local caret rectangle into screen logical pixels.
19///
20/// `local_byte` is clamped to shaped UTF-8 length. Coordinates are rounded;
21/// width is at least one pixel and height at least `px_size + 2`.
22///
23/// # Examples
24///
25/// ```
26/// use ailloli_ui_editor::{input::caret::caret_rect_for_run, layout::EditorTextRun, EditorStyle};
27/// fn caret(run: &EditorTextRun) {
28///     let rect = caret_rect_for_run(0.0, 0.0, run, usize::MAX, EditorStyle::default());
29///     assert!(rect.w >= 1.0);
30/// }
31/// ```
32pub fn caret_rect_for_run(
33    content_x: f32,
34    content_y: f32,
35    run: &EditorTextRun,
36    local_byte: usize,
37    style: EditorStyle,
38) -> Rect {
39    let local = local_byte.min(run.layout.text().len());
40    let caret = run.layout.caret_rect_at(local, EDITOR_CARET_WIDTH);
41    let text_origin_y = run.baseline_y - first_layout_baseline(&run.layout);
42    let fallback_h = style.px_size as f32 + 2.0;
43    Rect::new(
44        (content_x + caret.x).round(),
45        (content_y + text_origin_y + caret.y).round(),
46        caret.w.max(EDITOR_CARET_WIDTH),
47        caret.h.max(fallback_h),
48    )
49}
50
51/// Finds the containing run or returns a fallback caret after the final run.
52///
53/// With no runs, the caret starts at the viewport text origin and has height
54/// `px_size + 2`. Boundary bytes may select the first matching adjacent run.
55///
56/// # Examples
57///
58/// ```
59/// use ailloli_ui_core::Rect;
60/// use ailloli_ui_editor::{input::caret::caret_rect_for_runs, EditorConfig, EditorStyle, EditorViewport};
61/// use ailloli_ui_text::TextEditState;
62/// let viewport = EditorViewport::new(Rect::new(0.0, 0.0, 80.0, 40.0), EditorConfig::default(), &TextEditState::new());
63/// let rect = caret_rect_for_runs(viewport, &[], 0, EditorStyle::default());
64/// assert_eq!((rect.x, rect.y, rect.w, rect.h), (10.0, 10.0, 1.0, 15.0));
65/// ```
66pub fn caret_rect_for_runs(
67    viewport: EditorViewport,
68    runs: &[EditorTextRun],
69    caret_byte: usize,
70    style: EditorStyle,
71) -> Rect {
72    let content_x = viewport.text_origin_x();
73    let content_y = viewport.text_origin_y();
74    for run in runs {
75        if run.byte_range.start <= caret_byte && caret_byte <= run.byte_range.end {
76            let local = caret_byte
77                .saturating_sub(run.byte_range.start)
78                .min(run.layout.text().len());
79            return caret_rect_for_run(content_x, content_y, run, local, style);
80        }
81    }
82    if let Some(run) = runs.last() {
83        return caret_rect_for_run(content_x, content_y, run, run.layout.text().len(), style);
84    }
85    Rect::new(
86        content_x,
87        content_y,
88        EDITOR_CARET_WIDTH,
89        style.px_size as f32 + 2.0,
90    )
91}
92
93/// Returns the shaped visual height clamped by the configured line height.
94///
95/// # Examples
96///
97/// ```
98/// use ailloli_ui_editor::{input::caret::run_height, layout::EditorTextRun, EditorStyle};
99/// fn height(run: &EditorTextRun) {
100///     let value: f32 = run_height(run, EditorStyle::default());
101///     assert!(value >= 18.0);
102/// }
103/// ```
104pub fn run_height(run: &EditorTextRun, style: EditorStyle) -> f32 {
105    layout_visual_height(&run.layout, style)
106}