use egui::text::{LayoutJob, LayoutSection};
use std::ops::Range;
pub struct SteelView<'a> {
code: &'a str,
highlights: &'a [Range<usize>],
errors: &'a [Range<usize>],
scroll_to: Option<usize>,
}
impl<'a> SteelView<'a> {
pub fn new(code: &'a str) -> Self {
Self {
code,
highlights: &[],
errors: &[],
scroll_to: None,
}
}
pub fn highlights(mut self, ranges: &'a [Range<usize>]) -> Self {
self.highlights = ranges;
self
}
pub fn errors(mut self, ranges: &'a [Range<usize>]) -> Self {
self.errors = ranges;
self
}
pub fn scroll_to(mut self, offset: Option<usize>) -> Self {
self.scroll_to = offset;
self
}
pub fn show(self, ui: &mut egui::Ui) -> egui::Response {
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style());
let mut job = egui_extras::syntax_highlighting::highlight(
ui.ctx(),
ui.style(),
&theme,
self.code,
"scm",
);
let highlight_bg = ui.visuals().selection.bg_fill.gamma_multiply(0.4);
let error_bg = ui.visuals().error_fg_color.gamma_multiply(0.3);
apply_background(&mut job, self.highlights, highlight_bg);
apply_background(&mut job, self.errors, error_bg);
job.wrap.max_width = f32::INFINITY;
let galley = ui.painter().layout_job(job);
let response = ui.add(egui::Label::new(galley.clone()).selectable(true));
if let Some(offset) = self.scroll_to {
let chars = self.code[..offset.min(self.code.len())].chars().count();
let row = galley.pos_from_cursor(egui::text::CCursor::new(chars));
let target = row.translate(response.rect.min.to_vec2());
ui.scroll_to_rect(target, Some(egui::Align::Center));
}
response
}
}
pub(crate) fn apply_background(job: &mut LayoutJob, ranges: &[Range<usize>], color: egui::Color32) {
for range in ranges {
if range.start >= range.end {
continue;
}
let mut sections = Vec::with_capacity(job.sections.len() + 2);
for section in job.sections.drain(..) {
let sr = section.byte_range.clone();
if sr.end <= range.start || range.end <= sr.start {
sections.push(section);
continue;
}
let mid = sr.start.max(range.start)..sr.end.min(range.end);
let mut first = true;
for (bytes, within) in [
(sr.start..mid.start, false),
(mid.clone(), true),
(mid.end..sr.end, false),
] {
if bytes.start >= bytes.end {
continue;
}
let mut format = section.format.clone();
if within {
format.background = color;
}
sections.push(LayoutSection {
leading_space: if first { section.leading_space } else { 0.0 },
byte_range: bytes,
format,
});
first = false;
}
}
job.sections = sections;
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::Color32;
fn job() -> LayoutJob {
let mut job = LayoutJob::default();
job.append("hello", 0.0, Default::default());
job.append(" world!", 0.0, Default::default());
job
}
fn ranges(job: &LayoutJob) -> Vec<(Range<usize>, bool)> {
job.sections
.iter()
.map(|s| {
(
s.byte_range.clone(),
s.format.background != Color32::default(),
)
})
.collect()
}
#[test]
fn splits_sections_at_range_boundaries() {
let mut job = job();
apply_background(&mut job, &[3..8], Color32::YELLOW);
assert_eq!(
ranges(&job),
vec![(0..3, false), (3..5, true), (5..8, true), (8..12, false)]
);
assert_eq!(job.sections.last().unwrap().byte_range.end, job.text.len());
}
#[test]
fn empty_and_out_of_range_are_noops() {
let mut job = job();
apply_background(&mut job, &[4..4, 50..60], Color32::YELLOW);
assert_eq!(ranges(&job), vec![(0..5, false), (5..12, false)]);
}
#[test]
fn overlapping_ranges_compose() {
let mut job = job();
apply_background(&mut job, &[0..4, 2..6], Color32::YELLOW);
let r = ranges(&job);
assert!(r.iter().all(|(range, bg)| *bg == (range.start < 6)));
assert_eq!(r.last().unwrap().0.end, 12);
}
}