diff --git a/Cargo.lock b/Cargo.lock index 5b55075..6783465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.2" @@ -225,6 +240,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "cocoa" version = "0.24.0" @@ -633,6 +658,16 @@ dependencies = [ "egui", ] +[[package]] +name = "fancy-regex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "filetime" version = "0.2.15" @@ -645,6 +680,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide 0.4.4", +] + [[package]] name = "fnv" version = "1.0.7" @@ -959,6 +1006,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "jni-sys" version = "0.3.0" @@ -1019,6 +1072,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.101" @@ -1045,6 +1104,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "linon" version = "0.1.0" @@ -1062,6 +1136,7 @@ dependencies = [ "image", "log", "notify", + "syntect", "tobj", "wasm-bindgen", "wasm-bindgen-futures", @@ -1496,6 +1571,20 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "plist" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38d026d73eeaf2ade76309d0c65db5a35ecf649e3cec428db316243ea9d6711" +dependencies = [ + "base64", + "chrono", + "indexmap", + "line-wrap", + "serde", + "xml-rs", +] + [[package]] name = "png" version = "0.16.8" @@ -1645,6 +1734,18 @@ dependencies = [ "owned_ttf_parser 0.6.0", ] +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -1678,6 +1779,28 @@ version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "slab" version = "0.4.4" @@ -1745,6 +1868,28 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syntect" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031" +dependencies = [ + "bincode", + "bitflags", + "fancy-regex", + "flate2", + "fnv", + "lazy_static", + "lazycell", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "walkdir", + "yaml-rust", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -2191,3 +2336,12 @@ name = "xml-rs" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index f940fe0..77ec69b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ tobj = "3.0" egui = "0.15" egui_winit_platform = "0.11" wgpu = "0.11" +syntect = { version = "4", default-features = false, features = ["default-fancy"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] notify = "5.0.0-pre.13" diff --git a/src/application.rs b/src/application.rs index 7c319d3..4611a62 100644 --- a/src/application.rs +++ b/src/application.rs @@ -4,6 +4,7 @@ use crate::cornell_box as cbox; use crate::functions::PredefinedFunction; use crate::main_view::{MainView, Settings}; use crate::reference_view::ReferenceView; +use crate::syntax_highlighting::code_view_ui; use crate::vertices::{get_center, normalize_vertices}; use anyhow::{Context, Result}; use wgpu::util::DeviceExt; @@ -451,7 +452,7 @@ impl Application { }); ui.vertical(|ui| { ui.label("Custom function:"); - if ui.code_editor(field_function).lost_focus() { + if code_view_ui(ui, field_function).lost_focus() { *predefined_function = PredefinedFunction::Custom; field_function_changed = true; } diff --git a/src/main.rs b/src/main.rs index 5b0c14b..ab6e63b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod functions; mod main_view; mod ray_samples; mod reference_view; +mod syntax_highlighting; mod texture; mod vertices; diff --git a/src/syntax_highlighting.rs b/src/syntax_highlighting.rs new file mode 100644 index 0000000..577da88 --- /dev/null +++ b/src/syntax_highlighting.rs @@ -0,0 +1,195 @@ +// Based on https://github.com/emilk/egui/blob/0.15.0/egui_demo_lib/src/syntax_highlighting.rs +// MIT License +use egui::text::LayoutJob; + +/// View some code with syntax highlighing and selection. +pub fn code_view_ui(ui: &mut egui::Ui, code: &mut S) -> egui::Response { + let language = "rs"; + let theme = CodeTheme::from_memory(ui.ctx()); + + let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { + let layout_job = highlight(ui.ctx(), &theme, string, language); + ui.fonts().layout_job(layout_job) + }; + + egui::ScrollArea::vertical().show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(code) + .text_style(egui::TextStyle::Monospace) + .code_editor() + .desired_rows(10) + .lock_focus(true) + .layouter(&mut layouter), + ) + }) +} + +/// Memoized Code highlighting +pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob { + impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highligher { + fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob { + self.highlight(theme, code, lang) + } + } + + type HighlightCache<'a> = egui::util::cache::FrameCache; + + let mut memory = ctx.memory(); + let highlight_cache = memory.caches.cache::>(); + highlight_cache.get((theme, code, language)) +} + +// ---------------------------------------------------------------------------- + +#[derive(Clone, Copy, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +enum SyntectTheme { + Base16MochaDark, + SolarizedLight, +} + +impl SyntectTheme { + fn syntect_key_name(&self) -> &'static str { + match self { + Self::Base16MochaDark => "base16-mocha.dark", + Self::SolarizedLight => "Solarized (light)", + } + } +} + +#[derive(Clone, Copy, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct CodeTheme { + dark_mode: bool, + + syntect_theme: SyntectTheme, +} + +impl Default for CodeTheme { + fn default() -> Self { + Self::dark() + } +} + +impl CodeTheme { + pub fn from_memory(ctx: &egui::Context) -> Self { + if ctx.style().visuals.dark_mode { + *ctx.memory() + .id_data + .get_or_insert_with(egui::Id::new("dark"), CodeTheme::dark) + } else { + *ctx.memory() + .id_data + .get_or_insert_with(egui::Id::new("light"), CodeTheme::light) + } + } +} + +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, + } + } +} + +// ---------------------------------------------------------------------------- + +struct Highligher { + ps: syntect::parsing::SyntaxSet, + ts: syntect::highlighting::ThemeSet, +} + +impl Default for Highligher { + fn default() -> Self { + Self { + ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), + ts: syntect::highlighting::ThemeSet::load_defaults(), + } + } +} + +impl Highligher { + #[allow(clippy::unused_self, clippy::unnecessary_wraps)] + fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob { + self.highlight_impl(theme, code, lang).unwrap_or_else(|| { + // Fallback: + LayoutJob::simple( + code.into(), + egui::TextStyle::Monospace, + 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 { + use syntect::easy::HighlightLines; + use syntect::highlighting::FontStyle; + use syntect::util::LinesWithEndings; + + let syntax = self + .ps + .find_syntax_by_name(language) + .or_else(|| self.ps.find_syntax_by_extension(language))?; + + let theme = theme.syntect_theme.syntect_key_name(); + let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]); + + use egui::text::{LayoutSection, TextFormat}; + + let mut job = LayoutJob { + text: text.into(), + ..Default::default() + }; + + for line in LinesWithEndings::from(text) { + for (style, range) in h.highlight(line, &self.ps) { + 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 = style.font_style.contains(FontStyle::ITALIC); + let underline = if underline { + egui::Stroke::new(1.0, text_color) + } else { + egui::Stroke::none() + }; + job.sections.push(LayoutSection { + leading_space: 0.0, + byte_range: as_byte_range(text, range), + format: TextFormat { + style: egui::TextStyle::Monospace, + color: text_color, + italics, + underline, + ..Default::default() + }, + }); + } + } + + Some(job) + } +} + +fn as_byte_range(whole: &str, range: &str) -> std::ops::Range { + let whole_start = whole.as_ptr() as usize; + let range_start = range.as_ptr() as usize; + assert!(whole_start <= range_start); + assert!(range_start + range.len() <= whole_start + whole.len()); + let offset = range_start - whole_start; + offset..(offset + range.len()) +}