demo: add syntax_highlighting module with theme editor
This commit is contained in:
parent
0cb1b18a6a
commit
45ab9a2450
4 changed files with 518 additions and 453 deletions
|
@ -1,137 +1,15 @@
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[derive(enum_map::Enum)]
|
|
||||||
enum TokenType {
|
|
||||||
Comment,
|
|
||||||
Keyword,
|
|
||||||
Literal,
|
|
||||||
StringLiteral,
|
|
||||||
Punctuation,
|
|
||||||
Whitespace,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
|
||||||
struct CodeTheme {
|
|
||||||
dark_mode: bool,
|
|
||||||
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CodeTheme {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::dark(egui::TextStyle::Monospace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodeTheme {
|
|
||||||
fn dark(text_style: egui::TextStyle) -> Self {
|
|
||||||
use egui::{Color32, TextFormat};
|
|
||||||
Self {
|
|
||||||
dark_mode: true,
|
|
||||||
formats: enum_map::enum_map![
|
|
||||||
TokenType::Comment => TextFormat::simple(text_style, Color32::from_gray(120)),
|
|
||||||
TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(255, 100, 100)),
|
|
||||||
TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(178, 108, 210)),
|
|
||||||
TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(109, 147, 226)),
|
|
||||||
TokenType::Punctuation => TextFormat::simple(text_style, Color32::LIGHT_GRAY),
|
|
||||||
TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn light(text_style: egui::TextStyle) -> Self {
|
|
||||||
use egui::{Color32, TextFormat};
|
|
||||||
Self {
|
|
||||||
dark_mode: false,
|
|
||||||
formats: enum_map::enum_map![
|
|
||||||
TokenType::Comment => TextFormat::simple(text_style, Color32::GRAY),
|
|
||||||
TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(235, 0, 0)),
|
|
||||||
TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(153, 134, 255)),
|
|
||||||
TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(37, 203, 105)),
|
|
||||||
TokenType::Punctuation => TextFormat::simple(text_style, Color32::DARK_GRAY),
|
|
||||||
TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodeTheme {
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, reset_value: CodeTheme) {
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
let mut selected_tt: TokenType = *ui.memory().data.get_or(TokenType::Comment);
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
|
||||||
|
|
||||||
// ui.separator(); // TODO: fix forever-expand
|
|
||||||
ui.add_space(14.0);
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
for (tt, tt_name) in [
|
|
||||||
(TokenType::Comment, "// comment"),
|
|
||||||
(TokenType::Keyword, "keyword"),
|
|
||||||
(TokenType::Literal, "literal"),
|
|
||||||
(TokenType::StringLiteral, "\"string literal\""),
|
|
||||||
(TokenType::Punctuation, "punctuation ;"),
|
|
||||||
// (TokenType::Whitespace, "whitespace"),
|
|
||||||
] {
|
|
||||||
let format = &mut self.formats[tt];
|
|
||||||
ui.style_mut().override_text_style = Some(format.style);
|
|
||||||
ui.visuals_mut().override_text_color = Some(format.color);
|
|
||||||
ui.radio_value(&mut selected_tt, tt, tt_name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(14.0);
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.add(egui::Button::new("Reset theme").enabled(*self != reset_value))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
*self = reset_value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(16.0);
|
|
||||||
// ui.separator(); // TODO: fix forever-expand
|
|
||||||
|
|
||||||
ui.memory().data.insert(selected_tt);
|
|
||||||
|
|
||||||
egui::Frame::group(ui.style())
|
|
||||||
.margin(egui::Vec2::splat(2.0))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
// ui.group(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
|
|
||||||
ui.spacing_mut().slider_width = 128.0; // Controls color picker size
|
|
||||||
egui::widgets::color_picker::color_picker_color32(
|
|
||||||
ui,
|
|
||||||
&mut self.formats[selected_tt].color,
|
|
||||||
egui::color_picker::Alpha::Opaque,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[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))]
|
||||||
pub struct CodeEditor {
|
pub struct CodeEditor {
|
||||||
theme_dark: CodeTheme,
|
|
||||||
theme_light: CodeTheme,
|
|
||||||
language: String,
|
language: String,
|
||||||
code: String,
|
code: String,
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
|
||||||
highlighter: MemoizedSyntaxHighlighter,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CodeEditor {
|
impl Default for CodeEditor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
theme_dark: CodeTheme::dark(egui::TextStyle::Monospace),
|
|
||||||
theme_light: CodeTheme::light(egui::TextStyle::Monospace),
|
|
||||||
language: "rs".into(),
|
language: "rs".into(),
|
||||||
code: "// A very simple example\n\
|
code: "// A very simple example\n\
|
||||||
fn main() {\n\
|
fn main() {\n\
|
||||||
|
@ -139,7 +17,6 @@ fn main() {\n\
|
||||||
}\n\
|
}\n\
|
||||||
"
|
"
|
||||||
.into(),
|
.into(),
|
||||||
highlighter: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,7 +27,7 @@ impl super::Demo for CodeEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
||||||
use super::View;
|
use super::View as _;
|
||||||
egui::Window::new(self.name())
|
egui::Window::new(self.name())
|
||||||
.open(open)
|
.open(open)
|
||||||
.default_height(500.0)
|
.default_height(500.0)
|
||||||
|
@ -160,13 +37,7 @@ impl super::Demo for CodeEditor {
|
||||||
|
|
||||||
impl super::View for CodeEditor {
|
impl super::View for CodeEditor {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
let Self {
|
let Self { language, code } = self;
|
||||||
theme_dark,
|
|
||||||
theme_light,
|
|
||||||
language,
|
|
||||||
code,
|
|
||||||
highlighter,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.set_height(0.0);
|
ui.set_height(0.0);
|
||||||
|
@ -196,26 +67,17 @@ impl super::View for CodeEditor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||||
ui.collapsing("Theme", |ui| {
|
ui.collapsing("Theme", |ui| {
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
if ui.visuals().dark_mode {
|
theme.ui(ui);
|
||||||
let reset_value = CodeTheme::dark(egui::TextStyle::Monospace);
|
theme.store_in_memory(ui.ctx());
|
||||||
theme_dark.ui(ui, reset_value);
|
|
||||||
} else {
|
|
||||||
let reset_value = CodeTheme::light(egui::TextStyle::Monospace);
|
|
||||||
theme_light.ui(ui, reset_value);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let theme = if ui.visuals().dark_mode {
|
|
||||||
theme_dark
|
|
||||||
} else {
|
|
||||||
theme_light
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = highlighter.highlight(theme, string, language);
|
let mut layout_job =
|
||||||
|
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||||
layout_job.wrap_width = wrap_width;
|
layout_job.wrap_width = wrap_width;
|
||||||
ui.fonts().layout_job(layout_job)
|
ui.fonts().layout_job(layout_job)
|
||||||
};
|
};
|
||||||
|
@ -233,229 +95,3 @@ impl super::View for CodeEditor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
use egui::text::LayoutJob;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct MemoizedSyntaxHighlighter {
|
|
||||||
theme: CodeTheme,
|
|
||||||
code: String,
|
|
||||||
language: String,
|
|
||||||
output: LayoutJob,
|
|
||||||
highligher: Highligher,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoizedSyntaxHighlighter {
|
|
||||||
fn highlight(&mut self, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
|
||||||
if (&self.theme, self.code.as_str(), self.language.as_str()) != (theme, code, language) {
|
|
||||||
self.theme = *theme;
|
|
||||||
self.code = code.to_owned();
|
|
||||||
self.language = language.to_owned();
|
|
||||||
self.output = self
|
|
||||||
.highligher
|
|
||||||
.highlight(theme, code, language)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
LayoutJob::simple(
|
|
||||||
code.into(),
|
|
||||||
egui::TextStyle::Monospace,
|
|
||||||
if theme.dark_mode {
|
|
||||||
egui::Color32::LIGHT_GRAY
|
|
||||||
} else {
|
|
||||||
egui::Color32::DARK_GRAY
|
|
||||||
},
|
|
||||||
f32::INFINITY,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
self.output.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
struct Highligher {
|
|
||||||
ps: syntect::parsing::SyntaxSet,
|
|
||||||
ts: syntect::highlighting::ThemeSet,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl Default for Highligher {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
|
||||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl Highligher {
|
|
||||||
fn highlight(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
|
||||||
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 = if theme.dark_mode {
|
|
||||||
"base16-mocha.dark"
|
|
||||||
} else {
|
|
||||||
"base16-ocean.light"
|
|
||||||
};
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
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;
|
|
||||||
assert!(whole_start <= range_start);
|
|
||||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
|
||||||
let offset = range_start - whole_start;
|
|
||||||
offset..(offset + range.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Highligher {}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
impl Highligher {
|
|
||||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
|
||||||
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> Option<LayoutJob> {
|
|
||||||
// Extremely simple syntax highlighter for when we compile without syntect
|
|
||||||
|
|
||||||
let mut job = LayoutJob::default();
|
|
||||||
|
|
||||||
while !text.is_empty() {
|
|
||||||
if text.starts_with("//") {
|
|
||||||
let end = text.find('\n').unwrap_or_else(|| text.len());
|
|
||||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment]);
|
|
||||||
text = &text[end..];
|
|
||||||
} else if text.starts_with('"') {
|
|
||||||
let end = text[1..]
|
|
||||||
.find('"')
|
|
||||||
.map(|i| i + 2)
|
|
||||||
.or_else(|| text.find('\n'))
|
|
||||||
.unwrap_or_else(|| text.len());
|
|
||||||
job.append(&text[..end], 0.0, theme.formats[TokenType::StringLiteral]);
|
|
||||||
text = &text[end..];
|
|
||||||
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
|
||||||
let end = text[1..]
|
|
||||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
|
||||||
.map(|i| i + 1)
|
|
||||||
.unwrap_or_else(|| text.len());
|
|
||||||
let word = &text[..end];
|
|
||||||
let tt = if is_keyword(word) {
|
|
||||||
TokenType::Keyword
|
|
||||||
} else {
|
|
||||||
TokenType::Literal
|
|
||||||
};
|
|
||||||
job.append(word, 0.0, theme.formats[tt]);
|
|
||||||
text = &text[end..];
|
|
||||||
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
|
|
||||||
let end = text[1..]
|
|
||||||
.find(|c: char| !c.is_ascii_whitespace())
|
|
||||||
.map(|i| i + 1)
|
|
||||||
.unwrap_or_else(|| text.len());
|
|
||||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Whitespace]);
|
|
||||||
text = &text[end..];
|
|
||||||
} else {
|
|
||||||
let mut it = text.char_indices();
|
|
||||||
it.next();
|
|
||||||
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
|
|
||||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Punctuation]);
|
|
||||||
text = &text[end..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
fn is_keyword(word: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
word,
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct Resource {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource {
|
impl Resource {
|
||||||
fn from_response(response: ehttp::Response) -> Self {
|
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
||||||
let content_type = response.content_type().unwrap_or_default();
|
let content_type = response.content_type().unwrap_or_default();
|
||||||
let image = if content_type.starts_with("image/") {
|
let image = if content_type.starts_with("image/") {
|
||||||
Image::decode(&response.bytes)
|
Image::decode(&response.bytes)
|
||||||
|
@ -26,7 +26,7 @@ impl Resource {
|
||||||
|
|
||||||
let colored_text = text
|
let colored_text = text
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|text| syntax_highlighting(&response, text));
|
.and_then(|text| syntax_highlighting(ctx, &response, text));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
response,
|
response,
|
||||||
|
@ -87,7 +87,7 @@ impl epi::App for HttpApp {
|
||||||
// Are we there yet?
|
// Are we there yet?
|
||||||
if let Ok(result) = receiver.try_recv() {
|
if let Ok(result) = receiver.try_recv() {
|
||||||
self.in_progress = None;
|
self.in_progress = None;
|
||||||
self.result = Some(result.map(Resource::from_response));
|
self.result = Some(result.map(|response| Resource::from_response(ctx, response)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,74 +291,24 @@ fn ui_resource(
|
||||||
// Syntax highlighting:
|
// Syntax highlighting:
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
fn syntax_highlighting(response: &ehttp::Response, text: &str) -> Option<ColoredText> {
|
fn syntax_highlighting(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
response: &ehttp::Response,
|
||||||
|
text: &str,
|
||||||
|
) -> 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.get(0)?;
|
||||||
ColoredText::text_with_extension(text, extension)
|
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
|
||||||
|
Some(ColoredText(crate::syntax_highlighting::highlight(
|
||||||
|
ctx, &theme, text, extension,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lines of text fragments
|
/// Lines of text fragments
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
struct ColoredText(egui::text::LayoutJob);
|
struct ColoredText(egui::text::LayoutJob);
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl ColoredText {
|
impl ColoredText {
|
||||||
/// e.g. `text_with_extension("fn foo() {}", "rs")`
|
#[cfg(feature = "syntect")]
|
||||||
pub fn text_with_extension(text: &str, extension: &str) -> Option<ColoredText> {
|
|
||||||
use syntect::easy::HighlightLines;
|
|
||||||
use syntect::highlighting::FontStyle;
|
|
||||||
use syntect::highlighting::ThemeSet;
|
|
||||||
use syntect::parsing::SyntaxSet;
|
|
||||||
use syntect::util::LinesWithEndings;
|
|
||||||
|
|
||||||
let ps = SyntaxSet::load_defaults_newlines(); // should be cached and reused
|
|
||||||
let ts = ThemeSet::load_defaults(); // should be cached and reused
|
|
||||||
|
|
||||||
let syntax = ps.find_syntax_by_extension(extension)?;
|
|
||||||
|
|
||||||
let dark_mode = true;
|
|
||||||
let theme = if dark_mode {
|
|
||||||
"base16-mocha.dark"
|
|
||||||
} else {
|
|
||||||
"base16-ocean.light"
|
|
||||||
};
|
|
||||||
let mut h = HighlightLines::new(syntax, &ts.themes[theme]);
|
|
||||||
|
|
||||||
use egui::text::{LayoutJob, LayoutSection, TextFormat};
|
|
||||||
|
|
||||||
let mut job = LayoutJob {
|
|
||||||
text: text.into(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for line in LinesWithEndings::from(text) {
|
|
||||||
for (style, range) in h.highlight(line, &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(ColoredText(job))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&self, ui: &mut egui::Ui) {
|
pub fn ui(&self, ui: &mut egui::Ui) {
|
||||||
let mut job = self.0.clone();
|
let mut job = self.0.clone();
|
||||||
job.wrap_width = ui.available_width();
|
job.wrap_width = ui.available_width();
|
||||||
|
@ -366,29 +316,16 @@ impl ColoredText {
|
||||||
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
||||||
painter.add(egui::Shape::galley(response.rect.min, galley));
|
painter.add(egui::Shape::galley(response.rect.min, galley));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(not(feature = "syntect"))]
|
||||||
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;
|
|
||||||
assert!(whole_start <= range_start);
|
|
||||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
|
||||||
let offset = range_start - whole_start;
|
|
||||||
offset..(offset + range.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
fn syntax_highlighting(_: &ehttp::Response, _: &str) -> Option<ColoredText> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
struct ColoredText();
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
impl ColoredText {
|
|
||||||
pub fn ui(&self, _ui: &mut egui::Ui) {}
|
pub fn ui(&self, _ui: &mut egui::Ui) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option<ColoredText> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Texture/image handling is very manual at the moment.
|
// Texture/image handling is very manual at the moment.
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ mod apps;
|
||||||
mod backend_panel;
|
mod backend_panel;
|
||||||
pub mod easy_mark;
|
pub mod easy_mark;
|
||||||
pub(crate) mod frame_history;
|
pub(crate) mod frame_history;
|
||||||
|
pub mod syntax_highlighting;
|
||||||
mod wrap_app;
|
mod wrap_app;
|
||||||
|
|
||||||
pub use apps::ColorTest; // used for tests
|
pub use apps::ColorTest; // used for tests
|
||||||
|
|
491
egui_demo_lib/src/syntax_highlighting.rs
Normal file
491
egui_demo_lib/src/syntax_highlighting.rs
Normal file
|
@ -0,0 +1,491 @@
|
||||||
|
use egui::text::LayoutJob;
|
||||||
|
|
||||||
|
/// View some code with syntax highlighing and selection.
|
||||||
|
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
||||||
|
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);
|
||||||
|
// layout_job.wrap_width = wrap_width; // no wrapping
|
||||||
|
ui.fonts().layout_job(layout_job)
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut code)
|
||||||
|
.text_style(egui::TextStyle::Monospace) // for cursor height
|
||||||
|
.code_editor()
|
||||||
|
.desired_rows(1)
|
||||||
|
.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<LayoutJob, Highligher>;
|
||||||
|
|
||||||
|
let mut memory = ctx.memory();
|
||||||
|
let highlight_cache = memory.caches.cache::<HighlightCache<'_>>();
|
||||||
|
highlight_cache.get((theme, code, language))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[derive(enum_map::Enum)]
|
||||||
|
enum TokenType {
|
||||||
|
Comment,
|
||||||
|
Keyword,
|
||||||
|
Literal,
|
||||||
|
StringLiteral,
|
||||||
|
Punctuation,
|
||||||
|
Whitespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
#[derive(Clone, Copy, Hash, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
enum SyntectTheme {
|
||||||
|
Base16EightiesDark,
|
||||||
|
Base16MochaDark,
|
||||||
|
Base16OceanDark,
|
||||||
|
Base16OceanLight,
|
||||||
|
InspiredGitHub,
|
||||||
|
SolarizedDark,
|
||||||
|
SolarizedLight,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
impl SyntectTheme {
|
||||||
|
fn all() -> impl Iterator<Item = Self> {
|
||||||
|
[
|
||||||
|
Self::Base16EightiesDark,
|
||||||
|
Self::Base16MochaDark,
|
||||||
|
Self::Base16OceanDark,
|
||||||
|
Self::Base16OceanLight,
|
||||||
|
Self::InspiredGitHub,
|
||||||
|
Self::SolarizedDark,
|
||||||
|
Self::SolarizedLight,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Base16EightiesDark => "Base16 Eighties (dark)",
|
||||||
|
Self::Base16MochaDark => "Base16 Mocha (dark)",
|
||||||
|
Self::Base16OceanDark => "Base16 Ocean (dark)",
|
||||||
|
Self::Base16OceanLight => "Base16 Ocean (light)",
|
||||||
|
Self::InspiredGitHub => "InspiredGitHub (light)",
|
||||||
|
Self::SolarizedDark => "Solarized (dark)",
|
||||||
|
Self::SolarizedLight => "Solarized (light)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntect_key_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Base16EightiesDark => "base16-eighties.dark",
|
||||||
|
Self::Base16MochaDark => "base16-mocha.dark",
|
||||||
|
Self::Base16OceanDark => "base16-ocean.dark",
|
||||||
|
Self::Base16OceanLight => "base16-ocean.light",
|
||||||
|
Self::InspiredGitHub => "InspiredGitHub",
|
||||||
|
Self::SolarizedDark => "Solarized (dark)",
|
||||||
|
Self::SolarizedLight => "Solarized (light)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dark(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Base16EightiesDark
|
||||||
|
| Self::Base16MochaDark
|
||||||
|
| Self::Base16OceanDark
|
||||||
|
| Self::SolarizedDark => true,
|
||||||
|
|
||||||
|
Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
syntect_theme: SyntectTheme,
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CodeTheme {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::dark()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeTheme {
|
||||||
|
pub fn from_style(style: &egui::Style) -> Self {
|
||||||
|
if style.visuals.dark_mode {
|
||||||
|
Self::dark()
|
||||||
|
} else {
|
||||||
|
Self::light()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_in_memory(&self, ctx: &egui::Context) {
|
||||||
|
if self.dark_mode {
|
||||||
|
ctx.memory().id_data.insert(egui::Id::new("dark"), *self);
|
||||||
|
} else {
|
||||||
|
ctx.memory().id_data.insert(egui::Id::new("light"), *self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
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 ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||||
|
|
||||||
|
for theme in SyntectTheme::all() {
|
||||||
|
if theme.is_dark() == self.dark_mode {
|
||||||
|
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
impl CodeTheme {
|
||||||
|
pub fn dark() -> Self {
|
||||||
|
let text_style = egui::TextStyle::Monospace;
|
||||||
|
use egui::{Color32, TextFormat};
|
||||||
|
Self {
|
||||||
|
dark_mode: true,
|
||||||
|
formats: enum_map::enum_map![
|
||||||
|
TokenType::Comment => TextFormat::simple(text_style, Color32::from_gray(120)),
|
||||||
|
TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(255, 100, 100)),
|
||||||
|
TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(87, 165, 171)),
|
||||||
|
TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(109, 147, 226)),
|
||||||
|
TokenType::Punctuation => TextFormat::simple(text_style, Color32::LIGHT_GRAY),
|
||||||
|
TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light() -> Self {
|
||||||
|
let text_style = egui::TextStyle::Monospace;
|
||||||
|
use egui::{Color32, TextFormat};
|
||||||
|
Self {
|
||||||
|
dark_mode: false,
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
formats: enum_map::enum_map![
|
||||||
|
TokenType::Comment => TextFormat::simple(text_style, Color32::GRAY),
|
||||||
|
TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(235, 0, 0)),
|
||||||
|
TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(153, 134, 255)),
|
||||||
|
TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(37, 203, 105)),
|
||||||
|
TokenType::Punctuation => TextFormat::simple(text_style, Color32::DARK_GRAY),
|
||||||
|
TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
let mut selected_tt: TokenType = *ui.memory().data.get_or(TokenType::Comment);
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.set_width(150.0);
|
||||||
|
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
ui.scope(|ui| {
|
||||||
|
for (tt, tt_name) in [
|
||||||
|
(TokenType::Comment, "// comment"),
|
||||||
|
(TokenType::Keyword, "keyword"),
|
||||||
|
(TokenType::Literal, "literal"),
|
||||||
|
(TokenType::StringLiteral, "\"string literal\""),
|
||||||
|
(TokenType::Punctuation, "punctuation ;"),
|
||||||
|
// (TokenType::Whitespace, "whitespace"),
|
||||||
|
] {
|
||||||
|
let format = &mut self.formats[tt];
|
||||||
|
ui.style_mut().override_text_style = Some(format.style);
|
||||||
|
ui.visuals_mut().override_text_color = Some(format.color);
|
||||||
|
ui.radio_value(&mut selected_tt, tt, tt_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let reset_value = if self.dark_mode {
|
||||||
|
CodeTheme::dark()
|
||||||
|
} else {
|
||||||
|
CodeTheme::light()
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.add(egui::Button::new("Reset theme").enabled(*self != reset_value))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*self = reset_value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
// ui.separator(); // TODO: fix forever-expand
|
||||||
|
|
||||||
|
ui.memory().data.insert(selected_tt);
|
||||||
|
|
||||||
|
egui::Frame::group(ui.style())
|
||||||
|
.margin(egui::Vec2::splat(2.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
// ui.group(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
|
||||||
|
ui.spacing_mut().slider_width = 128.0; // Controls color picker size
|
||||||
|
egui::widgets::color_picker::color_picker_color32(
|
||||||
|
ui,
|
||||||
|
&mut self.formats[selected_tt].color,
|
||||||
|
egui::color_picker::Alpha::Opaque,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
struct Highligher {
|
||||||
|
ps: syntect::parsing::SyntaxSet,
|
||||||
|
ts: syntect::highlighting::ThemeSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
impl Default for Highligher {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||||
|
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
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<LayoutJob> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
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;
|
||||||
|
assert!(whole_start <= range_start);
|
||||||
|
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||||
|
let offset = range_start - whole_start;
|
||||||
|
offset..(offset + range.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Highligher {}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
impl Highligher {
|
||||||
|
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||||
|
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
|
||||||
|
// Extremely simple syntax highlighter for when we compile without syntect
|
||||||
|
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
|
|
||||||
|
while !text.is_empty() {
|
||||||
|
if text.starts_with("//") {
|
||||||
|
let end = text.find('\n').unwrap_or_else(|| text.len());
|
||||||
|
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment]);
|
||||||
|
text = &text[end..];
|
||||||
|
} else if text.starts_with('"') {
|
||||||
|
let end = text[1..]
|
||||||
|
.find('"')
|
||||||
|
.map(|i| i + 2)
|
||||||
|
.or_else(|| text.find('\n'))
|
||||||
|
.unwrap_or_else(|| text.len());
|
||||||
|
job.append(&text[..end], 0.0, theme.formats[TokenType::StringLiteral]);
|
||||||
|
text = &text[end..];
|
||||||
|
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
||||||
|
let end = text[1..]
|
||||||
|
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or_else(|| text.len());
|
||||||
|
let word = &text[..end];
|
||||||
|
let tt = if is_keyword(word) {
|
||||||
|
TokenType::Keyword
|
||||||
|
} else {
|
||||||
|
TokenType::Literal
|
||||||
|
};
|
||||||
|
job.append(word, 0.0, theme.formats[tt]);
|
||||||
|
text = &text[end..];
|
||||||
|
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
|
||||||
|
let end = text[1..]
|
||||||
|
.find(|c: char| !c.is_ascii_whitespace())
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or_else(|| text.len());
|
||||||
|
job.append(&text[..end], 0.0, theme.formats[TokenType::Whitespace]);
|
||||||
|
text = &text[end..];
|
||||||
|
} else {
|
||||||
|
let mut it = text.char_indices();
|
||||||
|
it.next();
|
||||||
|
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
|
||||||
|
job.append(&text[..end], 0.0, theme.formats[TokenType::Punctuation]);
|
||||||
|
text = &text[end..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
job
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
fn is_keyword(word: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
word,
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue