twrite_gpui/editor/
geometry.rs1use gpui::{Pixels, Point, TextRun, Window, point, px};
2use twrite_core::Selection;
3
4use crate::canvas::{RunFonts, build_line_text_runs};
5
6use super::{Editor, VisibleLineLayout};
7
8pub(crate) fn find_visible_line(
12 lines: &[VisibleLineLayout],
13 y: Pixels,
14) -> Option<&VisibleLineLayout> {
15 let idx = lines.partition_point(|l| l.top <= y);
16 let line = lines.get(idx.checked_sub(1)?)?;
17 (y < line.bottom).then_some(line)
18}
19
20impl Editor {
21 pub fn scroll_up(&mut self, count: usize) {
23 self.scroll_row = self.scroll_row.saturating_sub(count);
24 }
25
26 pub fn scroll_down(&mut self, count: usize) {
28 let total_lines = self.buffer.len_lines();
29 self.scroll_row = (self.scroll_row + count).min(total_lines.saturating_sub(1));
30 }
31
32 pub fn move_cursor_to(&mut self, new_offset: usize, select: bool) {
34 if select {
35 let anchor = self
36 .selection
37 .map(|s| s.anchor)
38 .unwrap_or_else(|| self.buffer.cursor_offset());
39 self.buffer.set_cursor_offset(new_offset);
40 if anchor != new_offset {
41 self.selection = Some(Selection::range(anchor, new_offset));
42 } else {
43 self.selection = None;
44 }
45 } else {
46 self.buffer.set_cursor_offset(new_offset);
47 self.selection = None;
48 }
49 }
50
51 pub fn scroll_to_cursor(&mut self, window: Option<&Window>) {
55 let total_lines = self.buffer.len_lines();
56 if total_lines == 0 {
57 self.scroll_row = 0;
58 return;
59 }
60
61 let cursor_row = self
62 .buffer
63 .cursor_point()
64 .row
65 .min(total_lines.saturating_sub(1));
66
67 let margin_lines = 1;
68 if cursor_row < self.scroll_row + margin_lines {
69 self.scroll_row = cursor_row.saturating_sub(margin_lines);
70 return;
71 }
72
73 let bounds = match self.last_bounds {
74 Some(b) => b,
75 None => return,
76 };
77
78 let viewport_height = bounds.size.height;
79 if viewport_height <= px(0.0) {
80 return;
81 }
82
83 let line_height = self.config.line_height;
84 let margin = line_height * margin_lines as f32;
85
86 if self.config.line_wrap
87 && let Some(win) = window
88 {
89 let gutter_width = if self.config.line_numbers {
90 px(48.0)
91 } else {
92 px(0.0)
93 };
94 let wrap_width = Some((bounds.size.width - gutter_width - px(24.0)).max(px(50.0)));
95 let font = self.resolved_base_font(&win.text_style().font());
96
97 let get_row_visual_lines = |row: usize| -> usize {
98 let raw_line = self.buffer.line_to_string(row);
99 let line_text = raw_line.trim_end_matches(['\r', '\n']);
100 if line_text.is_empty() {
101 return 1;
102 }
103 let wraps = self
104 .highlighter
105 .as_deref()
106 .map(|h| h.should_wrap_line(&self.buffer, row))
107 .unwrap_or(true);
108 if !wraps {
109 return 1;
110 }
111 let runs = [TextRun {
112 len: line_text.len(),
113 font: font.clone(),
114 color: self.theme.foreground,
115 background_color: None,
116 underline: None,
117 strikethrough: None,
118 }];
119 win.text_system()
120 .shape_text(
121 line_text.to_string().into(),
122 self.config.font_size,
123 &runs,
124 wrap_width,
125 None,
126 )
127 .ok()
128 .and_then(|mut l| l.pop())
129 .map(|l| l.wrap_boundaries.len() + 1)
130 .unwrap_or(1)
131 };
132
133 let mut accumulated = line_height * get_row_visual_lines(cursor_row) as f32;
134 let mut new_scroll_row = cursor_row;
135
136 while new_scroll_row > 0 {
137 let prev_lines = get_row_visual_lines(new_scroll_row - 1);
138 let prev_height = line_height * prev_lines as f32;
139 if accumulated + prev_height + margin > viewport_height {
140 break;
141 }
142 accumulated += prev_height;
143 new_scroll_row -= 1;
144 }
145
146 if new_scroll_row > self.scroll_row {
147 self.scroll_row = new_scroll_row;
148 }
149 } else {
150 let visible_lines = (viewport_height / line_height).floor() as usize;
151 let effective_visible = visible_lines.saturating_sub(margin_lines).max(1);
152
153 if cursor_row >= self.scroll_row + effective_visible {
154 self.scroll_row = cursor_row.saturating_sub(effective_visible.saturating_sub(1));
155 }
156 }
157 }
158
159 pub fn offset_for_position(&mut self, pos: Point<Pixels>, window: &Window) -> usize {
164 let bounds = match self.last_bounds {
165 Some(b) => b,
166 None => return self.buffer.cursor_offset(),
167 };
168
169 let total_lines = self.buffer.len_lines();
170 if total_lines == 0 {
171 return 0;
172 }
173
174 if !self.visible_lines.is_empty() {
175 if pos.y < self.visible_lines[0].top {
176 return self.visible_lines[0].line_start_byte;
177 }
178
179 let last = self.visible_lines.last().unwrap();
180 if pos.y >= last.bottom {
181 return (last.line_start_byte + last.line_len_bytes).min(self.buffer.len_bytes());
182 }
183
184 let target_line = match find_visible_line(&self.visible_lines, pos.y) {
185 Some(l) => l,
186 None => last,
187 };
188
189 let row = target_line.row;
190 let line_start_byte = target_line.line_start_byte;
191 let task_state = target_line.task_state;
192 let text_origin_x = target_line.text_origin_x;
193 let line_top = target_line.top;
194 let line_height = target_line.line_height;
195 let raw_line = self.buffer.line_to_string(row);
196 let line_text = raw_line.trim_end_matches(['\r', '\n']);
197
198 if line_text.is_empty() || pos.x <= text_origin_x {
199 return line_start_byte;
200 }
201
202 let cursor_row = self.buffer.cursor_point().row;
203 let highlighter_rev = self.highlighter_rev;
204 let host_font = window.text_style().font();
205 let font = self.resolved_base_font(&host_font);
206 let code_font = self.resolved_code_font(&host_font);
207 let cached = self.layout_cache.cached_input(
208 &self.buffer,
209 self.highlighter.as_deref(),
210 highlighter_rev,
211 cursor_row,
212 row,
213 line_text,
214 );
215 let concealed = &cached.concealed;
216
217 let is_checked_task =
218 task_state == Some(true) && line_text.len() != concealed.display_text.len();
219
220 let fonts = RunFonts {
221 base: &font,
222 code: &code_font,
223 };
224 let runs = build_line_text_runs(
225 &concealed.display_text,
226 &concealed.spans,
227 None,
228 &fonts,
229 &self.theme,
230 false,
231 is_checked_task,
232 );
233
234 let wrap_width = if self.config.line_wrap && cached.allow_wrap {
235 let available = bounds.size.width - (text_origin_x - bounds.left()) - px(12.0);
236 Some(available.max(px(50.0)))
237 } else {
238 None
239 };
240
241 let text_line = window
242 .text_system()
243 .shape_text(
244 concealed.display_text.clone().into(),
245 self.config.font_size,
246 &runs,
247 wrap_width,
248 None,
249 )
250 .ok()
251 .and_then(|mut l| l.pop())
252 .unwrap_or_default();
253
254 let line_rel_y = (pos.y - line_top).max(px(0.0));
255 let line_rel_x = (pos.x - text_origin_x).max(px(0.0));
256 let rel_pos = point(line_rel_x, line_rel_y);
257
258 let col_display = text_line
259 .closest_index_for_position(rel_pos, line_height)
260 .unwrap_or_else(|idx| idx);
261
262 let col_src = concealed.display_to_source(col_display);
263 return line_start_byte + col_src.min(line_text.len());
264 }
265
266 0
267 }
268
269 pub fn cursor_pixel_position(&self) -> Option<Point<Pixels>> {
275 self.last_cursor_pixel
276 }
277
278 pub fn link_at_position(&self, pos: Point<Pixels>) -> Option<String> {
280 let bounds = self.last_bounds?;
281 if !bounds.contains(&pos) {
282 return None;
283 }
284
285 if let Some(line) = find_visible_line(&self.visible_lines, pos.y) {
286 for link in &line.links {
287 if link.bounds.contains(&pos) {
288 return Some(link.url.clone());
289 }
290 }
291 }
292
293 None
294 }
295
296 pub(crate) fn is_position_over_task_checkbox(
297 &self,
298 pos: Point<Pixels>,
299 _window: &Window,
300 ) -> bool {
301 let bounds = match self.last_bounds {
302 Some(b) => b,
303 None => return false,
304 };
305
306 if !bounds.contains(&pos) {
307 return false;
308 }
309
310 if let Some(line) = find_visible_line(&self.visible_lines, pos.y)
311 && line.is_task_checkbox
312 {
313 return pos.x >= line.checkbox_box_x && pos.x <= line.checkbox_box_x + px(22.0);
314 }
315
316 false
317 }
318}