use crate::ui::theme;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Span {
Words(String),
Code(String),
Heading(String),
}
const TICK: char = '`';
pub fn spans(prose: &str) -> Vec<Span> {
let mut out = Vec::new();
for (n, line) in prose.split('\n').enumerate() {
if n > 0 {
words(&mut out, "\n");
}
if let Some(said) = heading(line) {
out.push(Span::Heading(said.to_owned()));
continue;
}
let mut rest = line;
while let Some((before, code, after)) = ticked(rest) {
words(&mut out, before);
out.push(Span::Code(code.to_owned()));
rest = after;
}
words(&mut out, rest);
}
out
}
fn heading(line: &str) -> Option<&str> {
let rest = line.trim_start_matches('#');
let marks = line.len() - rest.len();
let said = rest.strip_prefix(' ')?;
(1..=6).contains(&marks).then(|| said.trim())
}
fn ticked(line: &str) -> Option<(&str, &str, &str)> {
let (before, inner) = line.split_once(TICK)?;
let (code, after) = inner.split_once(TICK)?;
(!code.is_empty()).then_some((before, code, after))
}
fn words(out: &mut Vec<Span>, said: &str) {
if said.is_empty() {
return;
}
match out.last_mut() {
Some(Span::Words(held)) => held.push_str(said),
_ => out.push(Span::Words(said.to_owned())),
}
}
pub fn job(prose: &str, width: f32, ink: egui::Color32) -> egui::text::LayoutJob {
let face = |size, family| egui::TextFormat {
font_id: egui::FontId::new(size, family),
color: ink,
line_height: Some(theme::type_scale::LEADING),
..egui::TextFormat::default()
};
let body = face(theme::type_scale::BODY, egui::FontFamily::Proportional);
let mut job = egui::text::LayoutJob::default();
for span in spans(prose) {
match span {
Span::Words(said) => job.append(&said, 0.0, body.clone()),
Span::Code(said) => job.append(
&said,
0.0,
egui::TextFormat {
background: theme::SURFACE,
..face(theme::type_scale::MONO, egui::FontFamily::Monospace)
},
),
Span::Heading(said) => job.append(
&said,
0.0,
face(theme::type_scale::HEADING, egui::FontFamily::Proportional),
),
}
}
job.wrap.max_width = width;
job
}
#[cfg(test)]
mod tests;