use crate::prelude::egui;
#[cfg(not(target_arch = "wasm32"))]
use egui::text::LayoutJob;
#[cfg(not(target_arch = "wasm32"))]
use syntect::easy::HighlightLines;
#[cfg(not(target_arch = "wasm32"))]
use syntect::highlighting::{FontStyle, ThemeSet};
#[cfg(not(target_arch = "wasm32"))]
use syntect::parsing::SyntaxSet;
#[cfg(not(target_arch = "wasm32"))]
use syntect::util::LinesWithEndings;
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Hash, PartialEq)]
enum SyntectTheme {
Base16MochaDark,
SolarizedLight,
}
#[cfg(not(target_arch = "wasm32"))]
impl SyntectTheme {
fn syntect_key_name(&self) -> &'static str {
match self {
Self::Base16MochaDark => "base16-mocha.dark",
Self::SolarizedLight => "Solarized (light)",
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Hash, PartialEq)]
pub struct CodeTheme {
dark_mode: bool,
syntect_theme: SyntectTheme,
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for CodeTheme {
fn default() -> Self {
Self::dark()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl CodeTheme {
pub fn dark() -> Self {
Self {
dark_mode: true,
syntect_theme: SyntectTheme::Base16MochaDark,
}
}
pub fn light() -> Self {
Self {
dark_mode: false,
syntect_theme: SyntectTheme::SolarizedLight,
}
}
pub fn from_memory(ctx: &egui::Context) -> Self {
if ctx.style().visuals.dark_mode {
ctx.data_mut(|d| {
d.get_temp(egui::Id::new("code_theme_dark"))
.unwrap_or_else(CodeTheme::dark)
})
} else {
ctx.data_mut(|d| {
d.get_temp(egui::Id::new("code_theme_light"))
.unwrap_or_else(CodeTheme::light)
})
}
}
}
#[cfg_attr(target_arch = "wasm32", derive(Default))]
pub struct CodeEditor {
#[cfg(not(target_arch = "wasm32"))]
pub language: String,
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for CodeEditor {
fn default() -> Self {
Self {
language: "rs".to_string(),
}
}
}
impl CodeEditor {
#[cfg(not(target_arch = "wasm32"))]
pub fn ui(&self, ui: &mut egui::Ui, code: &mut String) -> egui::Response {
let theme = CodeTheme::from_memory(ui.ctx());
let language = self.language.clone();
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
let mut layout_job = highlight(ui.ctx(), &theme, string.as_str(), &language);
layout_job.wrap.max_width = wrap_width;
ui.fonts_mut(|f| f.layout_job(layout_job))
};
ui.add(
egui::TextEdit::multiline(code)
.font(egui::TextStyle::Monospace)
.code_editor()
.desired_rows(10)
.lock_focus(true)
.desired_width(f32::INFINITY)
.layouter(&mut layouter),
)
}
#[cfg(target_arch = "wasm32")]
pub fn ui(&self, ui: &mut egui::Ui, code: &mut String) -> egui::Response {
ui.add(
egui::TextEdit::multiline(code)
.font(egui::TextStyle::Monospace)
.code_editor()
.desired_rows(10)
.lock_focus(true)
.desired_width(f32::INFINITY),
)
}
}
#[cfg(not(target_arch = "wasm32"))]
struct Highlighter {
ps: SyntaxSet,
ts: ThemeSet,
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for Highlighter {
fn default() -> Self {
Self {
ps: SyntaxSet::load_defaults_newlines(),
ts: ThemeSet::load_defaults(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Highlighter {
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
LayoutJob::simple(
code.into(),
egui::FontId::monospace(12.0),
if theme.dark_mode {
egui::Color32::LIGHT_GRAY
} else {
egui::Color32::DARK_GRAY
},
f32::INFINITY,
)
})
}
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
let syntax = self
.ps
.find_syntax_by_name(language)
.or_else(|| self.ps.find_syntax_by_extension(language))?;
let theme_name = theme.syntect_theme.syntect_key_name();
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme_name]);
let mut job = LayoutJob {
text: text.into(),
..Default::default()
};
for line in LinesWithEndings::from(text) {
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
let fg = style.foreground;
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
let italics = style.font_style.contains(FontStyle::ITALIC);
let underline = if style.font_style.contains(FontStyle::UNDERLINE) {
egui::Stroke::new(1.0, text_color)
} else {
egui::Stroke::NONE
};
job.sections.push(egui::text::LayoutSection {
leading_space: 0.0,
byte_range: as_byte_range(text, range),
format: egui::text::TextFormat {
font_id: egui::FontId::monospace(12.0),
color: text_color,
italics,
underline,
..Default::default()
},
});
}
}
Some(job)
}
}
#[cfg(not(target_arch = "wasm32"))]
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
let whole_start = whole.as_ptr() as usize;
let range_start = range.as_ptr() as usize;
let offset = range_start.saturating_sub(whole_start);
offset..(offset + range.len())
}
#[cfg(not(target_arch = "wasm32"))]
fn highlight(_ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
thread_local! {
static HIGHLIGHTER: std::cell::RefCell<Highlighter> = std::cell::RefCell::new(Highlighter::default());
}
HIGHLIGHTER.with(|h| h.borrow().highlight(theme, code, language))
}