use gpui::{Pixels, Point, TextRun, Window, point, px};
use twrite_core::Selection;
use crate::canvas::{LineMetrics, RunFonts, build_line_text_runs};
use super::{Editor, VisibleLineLayout};
pub(crate) fn find_visible_line(
lines: &[VisibleLineLayout],
y: Pixels,
) -> Option<&VisibleLineLayout> {
let idx = lines.partition_point(|l| l.top <= y);
let line = lines.get(idx.checked_sub(1)?)?;
(y < line.bottom).then_some(line)
}
impl Editor {
pub fn scroll_up(&mut self, count: usize) {
self.scroll_row = self.scroll_row.saturating_sub(count);
}
pub fn scroll_down(&mut self, count: usize) {
let total_lines = self.buffer.len_lines();
self.scroll_row = (self.scroll_row + count).min(total_lines.saturating_sub(1));
}
pub fn move_cursor_to(&mut self, new_offset: usize, select: bool) {
if select {
let anchor = self
.selection
.map(|s| s.anchor)
.unwrap_or_else(|| self.buffer.cursor_offset());
self.buffer.set_cursor_offset(new_offset);
if anchor != new_offset {
self.selection = Some(Selection::range(anchor, new_offset));
} else {
self.selection = None;
}
} else {
self.buffer.set_cursor_offset(new_offset);
self.selection = None;
}
}
pub fn scroll_to_cursor(&mut self, window: Option<&Window>) {
let total_lines = self.buffer.len_lines();
if total_lines == 0 {
self.scroll_row = 0;
return;
}
let cursor_row = self
.buffer
.cursor_point()
.row
.min(total_lines.saturating_sub(1));
let margin_lines = 1;
if cursor_row < self.scroll_row + margin_lines {
self.scroll_row = cursor_row.saturating_sub(margin_lines);
return;
}
let bounds = match self.last_bounds {
Some(b) => b,
None => return,
};
let viewport_height = bounds.size.height;
if viewport_height <= px(0.0) {
return;
}
let line_height = self.config.line_height;
let margin = line_height * margin_lines as f32;
if self.config.line_wrap
&& let Some(win) = window
{
let gutter_width = if self.config.line_numbers {
px(48.0)
} else {
px(0.0)
};
let wrap_width = Some((bounds.size.width - gutter_width - px(24.0)).max(px(50.0)));
let font = self.resolved_base_font(&win.text_style().font());
let get_row_visual_lines = |row: usize| -> usize {
let raw_line = self.buffer.line_to_string(row);
let line_text = raw_line.trim_end_matches(['\r', '\n']);
if line_text.is_empty() {
return 1;
}
let wraps = self
.highlighter
.as_deref()
.map(|h| h.should_wrap_line(&self.buffer, row))
.unwrap_or(true);
if !wraps {
return 1;
}
let runs = [TextRun {
len: line_text.len(),
font: font.clone(),
color: self.theme.foreground,
background_color: None,
underline: None,
strikethrough: None,
}];
win.text_system()
.shape_text(
line_text.to_string().into(),
self.config.font_size,
&runs,
wrap_width,
None,
)
.ok()
.and_then(|mut l| l.pop())
.map(|l| l.wrap_boundaries.len() + 1)
.unwrap_or(1)
};
let mut accumulated = line_height * get_row_visual_lines(cursor_row) as f32;
let mut new_scroll_row = cursor_row;
while new_scroll_row > 0 {
let prev_lines = get_row_visual_lines(new_scroll_row - 1);
let prev_height = line_height * prev_lines as f32;
if accumulated + prev_height + margin > viewport_height {
break;
}
accumulated += prev_height;
new_scroll_row -= 1;
}
if new_scroll_row > self.scroll_row {
self.scroll_row = new_scroll_row;
}
} else {
let visible_lines = (viewport_height / line_height).floor() as usize;
let effective_visible = visible_lines.saturating_sub(margin_lines).max(1);
if cursor_row >= self.scroll_row + effective_visible {
self.scroll_row = cursor_row.saturating_sub(effective_visible.saturating_sub(1));
}
}
}
pub fn offset_for_position(&mut self, pos: Point<Pixels>, window: &Window) -> usize {
let bounds = match self.last_bounds {
Some(b) => b,
None => return self.buffer.cursor_offset(),
};
let total_lines = self.buffer.len_lines();
if total_lines == 0 {
return 0;
}
if !self.visible_lines.is_empty() {
if pos.y < self.visible_lines[0].top {
return self.visible_lines[0].line_start_byte;
}
let last = self.visible_lines.last().unwrap();
if pos.y >= last.bottom {
return (last.line_start_byte + last.line_len_bytes).min(self.buffer.len_bytes());
}
let target_line = match find_visible_line(&self.visible_lines, pos.y) {
Some(l) => l,
None => last,
};
let row = target_line.row;
let line_start_byte = target_line.line_start_byte;
let task_state = target_line.task_state;
let text_origin_x = target_line.text_origin_x;
let line_top = target_line.top;
let raw_line = self.buffer.line_to_string(row);
let line_text = raw_line.trim_end_matches(['\r', '\n']);
if line_text.is_empty() || pos.x <= text_origin_x {
return line_start_byte;
}
let base_font_size = self.config.font_size;
let base_line_height = self.config.line_height;
let cursor_row = self.buffer.cursor_point().row;
let highlighter_rev = self.highlighter_rev;
let host_font = window.text_style().font();
let font = self.resolved_base_font(&host_font);
let code_font = self.resolved_code_font(&host_font);
let cached = self.layout_cache.cached_input(
&self.buffer,
self.highlighter.as_deref(),
highlighter_rev,
cursor_row,
row,
line_text,
);
let concealed = &cached.concealed;
let metrics = LineMetrics::for_line(
line_text,
&concealed.display_text,
&cached.spans,
base_font_size,
base_line_height,
);
let is_checked_task =
task_state == Some(true) && line_text.len() != concealed.display_text.len();
let fonts = RunFonts {
base: &font,
code: &code_font,
};
let runs = build_line_text_runs(
&concealed.display_text,
&concealed.spans,
None,
&fonts,
&self.theme,
metrics.is_code_block,
is_checked_task,
);
let wrap_width = if self.config.line_wrap && cached.allow_wrap {
let available = bounds.size.width - (text_origin_x - bounds.left()) - px(12.0);
Some(available.max(px(50.0)))
} else {
None
};
let text_line = window
.text_system()
.shape_text(
concealed.display_text.clone().into(),
metrics.font_size,
&runs,
wrap_width,
None,
)
.ok()
.and_then(|mut l| l.pop())
.unwrap_or_default();
let line_rel_y = (pos.y - line_top).max(px(0.0));
let line_rel_x = (pos.x - text_origin_x).max(px(0.0));
let rel_pos = point(line_rel_x, line_rel_y);
let col_display = text_line
.closest_index_for_position(rel_pos, metrics.line_height)
.unwrap_or_else(|idx| idx);
let col_src = concealed.display_to_source(col_display);
return line_start_byte + col_src.min(line_text.len());
}
0
}
pub fn cursor_pixel_position(&self) -> Option<Point<Pixels>> {
self.last_cursor_pixel
}
pub fn link_at_position(&self, pos: Point<Pixels>) -> Option<String> {
let bounds = self.last_bounds?;
if !bounds.contains(&pos) {
return None;
}
if let Some(line) = find_visible_line(&self.visible_lines, pos.y) {
for link in &line.links {
if link.bounds.contains(&pos) {
return Some(link.url.clone());
}
}
}
None
}
pub(crate) fn is_position_over_task_checkbox(
&self,
pos: Point<Pixels>,
_window: &Window,
) -> bool {
let bounds = match self.last_bounds {
Some(b) => b,
None => return false,
};
if !bounds.contains(&pos) {
return false;
}
if let Some(line) = find_visible_line(&self.visible_lines, pos.y)
&& line.is_task_checkbox
{
return pos.x >= line.checkbox_box_x && pos.x <= line.checkbox_box_x + px(22.0);
}
false
}
}