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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// 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`, as an owned `String`.
///
/// Kept for the numbering-rule test: the paint path emits SVG paths, so a test
/// cannot read the number back out of the output. Production callers go through
/// [`Self::write_line_number_label`] to reuse one buffer across the gutter.
#[cfg(test)]
pub(crate) fn line_number_label(&self, line: usize) -> String {
let mut out = String::new();
self.write_line_number_label(line, &mut out);
out
}
/// Writes the gutter label for `line` into `out`, reusing its capacity.
///
/// The gutter repaints one label per visible row per frame; `format!` gave
/// each row its own heap allocation. Callers clear one buffer before the loop
/// and pass it back every row, so steady-state painting allocates nothing.
pub(crate) fn write_line_number_label(&self, line: usize, out: &mut String) {
use core::fmt::Write as _;
out.clear();
if self.config.relative_line_numbers && line != self.cursor.head.line {
let _ = write!(out, "{}", line.abs_diff(self.cursor.head.line));
} else {
let _ = write!(out, "{}", 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(crate) 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) {
if self.config.word_wrap {
// The cached layout is measured for the whole line, while the
// caret is painted on one segment. Subtract the segment's own
// measured origin and add the continuation indent, the same way
// the row itself is drawn.
let text = self.line_text(position.line).unwrap_or_default();
let grid = LineGrid::new(&text, self.config.tab_width);
let columns = self.text_columns().max(1);
let (segment, segment_start) = grid.segment_of_char(position.column, columns);
let base = self.cached_x(position.line, segment_start).unwrap_or(0.0);
let indent = if segment == 0 {
0.0
} else {
(grid.leading_indent_columns() as f32 * self.cell_width())
.min(columns.saturating_sub(4) as f32 * self.cell_width())
};
return self.screen_x(left, x - base + indent);
}
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);
self.draw_caret_shape(context, x, y, row_height, chrome);
} 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);
self.draw_caret_shape(context, x, y, row_height, chrome);
}
}
/// Paints one caret in the configured shape.
///
/// The default line keeps the previous 2 px bar byte for byte; block and
/// underline are opt-in through [`CaretStyle`].
fn draw_caret_shape(
&self,
context: &mut RenderContext,
x: i32,
y: i32,
row_height: f32,
chrome: &EditorChrome,
) {
let height = row_height.ceil().max(1.0) as u32;
match self.caret_style {
CaretStyle::Line => {
context.fill_rect(Rect::new(x, y, 2, height), chrome.accent);
}
CaretStyle::Block => {
let width = self.cell_width().round().max(2.0) as u32;
context.fill_rect(
Rect::new(x, y, width, height),
chrome.accent.blend(&chrome.surface, 0.45),
);
}
CaretStyle::Underline => {
let width = self.cell_width().round().max(2.0) as u32;
context.fill_rect(
Rect::new(x, y + height.saturating_sub(2) as i32, width, 2),
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();
}
}