Add syntax highlighing feature to `egui_extras` (#3333)
* Add syntax highlighing feature to egui_extras Enable "syntect" feature for great syntax highlighting of any language. If not a simple fallback is used that works fine for C++, Rust, Python * Check --no-default-features of egui_extras on CI * spelling * Fix building egui_extras without additional features
This commit is contained in:
parent
4b5146d35d
commit
5e785ae00a
|
|
@ -49,7 +49,7 @@ jobs:
|
||||||
run: cargo check --locked --all-features --all-targets
|
run: cargo check --locked --all-features --all-targets
|
||||||
|
|
||||||
- name: check egui_extras --all-features
|
- name: check egui_extras --all-features
|
||||||
run: cargo check --locked --all-features --all-targets -p egui_extras
|
run: cargo check --locked --all-features -p egui_extras
|
||||||
|
|
||||||
- name: check default features
|
- name: check default features
|
||||||
run: cargo check --locked --all-targets
|
run: cargo check --locked --all-targets
|
||||||
|
|
@ -57,11 +57,14 @@ jobs:
|
||||||
- name: check --no-default-features
|
- name: check --no-default-features
|
||||||
run: cargo check --locked --no-default-features --lib --all-targets
|
run: cargo check --locked --no-default-features --lib --all-targets
|
||||||
|
|
||||||
- name: check epaint --no-default-features
|
|
||||||
run: cargo check --locked --no-default-features --lib --all-targets -p epaint
|
|
||||||
|
|
||||||
- name: check eframe --no-default-features
|
- name: check eframe --no-default-features
|
||||||
run: cargo check --locked --no-default-features --features x11 --lib --all-targets -p eframe
|
run: cargo check --locked --no-default-features --features x11 --lib -p eframe
|
||||||
|
|
||||||
|
- name: check egui_extras --no-default-features
|
||||||
|
run: cargo check --locked --no-default-features --lib -p egui_extras
|
||||||
|
|
||||||
|
- name: check epaint --no-default-features
|
||||||
|
run: cargo check --locked --no-default-features --lib -p epaint
|
||||||
|
|
||||||
- name: Test doc-tests
|
- name: Test doc-tests
|
||||||
run: cargo test --doc --all-features
|
run: cargo test --doc --all-features
|
||||||
|
|
|
||||||
|
|
@ -1238,10 +1238,8 @@ dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"egui_plot",
|
"egui_plot",
|
||||||
"enum-map",
|
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"syntect",
|
|
||||||
"unicode_names2",
|
"unicode_names2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1253,12 +1251,14 @@ dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"egui",
|
"egui",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
|
"enum-map",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"puffin",
|
"puffin",
|
||||||
"resvg",
|
"resvg",
|
||||||
"serde",
|
"serde",
|
||||||
|
"syntect",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"usvg",
|
"usvg",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ impl DefaultBytesLoader {
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
.entry(uri.into())
|
.entry(uri.into())
|
||||||
.or_insert_with_key(|uri| {
|
.or_insert_with_key(|_uri| {
|
||||||
let bytes: Bytes = bytes.into();
|
let bytes: Bytes = bytes.into();
|
||||||
|
|
||||||
#[cfg(feature = "log")]
|
#[cfg(feature = "log")]
|
||||||
log::trace!("loaded {} bytes for uri {uri:?}", bytes.len());
|
log::trace!("loaded {} bytes for uri {_uri:?}", bytes.len());
|
||||||
|
|
||||||
bytes
|
bytes
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ image_viewer = ["image", "egui_extras/all-loaders", "rfd"]
|
||||||
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
||||||
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
||||||
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
||||||
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
|
syntect = ["egui_demo_lib/syntect"]
|
||||||
|
|
||||||
glow = ["eframe/glow"]
|
glow = ["eframe/glow"]
|
||||||
wgpu = ["eframe/wgpu", "bytemuck"]
|
wgpu = ["eframe/wgpu", "bytemuck"]
|
||||||
|
|
|
||||||
|
|
@ -223,25 +223,19 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Syntax highlighting:
|
// Syntax highlighting:
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
fn syntax_highlighting(
|
fn syntax_highlighting(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
response: &ehttp::Response,
|
response: &ehttp::Response,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Option<ColoredText> {
|
) -> Option<ColoredText> {
|
||||||
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
||||||
let extension = extension_and_rest.get(0)?;
|
let extension = extension_and_rest.first()?;
|
||||||
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
|
let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(&ctx.style());
|
||||||
Some(ColoredText(crate::syntax_highlighting::highlight(
|
Some(ColoredText(egui_extras::syntax_highlighting::highlight(
|
||||||
ctx, &theme, text, extension,
|
ctx, &theme, text, extension,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option<ColoredText> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ColoredText(egui::text::LayoutJob);
|
struct ColoredText(egui::text::LayoutJob);
|
||||||
|
|
||||||
impl ColoredText {
|
impl ColoredText {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ chrono = ["egui_extras/datepicker", "dep:chrono"]
|
||||||
serde = ["egui/serde", "egui_plot/serde", "dep:serde"]
|
serde = ["egui/serde", "egui_plot/serde", "dep:serde"]
|
||||||
|
|
||||||
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
|
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
|
||||||
syntax_highlighting = ["syntect"]
|
syntect = ["egui_extras/syntect"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
@ -37,7 +37,6 @@ egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
|
||||||
"log",
|
"log",
|
||||||
] }
|
] }
|
||||||
egui_plot = { version = "0.22.0", path = "../egui_plot" }
|
egui_plot = { version = "0.22.0", path = "../egui_plot" }
|
||||||
enum-map = { version = "2", features = ["serde"] }
|
|
||||||
log = { version = "0.4", features = ["std"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
unicode_names2 = { version = "0.6.0", default-features = false }
|
unicode_names2 = { version = "0.6.0", default-features = false }
|
||||||
|
|
||||||
|
|
@ -46,9 +45,6 @@ chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] }
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
serde = { version = "1", optional = true, features = ["derive"] }
|
serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
syntect = { version = "5", optional = true, default-features = false, features = [
|
|
||||||
"default-fancy",
|
|
||||||
] }
|
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ impl super::View for About {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn about_immediate_mode(ui: &mut egui::Ui) {
|
fn about_immediate_mode(ui: &mut egui::Ui) {
|
||||||
use crate::syntax_highlighting::code_view_ui;
|
|
||||||
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
|
@ -56,7 +55,7 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
code_view_ui(
|
crate::rust_view_ui(
|
||||||
ui,
|
ui,
|
||||||
r#"
|
r#"
|
||||||
if ui.button("Save").clicked() {
|
if ui.button("Save").clicked() {
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ impl super::View for CodeEditor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||||
ui.collapsing("Theme", |ui| {
|
ui.collapsing("Theme", |ui| {
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
theme.ui(ui);
|
theme.ui(ui);
|
||||||
|
|
@ -77,7 +77,7 @@ impl super::View for CodeEditor {
|
||||||
|
|
||||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
let mut layout_job =
|
let mut layout_job =
|
||||||
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
egui_extras::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||||
layout_job.wrap.max_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,11 @@ impl super::Demo for CodeExample {
|
||||||
|
|
||||||
impl super::View for CodeExample {
|
impl super::View for CodeExample {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
use crate::syntax_highlighting::code_view_ui;
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add(crate::egui_github_link_file!());
|
ui.add(crate::egui_github_link_file!());
|
||||||
});
|
});
|
||||||
|
|
||||||
code_view_ui(
|
crate::rust_view_ui(
|
||||||
ui,
|
ui,
|
||||||
r"
|
r"
|
||||||
pub struct CodeExample {
|
pub struct CodeExample {
|
||||||
|
|
@ -117,15 +115,15 @@ impl CodeExample {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
code_view_ui(ui, " }\n}");
|
crate::rust_view_ui(ui, " }\n}");
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
code_view_ui(ui, &format!("{self:#?}"));
|
crate::rust_view_ui(ui, &format!("{self:#?}"));
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||||
ui.collapsing("Theme", |ui| {
|
ui.collapsing("Theme", |ui| {
|
||||||
theme.ui(ui);
|
theme.ui(ui);
|
||||||
theme.store_in_memory(ui.ctx());
|
theme.store_in_memory(ui.ctx());
|
||||||
|
|
@ -135,7 +133,7 @@ impl CodeExample {
|
||||||
|
|
||||||
fn show_code(ui: &mut egui::Ui, code: &str) {
|
fn show_code(ui: &mut egui::Ui, code: &str) {
|
||||||
let code = remove_leading_indentation(code.trim_start_matches('\n'));
|
let code = remove_leading_indentation(code.trim_start_matches('\n'));
|
||||||
crate::syntax_highlighting::code_view_ui(ui, &code);
|
crate::rust_view_ui(ui, &code);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_leading_indentation(code: &str) -> String {
|
fn remove_leading_indentation(code: &str) -> String {
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,17 @@
|
||||||
mod color_test;
|
mod color_test;
|
||||||
mod demo;
|
mod demo;
|
||||||
pub mod easy_mark;
|
pub mod easy_mark;
|
||||||
pub mod syntax_highlighting;
|
|
||||||
|
|
||||||
pub use color_test::ColorTest;
|
pub use color_test::ColorTest;
|
||||||
pub use demo::DemoWindows;
|
pub use demo::DemoWindows;
|
||||||
|
|
||||||
|
/// View some Rust code with syntax highlighting and selection.
|
||||||
|
pub(crate) fn rust_view_ui(ui: &mut egui::Ui, code: &str) {
|
||||||
|
let language = "rs";
|
||||||
|
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||||
|
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github.
|
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github.
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,13 @@ puffin = ["dep:puffin", "egui/puffin"]
|
||||||
## Support loading svg images.
|
## Support loading svg images.
|
||||||
svg = ["resvg", "tiny-skia", "usvg"]
|
svg = ["resvg", "tiny-skia", "usvg"]
|
||||||
|
|
||||||
|
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
|
||||||
|
syntect = ["dep:syntect"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.22.0", path = "../egui", default-features = false }
|
egui = { version = "0.22.0", path = "../egui", default-features = false }
|
||||||
|
enum-map = { version = "2", features = ["serde"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
|
|
@ -83,6 +87,10 @@ mime_guess = { version = "2.0.4", optional = true, default-features = false }
|
||||||
|
|
||||||
puffin = { version = "0.16", optional = true }
|
puffin = { version = "0.16", optional = true }
|
||||||
|
|
||||||
|
syntect = { version = "5", optional = true, default-features = false, features = [
|
||||||
|
"default-fancy",
|
||||||
|
] }
|
||||||
|
|
||||||
# svg feature
|
# svg feature
|
||||||
resvg = { version = "0.28", optional = true, default-features = false }
|
resvg = { version = "0.28", optional = true, default-features = false }
|
||||||
tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg
|
tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
mod datepicker;
|
mod datepicker;
|
||||||
|
|
||||||
|
pub mod syntax_highlighting;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod image;
|
pub mod image;
|
||||||
mod layout;
|
mod layout;
|
||||||
|
|
|
||||||
|
|
@ -63,21 +63,29 @@ impl BytesLoader for FileLoader {
|
||||||
.spawn({
|
.spawn({
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let cache = self.cache.clone();
|
let cache = self.cache.clone();
|
||||||
let uri = uri.to_owned();
|
let _uri = uri.to_owned();
|
||||||
move || {
|
move || {
|
||||||
let result = match std::fs::read(&path) {
|
let result = match std::fs::read(&path) {
|
||||||
Ok(bytes) => Ok(File {
|
Ok(bytes) => {
|
||||||
bytes: bytes.into(),
|
#[cfg(feature = "mime_guess")]
|
||||||
mime: mime_guess::from_path(&path)
|
let mime = mime_guess::from_path(&path)
|
||||||
.first_raw()
|
.first_raw()
|
||||||
.map(|v| v.to_owned()),
|
.map(|v| v.to_owned());
|
||||||
}),
|
|
||||||
|
#[cfg(not(feature = "mime_guess"))]
|
||||||
|
let mime = None;
|
||||||
|
|
||||||
|
Ok(File {
|
||||||
|
bytes: bytes.into(),
|
||||||
|
mime,
|
||||||
|
})
|
||||||
|
}
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
};
|
};
|
||||||
let prev = cache.lock().insert(path, Poll::Ready(result));
|
let prev = cache.lock().insert(path, Poll::Ready(result));
|
||||||
assert!(matches!(prev, Some(Poll::Pending)));
|
assert!(matches!(prev, Some(Poll::Pending)));
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
crate::log_trace!("finished loading {uri:?}");
|
crate::log_trace!("finished loading {_uri:?}");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect("failed to spawn thread");
|
.expect("failed to spawn thread");
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ impl From<Vec<Size>> for Sizing {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sizing() {
|
fn test_sizing() {
|
||||||
let sizing: Sizing = vec![].into();
|
let sizing: Sizing = vec![].into();
|
||||||
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![]);
|
assert_eq!(sizing.to_lengths(50.0, 0.0), Vec::<f32>::new());
|
||||||
|
|
||||||
let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into();
|
let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into();
|
||||||
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]);
|
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
|
//! Syntax highlighting for code.
|
||||||
|
//!
|
||||||
|
//! Turn on the `syntect` feature for great syntax highlighting of any language.
|
||||||
|
//! Otherwise, a very simple fallback will be used, that works okish for C, C++, Rust, and Python.
|
||||||
|
|
||||||
use egui::text::LayoutJob;
|
use egui::text::LayoutJob;
|
||||||
|
|
||||||
/// View some code with syntax highlighting and selection.
|
/// View some code with syntax highlighting and selection.
|
||||||
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
pub fn code_view_ui(
|
||||||
let language = "rs";
|
ui: &mut egui::Ui,
|
||||||
let theme = CodeTheme::from_memory(ui.ctx());
|
theme: &CodeTheme,
|
||||||
|
mut code: &str,
|
||||||
|
language: &str,
|
||||||
|
) -> egui::Response {
|
||||||
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
||||||
let layout_job = highlight(ui.ctx(), &theme, string, language);
|
let layout_job = highlight(ui.ctx(), theme, string, language);
|
||||||
// layout_job.wrap.max_width = wrap_width; // no wrapping
|
// layout_job.wrap.max_width = wrap_width; // no wrapping
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|
@ -18,10 +25,12 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
||||||
.desired_rows(1)
|
.desired_rows(1)
|
||||||
.lock_focus(true)
|
.lock_focus(true)
|
||||||
.layouter(&mut layouter),
|
.layouter(&mut layouter),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Memoized Code highlighting
|
/// Add syntax highlighting to a code string.
|
||||||
|
///
|
||||||
|
/// The results are memoized, so you can call this every frame without performance penalty.
|
||||||
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
||||||
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
||||||
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
||||||
|
|
@ -118,6 +127,7 @@ impl SyntectTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A selected color theme.
|
||||||
#[derive(Clone, Hash, PartialEq)]
|
#[derive(Clone, Hash, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
|
@ -138,6 +148,7 @@ impl Default for CodeTheme {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodeTheme {
|
impl CodeTheme {
|
||||||
|
/// Selects either dark or light theme based on the given style.
|
||||||
pub fn from_style(style: &egui::Style) -> Self {
|
pub fn from_style(style: &egui::Style) -> Self {
|
||||||
if style.visuals.dark_mode {
|
if style.visuals.dark_mode {
|
||||||
Self::dark()
|
Self::dark()
|
||||||
|
|
@ -146,6 +157,10 @@ impl CodeTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load code theme from egui memory.
|
||||||
|
///
|
||||||
|
/// There is one dark and one light theme stored at any one time.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
pub fn from_memory(ctx: &egui::Context) -> Self {
|
pub fn from_memory(ctx: &egui::Context) -> Self {
|
||||||
if ctx.style().visuals.dark_mode {
|
if ctx.style().visuals.dark_mode {
|
||||||
ctx.data_mut(|d| {
|
ctx.data_mut(|d| {
|
||||||
|
|
@ -160,6 +175,10 @@ impl CodeTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Store theme to egui memory.
|
||||||
|
///
|
||||||
|
/// There is one dark and one light theme stored at any one time.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
pub fn store_in_memory(self, ctx: &egui::Context) {
|
pub fn store_in_memory(self, ctx: &egui::Context) {
|
||||||
if self.dark_mode {
|
if self.dark_mode {
|
||||||
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
|
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
|
||||||
|
|
@ -167,6 +186,36 @@ impl CodeTheme {
|
||||||
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("light"), self));
|
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("light"), self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load code theme from egui memory.
|
||||||
|
///
|
||||||
|
/// There is one dark and one light theme stored at any one time.
|
||||||
|
#[cfg(not(feature = "serde"))]
|
||||||
|
pub fn from_memory(ctx: &egui::Context) -> Self {
|
||||||
|
if ctx.style().visuals.dark_mode {
|
||||||
|
ctx.data_mut(|d| {
|
||||||
|
d.get_temp(egui::Id::new("dark"))
|
||||||
|
.unwrap_or_else(CodeTheme::dark)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ctx.data_mut(|d| {
|
||||||
|
d.get_temp(egui::Id::new("light"))
|
||||||
|
.unwrap_or_else(CodeTheme::light)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store theme to egui memory.
|
||||||
|
///
|
||||||
|
/// There is one dark and one light theme stored at any one time.
|
||||||
|
#[cfg(not(feature = "serde"))]
|
||||||
|
pub fn store_in_memory(self, ctx: &egui::Context) {
|
||||||
|
if self.dark_mode {
|
||||||
|
ctx.data_mut(|d| d.insert_temp(egui::Id::new("dark"), self));
|
||||||
|
} else {
|
||||||
|
ctx.data_mut(|d| d.insert_temp(egui::Id::new("light"), self));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
|
|
@ -185,6 +234,7 @@ impl CodeTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show UI for changing the color theme.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||||
|
|
||||||
|
|
@ -231,11 +281,16 @@ impl CodeTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show UI for changing the color theme.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal_top(|ui| {
|
||||||
let selected_id = egui::Id::null();
|
let selected_id = egui::Id::null();
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
let mut selected_tt: TokenType =
|
let mut selected_tt: TokenType =
|
||||||
ui.data_mut(|d| *d.get_persisted_mut_or(selected_id, TokenType::Comment));
|
ui.data_mut(|d| *d.get_persisted_mut_or(selected_id, TokenType::Comment));
|
||||||
|
#[cfg(not(feature = "serde"))]
|
||||||
|
let mut selected_tt: TokenType =
|
||||||
|
ui.data_mut(|d| *d.get_temp_mut_or(selected_id, TokenType::Comment));
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.set_width(150.0);
|
ui.set_width(150.0);
|
||||||
|
|
@ -277,7 +332,10 @@ impl CodeTheme {
|
||||||
|
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
ui.data_mut(|d| d.insert_persisted(selected_id, selected_tt));
|
ui.data_mut(|d| d.insert_persisted(selected_id, selected_tt));
|
||||||
|
#[cfg(not(feature = "serde"))]
|
||||||
|
ui.data_mut(|d| d.insert_temp(selected_id, selected_tt));
|
||||||
|
|
||||||
egui::Frame::group(ui.style())
|
egui::Frame::group(ui.style())
|
||||||
.inner_margin(egui::Vec2::splat(2.0))
|
.inner_margin(egui::Vec2::splat(2.0))
|
||||||
|
|
@ -306,6 +364,7 @@ struct Highlighter {
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
impl Default for Highlighter {
|
impl Default for Highlighter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
crate::profile_function!();
|
||||||
Self {
|
Self {
|
||||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||||
|
|
@ -313,7 +372,6 @@ impl Default for Highlighter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||||
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
||||||
|
|
@ -332,7 +390,10 @@ impl Highlighter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
use syntect::highlighting::FontStyle;
|
use syntect::highlighting::FontStyle;
|
||||||
use syntect::util::LinesWithEndings;
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
@ -400,13 +461,24 @@ struct Highlighter {}
|
||||||
#[cfg(not(feature = "syntect"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||||
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
|
fn highlight_impl(
|
||||||
|
&self,
|
||||||
|
theme: &CodeTheme,
|
||||||
|
mut text: &str,
|
||||||
|
language: &str,
|
||||||
|
) -> Option<LayoutJob> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
|
let language = Language::new(language)?;
|
||||||
|
|
||||||
// Extremely simple syntax highlighter for when we compile without syntect
|
// Extremely simple syntax highlighter for when we compile without syntect
|
||||||
|
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
|
|
||||||
while !text.is_empty() {
|
while !text.is_empty() {
|
||||||
if text.starts_with("//") {
|
if language.double_slash_comments && text.starts_with("//")
|
||||||
|
|| language.hash_comments && text.starts_with('#')
|
||||||
|
{
|
||||||
let end = text.find('\n').unwrap_or(text.len());
|
let end = text.find('\n').unwrap_or(text.len());
|
||||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
||||||
text = &text[end..];
|
text = &text[end..];
|
||||||
|
|
@ -427,7 +499,7 @@ impl Highlighter {
|
||||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||||
.map_or_else(|| text.len(), |i| i + 1);
|
.map_or_else(|| text.len(), |i| i + 1);
|
||||||
let word = &text[..end];
|
let word = &text[..end];
|
||||||
let tt = if is_keyword(word) {
|
let tt = if language.is_keyword(word) {
|
||||||
TokenType::Keyword
|
TokenType::Keyword
|
||||||
} else {
|
} else {
|
||||||
TokenType::Literal
|
TokenType::Literal
|
||||||
|
|
@ -457,50 +529,173 @@ impl Highlighter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
job
|
Some(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
fn is_keyword(word: &str) -> bool {
|
struct Language {
|
||||||
matches!(
|
/// `// comment`
|
||||||
word,
|
double_slash_comments: bool,
|
||||||
"as" | "async"
|
|
||||||
| "await"
|
/// `# comment`
|
||||||
| "break"
|
hash_comments: bool,
|
||||||
| "const"
|
|
||||||
| "continue"
|
keywords: std::collections::BTreeSet<&'static str>,
|
||||||
| "crate"
|
}
|
||||||
| "dyn"
|
|
||||||
| "else"
|
#[cfg(not(feature = "syntect"))]
|
||||||
| "enum"
|
impl Language {
|
||||||
| "extern"
|
fn new(language: &str) -> Option<Self> {
|
||||||
| "false"
|
match language.to_lowercase().as_str() {
|
||||||
| "fn"
|
"c" | "h" | "hpp" | "cpp" | "c++" => Some(Self::cpp()),
|
||||||
| "for"
|
"py" | "python" => Some(Self::python()),
|
||||||
| "if"
|
"rs" | "rust" => Some(Self::rust()),
|
||||||
| "impl"
|
_ => {
|
||||||
| "in"
|
None // unsupported language
|
||||||
| "let"
|
}
|
||||||
| "loop"
|
}
|
||||||
| "match"
|
}
|
||||||
| "mod"
|
|
||||||
| "move"
|
fn is_keyword(&self, word: &str) -> bool {
|
||||||
| "mut"
|
self.keywords.contains(word)
|
||||||
| "pub"
|
}
|
||||||
| "ref"
|
|
||||||
| "return"
|
fn cpp() -> Self {
|
||||||
| "self"
|
Self {
|
||||||
| "Self"
|
double_slash_comments: true,
|
||||||
| "static"
|
hash_comments: false,
|
||||||
| "struct"
|
keywords: [
|
||||||
| "super"
|
"alignas",
|
||||||
| "trait"
|
"alignof",
|
||||||
| "true"
|
"and_eq",
|
||||||
| "type"
|
"and",
|
||||||
| "unsafe"
|
"asm",
|
||||||
| "use"
|
"atomic_cancel",
|
||||||
| "where"
|
"atomic_commit",
|
||||||
| "while"
|
"atomic_noexcept",
|
||||||
)
|
"auto",
|
||||||
|
"bitand",
|
||||||
|
"bitor",
|
||||||
|
"bool",
|
||||||
|
"break",
|
||||||
|
"case",
|
||||||
|
"catch",
|
||||||
|
"char",
|
||||||
|
"char16_t",
|
||||||
|
"char32_t",
|
||||||
|
"char8_t",
|
||||||
|
"class",
|
||||||
|
"co_await",
|
||||||
|
"co_return",
|
||||||
|
"co_yield",
|
||||||
|
"compl",
|
||||||
|
"concept",
|
||||||
|
"const_cast",
|
||||||
|
"const",
|
||||||
|
"consteval",
|
||||||
|
"constexpr",
|
||||||
|
"constinit",
|
||||||
|
"continue",
|
||||||
|
"decltype",
|
||||||
|
"default",
|
||||||
|
"delete",
|
||||||
|
"do",
|
||||||
|
"double",
|
||||||
|
"dynamic_cast",
|
||||||
|
"else",
|
||||||
|
"enum",
|
||||||
|
"explicit",
|
||||||
|
"export",
|
||||||
|
"extern",
|
||||||
|
"false",
|
||||||
|
"float",
|
||||||
|
"for",
|
||||||
|
"friend",
|
||||||
|
"goto",
|
||||||
|
"if",
|
||||||
|
"inline",
|
||||||
|
"int",
|
||||||
|
"long",
|
||||||
|
"mutable",
|
||||||
|
"namespace",
|
||||||
|
"new",
|
||||||
|
"noexcept",
|
||||||
|
"not_eq",
|
||||||
|
"not",
|
||||||
|
"nullptr",
|
||||||
|
"operator",
|
||||||
|
"or_eq",
|
||||||
|
"or",
|
||||||
|
"private",
|
||||||
|
"protected",
|
||||||
|
"public",
|
||||||
|
"reflexpr",
|
||||||
|
"register",
|
||||||
|
"reinterpret_cast",
|
||||||
|
"requires",
|
||||||
|
"return",
|
||||||
|
"short",
|
||||||
|
"signed",
|
||||||
|
"sizeof",
|
||||||
|
"static_assert",
|
||||||
|
"static_cast",
|
||||||
|
"static",
|
||||||
|
"struct",
|
||||||
|
"switch",
|
||||||
|
"synchronized",
|
||||||
|
"template",
|
||||||
|
"this",
|
||||||
|
"thread_local",
|
||||||
|
"throw",
|
||||||
|
"true",
|
||||||
|
"try",
|
||||||
|
"typedef",
|
||||||
|
"typeid",
|
||||||
|
"typename",
|
||||||
|
"union",
|
||||||
|
"unsigned",
|
||||||
|
"using",
|
||||||
|
"virtual",
|
||||||
|
"void",
|
||||||
|
"volatile",
|
||||||
|
"wchar_t",
|
||||||
|
"while",
|
||||||
|
"xor_eq",
|
||||||
|
"xor",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn python() -> Self {
|
||||||
|
Self {
|
||||||
|
double_slash_comments: false,
|
||||||
|
hash_comments: true,
|
||||||
|
keywords: [
|
||||||
|
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else",
|
||||||
|
"except", "False", "finally", "for", "from", "global", "if", "import", "in", "is",
|
||||||
|
"lambda", "None", "nonlocal", "not", "or", "pass", "raise", "return", "True",
|
||||||
|
"try", "while", "with", "yield",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rust() -> Self {
|
||||||
|
Self {
|
||||||
|
double_slash_comments: true,
|
||||||
|
hash_comments: false,
|
||||||
|
keywords: [
|
||||||
|
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else",
|
||||||
|
"enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match",
|
||||||
|
"mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
|
||||||
|
"super", "trait", "true", "type", "unsafe", "use", "where", "while",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue