use std::sync::Arc;
use iced::advanced::graphics::text::Raw as TextRaw;
use iced::advanced::graphics::text::cosmic_text;
use iced::advanced::layout::Layout;
use iced::advanced::mouse;
use iced::advanced::renderer;
use iced::{Color, Point, Rectangle};
use super::theme;
use crate::util::UnwrapPoison;
#[must_use]
pub fn font_metrics() -> cosmic_text::Metrics {
cosmic_text::Metrics::relative(14.0, 1.3)
}
pub(crate) const MAX_HIGHLIGHT_SIZE: usize = 10 * 1024 * 1024;
pub(crate) const GUTTER_FONT_SIZE: f32 = 11.0;
pub(crate) const MAX_VISUAL_LINES_PER_SOURCE: usize = 10_000;
pub(crate) fn with_font_system<R>(f: impl FnOnce(&mut cosmic_text::FontSystem) -> R) -> R {
let mut guard = iced::advanced::graphics::text::font_system()
.write()
.unwrap_poison();
f(guard.raw())
}
pub(crate) fn reshape_and_shape(
buffer: &mut cosmic_text::Buffer,
font_sys: &mut cosmic_text::FontSystem,
scroll_y: Option<f32>,
text_area_width: f32,
text_area_height: f32,
) {
if let Some(scroll_y) = scroll_y {
buffer.set_scroll(cosmic_text::Scroll {
line: 0,
vertical: scroll_y,
horizontal: 0.0,
});
}
buffer.set_size(font_sys, Some(text_area_width), Some(text_area_height));
buffer.shape_until_scroll(font_sys, false);
}
#[expect(clippy::cast_precision_loss)]
pub(crate) fn compute_total_height(
buffer: &mut cosmic_text::Buffer,
font_sys: &mut cosmic_text::FontSystem,
metrics: cosmic_text::Metrics,
) -> f32 {
let mut total_visual_lines: f32 = 0.0;
for i in 0..buffer.lines.len() {
let visual_count = buffer
.line_layout(font_sys, i)
.map_or(1, |ll| ll.len().min(MAX_VISUAL_LINES_PER_SOURCE));
total_visual_lines += visual_count as f32;
}
total_visual_lines * metrics.line_height
}
pub(crate) fn text_area_rect(bounds: Rectangle, padding: f32, gutter_width: f32) -> Rectangle {
let x = bounds.x + padding + gutter_width + 4.0; let y = bounds.y + padding;
let width = (bounds.width - (x - bounds.x) - padding).max(0.0);
let height = (bounds.height - padding * 2.0).max(0.0);
Rectangle {
x,
y,
width,
height,
}
}
pub(crate) fn byte_line_and_start(text: &str, offset: usize) -> (usize, usize) {
let prefix = &text[..offset];
let line = prefix.bytes().filter(|&b| b == b'\n').count();
let line_start = prefix.rfind('\n').map_or(0, |p| p + 1);
(line, line_start)
}
pub(crate) fn cursor_to_buffer_coords(
layout: Layout<'_>,
cursor: mouse::Cursor,
gutter_width: f32,
padding: f32,
) -> Option<(f32, f32)> {
let bounds = layout.bounds();
let pos = cursor.position_in(bounds)?;
let buf_x = pos.x - padding - gutter_width - 4.0;
let buf_y = pos.y - padding;
if buf_x < 0.0 || buf_y < 0.0 {
None
} else {
Some((buf_x, buf_y))
}
}
pub(crate) fn gutter_clip_rect(
bounds: Rectangle,
padding: f32,
gutter_width: f32,
text_area_height: f32,
) -> Rectangle {
Rectangle {
x: bounds.x + padding,
y: bounds.y + padding,
width: gutter_width,
height: text_area_height,
}
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn draw_highlight_background<Renderer>(
renderer: &mut Renderer,
text_clip: Rectangle,
text_x: f32,
text_y: f32,
run: &cosmic_text::LayoutRun,
x_offset: f32,
width: f32,
color: Color,
) where
Renderer: iced::advanced::Renderer,
{
let rect = Rectangle {
x: text_x + x_offset,
y: text_y + run.line_top,
width,
height: run.line_height,
};
if let Some(clipped) = text_clip.intersection(&rect) {
renderer.fill_quad(
renderer::Quad {
bounds: clipped,
border: iced::Border::default(),
..renderer::Quad::default()
},
color,
);
}
}
pub(crate) fn draw_background<Renderer>(renderer: &mut Renderer, bounds: Rectangle)
where
Renderer: iced::advanced::Renderer,
{
renderer.fill_quad(
renderer::Quad {
bounds,
border: iced::Border::default(),
..renderer::Quad::default()
},
theme::BG_BASE,
);
}
pub(crate) fn draw_buffer_text<Renderer>(
renderer: &mut Renderer,
buffer: &Arc<cosmic_text::Buffer>,
position: Point,
clip_bounds: Rectangle,
) where
Renderer: iced::advanced::graphics::text::Renderer,
{
renderer.fill_raw(TextRaw {
buffer: Arc::downgrade(buffer),
position,
color: Color::WHITE,
clip_bounds,
});
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn draw_run_highlights<Renderer, F>(
renderer: &mut Renderer,
buffer: &cosmic_text::Buffer,
text_clip: Rectangle,
text_x: f32,
text_y: f32,
color: Color,
stop_after_first: bool,
visible_y: Option<(f32, f32)>,
mut filter: F,
) where
Renderer: iced::advanced::Renderer,
F: FnMut(&cosmic_text::LayoutRun) -> Option<((usize, usize), (usize, usize))>,
{
for run in buffer.layout_runs() {
if let Some((top, bottom)) = visible_y {
let run_top = text_y + run.line_top;
let run_bottom = run_top + run.line_height;
if run_bottom <= top {
continue;
}
if run_top >= bottom {
break;
}
}
let Some(((start_line, start_idx), (end_line, end_idx))) = filter(&run) else {
continue;
};
let start = cosmic_text::Cursor {
line: start_line,
index: start_idx,
..cosmic_text::Cursor::default()
};
let end = cosmic_text::Cursor {
line: end_line,
index: end_idx,
..cosmic_text::Cursor::default()
};
if let Some((x_offset, width)) = run.highlight(start, end) {
draw_highlight_background(
renderer, text_clip, text_x, text_y, &run, x_offset, width, color,
);
if stop_after_first {
break;
}
}
}
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(crate) fn iced_color_to_cosmic(c: Color) -> cosmic_text::Color {
let r = (c.r * 255.0).round() as u8;
let g = (c.g * 255.0).round() as u8;
let b = (c.b * 255.0).round() as u8;
cosmic_text::Color::rgb(r, g, b)
}
pub(crate) fn push_or_merge<'a>(
text: &'a str,
result: &mut Vec<(&'a str, cosmic_text::Attrs<'a>)>,
new_text: &'a str,
new_attrs: cosmic_text::Attrs<'a>,
) {
if let Some(last) = result.last_mut() {
if last.1 == new_attrs {
let start = (last.0.as_ptr() as usize) - (text.as_ptr() as usize);
let last_end = start + last.0.len();
let new_start = (new_text.as_ptr() as usize) - (text.as_ptr() as usize);
if last_end == new_start {
let end = new_start + new_text.len();
last.0 = &text[start..end];
return;
}
}
}
result.push((new_text, new_attrs));
}
pub(crate) fn fill_rich_spans<'a>(
text: &'a str,
spans: impl IntoIterator<Item = (usize, usize, cosmic_text::Attrs<'a>)>,
base_attrs: &cosmic_text::Attrs<'a>,
) -> Vec<(&'a str, cosmic_text::Attrs<'a>)> {
let mut result: Vec<(&str, cosmic_text::Attrs)> = Vec::new();
let mut byte_pos = 0usize;
for (start, end, attrs) in spans {
if start > byte_pos {
push_or_merge(
text,
&mut result,
&text[byte_pos..start],
base_attrs.clone(),
);
}
if end > start {
push_or_merge(text, &mut result, &text[start..end], attrs);
byte_pos = end;
}
}
if byte_pos < text.len() {
push_or_merge(text, &mut result, &text[byte_pos..], base_attrs.clone());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_or_merge_contiguous_same_attrs_merges() {
let text = "hello world";
let attrs = cosmic_text::Attrs::new();
let mut result = Vec::new();
result.push((&text[0..5], attrs.clone()));
push_or_merge(text, &mut result, &text[5..11], attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "hello world");
}
#[test]
fn push_or_merge_non_contiguous_same_attrs_pushes_separately() {
let text = "hello---world";
let attrs = cosmic_text::Attrs::new();
let mut result = Vec::new();
result.push((&text[0..5], attrs.clone()));
push_or_merge(text, &mut result, &text[8..13], attrs);
assert_eq!(result.len(), 2);
assert_eq!(result[0].0, "hello");
assert_eq!(result[1].0, "world");
}
#[test]
fn push_or_merge_contiguous_different_attrs_pushes_separately() {
let text = "hello world";
let attrs1 = cosmic_text::Attrs::new();
let attrs2 = cosmic_text::Attrs::new().color(cosmic_text::Color::rgb(255, 0, 0));
let mut result = Vec::new();
result.push((&text[0..5], attrs1));
push_or_merge(text, &mut result, &text[5..11], attrs2);
assert_eq!(result.len(), 2);
}
#[test]
fn push_or_merge_empty_result_pushes() {
let text = "hello";
let attrs = cosmic_text::Attrs::new();
let mut result = Vec::new();
push_or_merge(text, &mut result, &text[0..5], attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "hello");
}
}