summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml10
-rw-r--r--core/src/text/highlighter.rs11
-rw-r--r--examples/editor/Cargo.toml2
-rw-r--r--examples/editor/src/main.rs251
-rw-r--r--highlighter/Cargo.toml16
-rw-r--r--highlighter/src/lib.rs225
-rw-r--r--src/lib.rs3
-rw-r--r--style/src/text_editor.rs16
-rw-r--r--widget/src/text_editor.rs28
9 files changed, 302 insertions, 260 deletions
diff --git a/Cargo.toml b/Cargo.toml
index e887afc0..8899fa67 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,6 +47,8 @@ system = ["iced_winit/system"]
web-colors = ["iced_renderer/web-colors"]
# Enables the WebGL backend, replacing WebGPU
webgl = ["iced_renderer/webgl"]
+# Enables the syntax `highlighter` module
+highlighter = ["iced_highlighter"]
# Enables the advanced module
advanced = []
@@ -58,6 +60,9 @@ iced_widget.workspace = true
iced_winit.features = ["application"]
iced_winit.workspace = true
+iced_highlighter.workspace = true
+iced_highlighter.optional = true
+
thiserror.workspace = true
image.workspace = true
@@ -78,8 +83,9 @@ members = [
"core",
"futures",
"graphics",
- "runtime",
+ "highlighter",
"renderer",
+ "runtime",
"style",
"tiny_skia",
"wgpu",
@@ -103,6 +109,7 @@ iced = { version = "0.12", path = "." }
iced_core = { version = "0.12", path = "core" }
iced_futures = { version = "0.12", path = "futures" }
iced_graphics = { version = "0.12", path = "graphics" }
+iced_highlighter = { version = "0.12", path = "highlighter" }
iced_renderer = { version = "0.12", path = "renderer" }
iced_runtime = { version = "0.12", path = "runtime" }
iced_style = { version = "0.12", path = "style" }
@@ -137,6 +144,7 @@ resvg = "0.35"
rustc-hash = "1.0"
smol = "1.0"
softbuffer = "0.2"
+syntect = "5.1"
sysinfo = "0.28"
thiserror = "1.0"
tiny-skia = "0.10"
diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs
index b462d083..9a9cff89 100644
--- a/core/src/text/highlighter.rs
+++ b/core/src/text/highlighter.rs
@@ -52,8 +52,17 @@ impl Highlighter for PlainText {
}
}
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Format<Font> {
pub color: Option<Color>,
pub font: Option<Font>,
}
+
+impl<Font> Default for Format<Font> {
+ fn default() -> Self {
+ Self {
+ color: None,
+ font: None,
+ }
+ }
+}
diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml
index eeb34aa1..a77b1e9f 100644
--- a/examples/editor/Cargo.toml
+++ b/examples/editor/Cargo.toml
@@ -7,7 +7,7 @@ publish = false
[dependencies]
iced.workspace = true
-iced.features = ["advanced", "tokio", "debug"]
+iced.features = ["highlighter", "tokio", "debug"]
tokio.workspace = true
tokio.features = ["fs"]
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
index 36d4287c..d513090f 100644
--- a/examples/editor/src/main.rs
+++ b/examples/editor/src/main.rs
@@ -1,4 +1,5 @@
use iced::executor;
+use iced::highlighter::{self, Highlighter};
use iced::keyboard;
use iced::theme::{self, Theme};
use iced::widget::{
@@ -10,8 +11,6 @@ use iced::{
Subscription,
};
-use highlighter::Highlighter;
-
use std::ffi;
use std::io;
use std::path::{Path, PathBuf};
@@ -210,16 +209,19 @@ impl Application for Editor {
controls,
text_editor(&self.content)
.on_edit(Message::Edit)
- .highlight::<Highlighter>(highlighter::Settings {
- theme: self.theme,
- extension: self
- .file
- .as_deref()
- .and_then(Path::extension)
- .and_then(ffi::OsStr::to_str)
- .map(str::to_string)
- .unwrap_or(String::from("rs")),
- }),
+ .highlight::<Highlighter>(
+ highlighter::Settings {
+ theme: self.theme,
+ extension: self
+ .file
+ .as_deref()
+ .and_then(Path::extension)
+ .and_then(ffi::OsStr::to_str)
+ .map(str::to_string)
+ .unwrap_or(String::from("rs")),
+ },
+ |highlight, _theme| highlight.to_format()
+ ),
status,
]
.spacing(10)
@@ -321,228 +323,3 @@ fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> {
text(codepoint).font(ICON_FONT).into()
}
-
-mod highlighter {
- use iced::advanced::text::highlighter;
- use iced::widget::text_editor;
- use iced::{Color, Font};
-
- use std::ops::Range;
- use syntect::highlighting;
- use syntect::parsing;
-
- #[derive(Debug, Clone, PartialEq)]
- pub struct Settings {
- pub theme: Theme,
- pub extension: String,
- }
-
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
- pub enum Theme {
- SolarizedDark,
- InspiredGitHub,
- Base16Mocha,
- }
-
- impl Theme {
- pub const ALL: &[Self] =
- &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha];
-
- fn key(&self) -> &'static str {
- match self {
- Theme::InspiredGitHub => "InspiredGitHub",
- Theme::Base16Mocha => "base16-mocha.dark",
- Theme::SolarizedDark => "Solarized (dark)",
- }
- }
- }
-
- impl std::fmt::Display for Theme {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Theme::InspiredGitHub => write!(f, "Inspired GitHub"),
- Theme::Base16Mocha => write!(f, "Mocha"),
- Theme::SolarizedDark => write!(f, "Solarized Dark"),
- }
- }
- }
-
- pub struct Highlight(highlighting::StyleModifier);
-
- impl text_editor::Highlight for Highlight {
- fn format(&self, _theme: &iced::Theme) -> highlighter::Format<Font> {
- highlighter::Format {
- color: self.0.foreground.map(|color| {
- Color::from_rgba8(
- color.r,
- color.g,
- color.b,
- color.a as f32 / 255.0,
- )
- }),
- font: None,
- }
- }
- }
-
- pub struct Highlighter {
- syntaxes: parsing::SyntaxSet,
- syntax: parsing::SyntaxReference,
- theme: highlighting::Theme,
- caches: Vec<(parsing::ParseState, parsing::ScopeStack)>,
- current_line: usize,
- }
-
- const LINES_PER_SNAPSHOT: usize = 50;
-
- impl highlighter::Highlighter for Highlighter {
- type Settings = Settings;
- type Highlight = Highlight;
-
- type Iterator<'a> =
- Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>;
-
- fn new(settings: &Self::Settings) -> Self {
- let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines();
-
- let syntax = syntaxes
- .find_syntax_by_token(&settings.extension)
- .unwrap_or_else(|| syntaxes.find_syntax_plain_text());
-
- let theme = highlighting::ThemeSet::load_defaults()
- .themes
- .remove(settings.theme.key())
- .unwrap();
-
- let parser = parsing::ParseState::new(syntax);
- let stack = parsing::ScopeStack::new();
-
- Highlighter {
- syntax: syntax.clone(),
- syntaxes,
- theme,
- caches: vec![(parser, stack)],
- current_line: 0,
- }
- }
-
- fn update(&mut self, new_settings: &Self::Settings) {
- self.syntax = self
- .syntaxes
- .find_syntax_by_token(&new_settings.extension)
- .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text())
- .clone();
-
- self.theme = highlighting::ThemeSet::load_defaults()
- .themes
- .remove(new_settings.theme.key())
- .unwrap();
-
- // Restart the highlighter
- self.change_line(0);
- }
-
- fn change_line(&mut self, line: usize) {
- let snapshot = line / LINES_PER_SNAPSHOT;
-
- if snapshot <= self.caches.len() {
- self.caches.truncate(snapshot);
- self.current_line = snapshot * LINES_PER_SNAPSHOT;
- } else {
- self.caches.truncate(1);
- self.current_line = 0;
- }
-
- let (parser, stack) =
- self.caches.last().cloned().unwrap_or_else(|| {
- (
- parsing::ParseState::new(&self.syntax),
- parsing::ScopeStack::new(),
- )
- });
-
- self.caches.push((parser, stack));
- }
-
- fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
- if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() {
- let (parser, stack) =
- self.caches.last().expect("Caches must not be empty");
-
- self.caches.push((parser.clone(), stack.clone()));
- }
-
- self.current_line += 1;
-
- let (parser, stack) =
- self.caches.last_mut().expect("Caches must not be empty");
-
- let ops =
- parser.parse_line(line, &self.syntaxes).unwrap_or_default();
-
- let highlighter = highlighting::Highlighter::new(&self.theme);
-
- Box::new(
- ScopeRangeIterator {
- ops,
- line_length: line.len(),
- index: 0,
- last_str_index: 0,
- }
- .filter_map(move |(range, scope)| {
- let _ = stack.apply(&scope);
-
- if range.is_empty() {
- None
- } else {
- Some((
- range,
- Highlight(
- highlighter.style_mod_for_stack(&stack.scopes),
- ),
- ))
- }
- }),
- )
- }
-
- fn current_line(&self) -> usize {
- self.current_line
- }
- }
-
- pub struct ScopeRangeIterator {
- ops: Vec<(usize, parsing::ScopeStackOp)>,
- line_length: usize,
- index: usize,
- last_str_index: usize,
- }
-
- impl Iterator for ScopeRangeIterator {
- type Item = (std::ops::Range<usize>, parsing::ScopeStackOp);
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.index > self.ops.len() {
- return None;
- }
-
- let next_str_i = if self.index == self.ops.len() {
- self.line_length
- } else {
- self.ops[self.index].0
- };
-
- let range = self.last_str_index..next_str_i;
- self.last_str_index = next_str_i;
-
- let op = if self.index == 0 {
- parsing::ScopeStackOp::Noop
- } else {
- self.ops[self.index - 1].1.clone()
- };
-
- self.index += 1;
- Some((range, op))
- }
- }
-}
diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml
new file mode 100644
index 00000000..311d2998
--- /dev/null
+++ b/highlighter/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "iced_highlighter"
+description = "A syntax higlighter for iced"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+categories.workspace = true
+keywords.workspace = true
+
+[dependencies]
+iced_core.workspace = true
+
+syntect.workspace = true
diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs
new file mode 100644
index 00000000..79cfafcd
--- /dev/null
+++ b/highlighter/src/lib.rs
@@ -0,0 +1,225 @@
+use iced_core as core;
+
+use crate::core::text::highlighter::{self, Format};
+use crate::core::{Color, Font};
+
+use std::ops::Range;
+use syntect::highlighting;
+use syntect::parsing;
+
+pub struct Highlighter {
+ syntaxes: parsing::SyntaxSet,
+ syntax: parsing::SyntaxReference,
+ theme: highlighting::Theme,
+ caches: Vec<(parsing::ParseState, parsing::ScopeStack)>,
+ current_line: usize,
+}
+
+const LINES_PER_SNAPSHOT: usize = 50;
+
+impl highlighter::Highlighter for Highlighter {
+ type Settings = Settings;
+ type Highlight = Highlight;
+
+ type Iterator<'a> =
+ Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>;
+
+ fn new(settings: &Self::Settings) -> Self {
+ let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines();
+
+ let syntax = syntaxes
+ .find_syntax_by_token(&settings.extension)
+ .unwrap_or_else(|| syntaxes.find_syntax_plain_text());
+
+ let theme = highlighting::ThemeSet::load_defaults()
+ .themes
+ .remove(settings.theme.key())
+ .unwrap();
+
+ let parser = parsing::ParseState::new(syntax);
+ let stack = parsing::ScopeStack::new();
+
+ Highlighter {
+ syntax: syntax.clone(),
+ syntaxes,
+ theme,
+ caches: vec![(parser, stack)],
+ current_line: 0,
+ }
+ }
+
+ fn update(&mut self, new_settings: &Self::Settings) {
+ self.syntax = self
+ .syntaxes
+ .find_syntax_by_token(&new_settings.extension)
+ .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text())
+ .clone();
+
+ self.theme = highlighting::ThemeSet::load_defaults()
+ .themes
+ .remove(new_settings.theme.key())
+ .unwrap();
+
+ // Restart the highlighter
+ self.change_line(0);
+ }
+
+ fn change_line(&mut self, line: usize) {
+ let snapshot = line / LINES_PER_SNAPSHOT;
+
+ if snapshot <= self.caches.len() {
+ self.caches.truncate(snapshot);
+ self.current_line = snapshot * LINES_PER_SNAPSHOT;
+ } else {
+ self.caches.truncate(1);
+ self.current_line = 0;
+ }
+
+ let (parser, stack) =
+ self.caches.last().cloned().unwrap_or_else(|| {
+ (
+ parsing::ParseState::new(&self.syntax),
+ parsing::ScopeStack::new(),
+ )
+ });
+
+ self.caches.push((parser, stack));
+ }
+
+ fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
+ if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() {
+ let (parser, stack) =
+ self.caches.last().expect("Caches must not be empty");
+
+ self.caches.push((parser.clone(), stack.clone()));
+ }
+
+ self.current_line += 1;
+
+ let (parser, stack) =
+ self.caches.last_mut().expect("Caches must not be empty");
+
+ let ops = parser.parse_line(line, &self.syntaxes).unwrap_or_default();
+
+ let highlighter = highlighting::Highlighter::new(&self.theme);
+
+ Box::new(
+ ScopeRangeIterator {
+ ops,
+ line_length: line.len(),
+ index: 0,
+ last_str_index: 0,
+ }
+ .filter_map(move |(range, scope)| {
+ let _ = stack.apply(&scope);
+
+ if range.is_empty() {
+ None
+ } else {
+ Some((
+ range,
+ Highlight(
+ highlighter.style_mod_for_stack(&stack.scopes),
+ ),
+ ))
+ }
+ }),
+ )
+ }
+
+ fn current_line(&self) -> usize {
+ self.current_line
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Settings {
+ pub theme: Theme,
+ pub extension: String,
+}
+
+pub struct Highlight(highlighting::StyleModifier);
+
+impl Highlight {
+ pub fn color(&self) -> Option<Color> {
+ self.0.foreground.map(|color| {
+ Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0)
+ })
+ }
+
+ pub fn font(&self) -> Option<Font> {
+ None
+ }
+
+ pub fn to_format(&self) -> Format<Font> {
+ Format {
+ color: self.color(),
+ font: self.font(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Theme {
+ SolarizedDark,
+ InspiredGitHub,
+ Base16Mocha,
+}
+
+impl Theme {
+ pub const ALL: &[Self] =
+ &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha];
+
+ fn key(&self) -> &'static str {
+ match self {
+ Theme::InspiredGitHub => "InspiredGitHub",
+ Theme::Base16Mocha => "base16-mocha.dark",
+ Theme::SolarizedDark => "Solarized (dark)",
+ }
+ }
+}
+
+impl std::fmt::Display for Theme {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Theme::InspiredGitHub => write!(f, "Inspired GitHub"),
+ Theme::Base16Mocha => write!(f, "Mocha"),
+ Theme::SolarizedDark => write!(f, "Solarized Dark"),
+ }
+ }
+}
+
+pub struct ScopeRangeIterator {
+ ops: Vec<(usize, parsing::ScopeStackOp)>,
+ line_length: usize,
+ index: usize,
+ last_str_index: usize,
+}
+
+impl Iterator for ScopeRangeIterator {
+ type Item = (std::ops::Range<usize>, parsing::ScopeStackOp);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.index > self.ops.len() {
+ return None;
+ }
+
+ let next_str_i = if self.index == self.ops.len() {
+ self.line_length
+ } else {
+ self.ops[self.index].0
+ };
+
+ let range = self.last_str_index..next_str_i;
+ self.last_str_index = next_str_i;
+
+ let op = if self.index == 0 {
+ parsing::ScopeStackOp::Noop
+ } else {
+ self.ops[self.index - 1].1.clone()
+ };
+
+ self.index += 1;
+ Some((range, op))
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3cbe716a..e435a041 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -174,6 +174,9 @@ use iced_winit::runtime;
pub use iced_futures::futures;
+#[cfg(feature = "highlighter")]
+pub use iced_highlighter as highlighter;
+
mod error;
mod sandbox;
diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs
index f1c31287..f6bae7e6 100644
--- a/style/src/text_editor.rs
+++ b/style/src/text_editor.rs
@@ -1,6 +1,5 @@
//! Change the appearance of a text editor.
-use crate::core::text::highlighter;
-use crate::core::{self, Background, BorderRadius, Color};
+use crate::core::{Background, BorderRadius, Color};
/// The appearance of a text input.
#[derive(Debug, Clone, Copy)]
@@ -46,16 +45,3 @@ pub trait StyleSheet {
/// Produces the style of a disabled text input.
fn disabled(&self, style: &Self::Style) -> Appearance;
}
-
-pub trait Highlight<Font = core::Font, Theme = crate::Theme> {
- fn format(&self, theme: &Theme) -> highlighter::Format<Font>;
-}
-
-impl<Font, Theme> Highlight<Font, Theme> for () {
- fn format(&self, _theme: &Theme) -> highlighter::Format<Font> {
- highlighter::Format {
- color: None,
- font: None,
- }
- }
-}
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index ad12a076..c384b8a2 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -16,7 +16,7 @@ use std::cell::RefCell;
use std::ops::DerefMut;
use std::sync::Arc;
-pub use crate::style::text_editor::{Appearance, Highlight, StyleSheet};
+pub use crate::style::text_editor::{Appearance, StyleSheet};
pub use text::editor::{Action, Edit, Motion};
pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer>
@@ -35,6 +35,10 @@ where
style: <Renderer::Theme as StyleSheet>::Style,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
highlighter_settings: Highlighter::Settings,
+ highlighter_format: fn(
+ &Highlighter::Highlight,
+ &Renderer::Theme,
+ ) -> highlighter::Format<Renderer::Font>,
}
impl<'a, Message, Renderer>
@@ -55,6 +59,9 @@ where
style: Default::default(),
on_edit: None,
highlighter_settings: (),
+ highlighter_format: |_highlight, _theme| {
+ highlighter::Format::default()
+ },
}
}
}
@@ -63,7 +70,6 @@ impl<'a, Highlighter, Message, Renderer>
TextEditor<'a, Highlighter, Message, Renderer>
where
Highlighter: text::Highlighter,
- Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
@@ -85,6 +91,10 @@ where
pub fn highlight<H: text::Highlighter>(
self,
settings: H::Settings,
+ to_format: fn(
+ &H::Highlight,
+ &Renderer::Theme,
+ ) -> highlighter::Format<Renderer::Font>,
) -> TextEditor<'a, H, Message, Renderer> {
TextEditor {
content: self.content,
@@ -97,6 +107,7 @@ where
style: self.style,
on_edit: self.on_edit,
highlighter_settings: settings,
+ highlighter_format: to_format,
}
}
}
@@ -203,13 +214,13 @@ struct State<Highlighter: text::Highlighter> {
drag_click: Option<mouse::click::Kind>,
highlighter: RefCell<Highlighter>,
highlighter_settings: Highlighter::Settings,
+ highlighter_format_address: usize,
}
impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer>
for TextEditor<'a, Highlighter, Message, Renderer>
where
Highlighter: text::Highlighter,
- Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
@@ -226,6 +237,7 @@ where
&self.highlighter_settings,
)),
highlighter_settings: self.highlighter_settings.clone(),
+ highlighter_format_address: self.highlighter_format as usize,
})
}
@@ -246,6 +258,13 @@ where
let mut internal = self.content.0.borrow_mut();
let state = tree.state.downcast_mut::<State<Highlighter>>();
+ if state.highlighter_format_address != self.highlighter_format as usize
+ {
+ state.highlighter.borrow_mut().change_line(0);
+
+ state.highlighter_format_address = self.highlighter_format as usize;
+ }
+
if state.highlighter_settings != self.highlighter_settings {
state
.highlighter
@@ -354,7 +373,7 @@ where
internal.editor.highlight(
self.font.unwrap_or_else(|| renderer.default_font()),
state.highlighter.borrow_mut().deref_mut(),
- |highlight| highlight.format(theme),
+ |highlight| (self.highlighter_format)(highlight, theme),
);
let is_disabled = self.on_edit.is_none();
@@ -458,7 +477,6 @@ impl<'a, Highlighter, Message, Renderer>
for Element<'a, Message, Renderer>
where
Highlighter: text::Highlighter,
- Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>,
Message: 'a,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,