1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT
//! Caret geometry and the IME composition overlay.
//!
//! Split out of `render.rs` (principle #9). The entry point stays in the
//! parent; these are the paint helpers it calls.
use super::*;
impl CodeEditor {
/// The x of a document position inside its visual row, and the row's segment
/// index, for a row that has not been painted (or a wrapped one, which is never
/// measured per character).
///
/// Wrapped rows need two corrections the grid alone cannot express: the row
/// starts at its segment's first character, and a continuation row is indented.
/// Without them the caret on the second segment pointed at `column * cell` from
/// the left edge of the *row*, which is where a character from the first segment
/// was drawn.
pub(super) fn unwrapped_x(&self, line: usize, column: usize) -> (usize, f32) {
let text = self.line_text(line).unwrap_or_default();
let grid = LineGrid::new(&text, self.config.tab_width);
let cell = self.cell_width();
if !self.config.word_wrap {
return (0, grid.column_of_char(column) as f32 * cell);
}
let columns = self.text_columns().max(1);
let (segment, segment_start) = grid.segment_of_char(column, columns);
let first = grid.column_of_char(segment_start);
let indent = if segment == 0 {
0.0
} else {
let indent = grid.leading_indent_columns() as f32 * cell;
indent.min(columns.saturating_sub(4) as f32 * cell)
};
(segment, indent + grid.column_of_char(column).saturating_sub(first) as f32 * cell)
}
/// The visual row the caret is painted on, including its wrap segment.
pub(crate) fn caret_visual_row(&self) -> usize {
self.visual_row_of(self.cursor.head)
}
/// The visual row a document position is painted on.
///
/// The line's first row, plus the position's wrap segment. It is the row the
/// selection rectangle and the caret both need, and deriving it from the shared
/// `unwrapped_x` keeps it consistent with the x those two get.
pub(crate) fn visual_row_of(&self, position: TextPosition) -> usize {
let first = self.line_to_visual_row(position.line);
if !self.config.word_wrap {
return first;
}
first + self.unwrapped_x(position.line, position.column).0
}
/// The gutter label for `line`.
///
/// Extracted so the numbering rule has a direct test: the paint path emits SVG
/// paths, so a test cannot read the number back out of the output.
pub(crate) fn line_number_label(&self, line: usize) -> String {
if self.config.relative_line_numbers && line != self.cursor.head.line {
format!("{}", line.abs_diff(self.cursor.head.line))
} else {
format!("{}", line + 1)
}
}
/// The grid column's x inside the row, measured when the row was painted.
///
/// The caret, the selection outline and the diagnostic underline all go through
/// this one function, so the cursor cannot point between two glyphs while the
/// highlight covers them.
pub(super) fn caret_x(&self, position: TextPosition) -> i32 {
let left = self.geometry().x + self.text_origin_x();
if let Some(x) = self.cached_x(position.line, position.column) {
return self.screen_x(left, x);
}
let (_, x) = self.unwrapped_x(position.line, position.column);
self.screen_x(left, x)
}
pub(super) fn draw_caret(
&self,
context: &mut RenderContext,
row_height: f32,
chrome: &EditorChrome,
) {
// Caret geometry comes from the same map the text rows used, so a caret can
// no longer be one rounding step away from the glyph it is supposed to sit
// beside.
let map = self.coordinate_map();
// Secondary carets first so the primary caret paints on top. A collapsed secondary caret is
// the same blinking insertion point as the primary one, so it follows the same phase — two
// carets flickering out of step would look like a rendering fault.
let all_carets = self.cursors();
for caret in all_carets.carets().iter().copied() {
if caret == self.cursor {
continue;
}
if caret.is_collapsed() {
if !self.caret_blink.is_visible() {
continue;
}
let row = self.line_to_visual_row(caret.head.line)
+ if self.config.word_wrap {
self.unwrapped_x(caret.head.line, caret.head.column).0
} else {
0
};
let y = map.row_y(row);
let x = self.caret_x(caret.head);
context.fill_rect(Rect::new(x, y, 2, row_height.ceil() as u32), chrome.accent);
} else if let Some(rect) = self.rect_for_range(caret.bounds().0, caret.bounds().1) {
context.fill_rect(rect, chrome.selection);
}
}
// Primary caret. A selection is not a caret, so the blink gates only the collapsed form: a
// selected range must stay fully visible in both halves of the cycle. A non-collapsed
// selection needs no work here at all: it is drawn as a filled band per row in
// `draw_row_text`, and an extra outline around its first row read as a second, unrelated
// box when the selection spanned lines.
if self.cursor.is_collapsed() && self.caret_blink.is_visible() {
let row = self.caret_visual_row();
let y = map.row_y(row);
let x = self.caret_x(self.cursor.head);
context.fill_rect(Rect::new(x, y, 2, row_height.ceil() as u32), chrome.accent);
}
}
/// Paints the in-progress IME composition at the caret.
///
/// # Why the editor draws it at all
///
/// A composition string is not in the document: the input method owns it until
/// the commit arrives. The previous editor dropped `ImePreedit` entirely, so a
/// Chinese/Japanese/Korean user saw an empty caret while composing — the text
/// only appeared once a candidate was committed, and a mis-typed candidate could
/// not be corrected before it landed in the buffer.
///
/// The overlay is painted after the rows and with an **opaque** surface behind it:
/// the characters that follow the caret are still laid out where they were, so a
/// translucent composition would sit on top of them. An opaque band, an accent
/// underline and a caret marker at the composition's own cursor is the same
/// three-part shape macOS and Zed show.
pub(super) fn draw_preedit(
&self,
context: &mut RenderContext,
rect: Rect,
chrome: &EditorChrome,
) {
let Some(preedit) = self.preedit.as_ref() else {
return;
};
if preedit.text.is_empty() {
return;
}
let area_left = rect.x + self.text_origin_x();
let area_top = rect.y + self.text_origin_y();
let area_right = rect.x + rect.width as i32 - self.scrollbar_width() - self.minimap_width();
let area_bottom = rect.y + rect.height as i32 - self.status_bar_height();
if area_right <= area_left || area_bottom <= area_top {
return;
}
let map = self.coordinate_map();
let row = self.caret_visual_row();
let y = map.row_y(row);
let x = self.caret_x(self.cursor.head);
let row_height = self.line_height().ceil() as u32;
let font = self.editor_font();
let width = context.measure_text(&preedit.text, &font).width as i32;
if width <= 0 {
return;
}
// The composition may start left of the text area when the caret is scrolled
// under the gutter. Clipping (rather than clamping the origin) keeps it
// attached to the caret it belongs to.
context.push_clip(
area_left,
area_top,
(area_right - area_left) as u32,
(area_bottom - area_top) as u32,
);
let band = Rect::new(x, y, width as u32, row_height);
context.fill_rect(band, chrome.surface);
// A hairline frame on the leading and trailing edges tells the reader where
// the composition begins and ends even when the characters themselves are
// narrow (or drawn in a face that covers them poorly).
context.draw_line(Point::new(x, y), Point::new(x, y + row_height as i32), chrome.accent);
let baseline = context.text_line(band, &font).y;
context.draw_text(
Point::new(x, baseline),
&preedit.text,
&font,
chrome.ink,
HorizontalAlignment::Left,
);
// The composition's own cursor, at the byte offset the IME reported. It is
// clamped to a character boundary: a byte in the middle of a CJK code point
// would otherwise slice the string and panic.
let cursor_byte =
floor_char_boundary(&preedit.text, preedit.cursor.min(preedit.text.len()));
let cursor = context.measure_text(&preedit.text[..cursor_byte], &font).width as i32;
context.fill_rect(Rect::new(x + cursor, y, 2, row_height), chrome.accent);
// Underline last, so it also marks the space under the cursor marker.
context.fill_rect(Rect::new(x, y + row_height as i32 - 2, width as u32, 1), chrome.accent);
context.pop_clip();
}
}