nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
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))
}