From 6448429103c9c82b90040ac5a5a097bdded23f82 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 12 Sep 2023 14:51:00 +0200 Subject: Draft `Editor` API and `TextEditor` widget --- examples/editor/Cargo.toml | 10 +++++++++ examples/editor/src/main.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 examples/editor/Cargo.toml create mode 100644 examples/editor/src/main.rs (limited to 'examples') diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml new file mode 100644 index 00000000..528cf23c --- /dev/null +++ b/examples/editor/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "editor" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["debug"] \ No newline at end of file diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs new file mode 100644 index 00000000..50989ac5 --- /dev/null +++ b/examples/editor/src/main.rs @@ -0,0 +1,49 @@ +use iced::widget::{container, text_editor}; +use iced::{Element, Font, Sandbox, Settings}; + +pub fn main() -> iced::Result { + Editor::run(Settings::default()) +} + +struct Editor { + content: text_editor::Content, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Edit(text_editor::Action), +} + +impl Sandbox for Editor { + type Message = Message; + + fn new() -> Self { + Self { + content: text_editor::Content::with(include_str!( + "../../../README.md" + )), + } + } + + fn title(&self) -> String { + String::from("Editor - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::Edit(action) => { + self.content.edit(action); + } + } + } + + fn view(&self) -> Element { + container( + text_editor(&self.content) + .on_edit(Message::Edit) + .font(Font::with_name("Hasklug Nerd Font Mono")), + ) + .padding(20) + .into() + } +} -- cgit From 52b36a9574f45138363a4bfc6394c6da03baa433 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 13 Sep 2023 15:17:04 +0200 Subject: Use `Theme::Dark` in `editor` example --- examples/editor/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 50989ac5..2a70b34c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{container, text_editor}; -use iced::{Element, Font, Sandbox, Settings}; +use iced::{Element, Font, Sandbox, Settings, Theme}; pub fn main() -> iced::Result { Editor::run(Settings::default()) @@ -46,4 +46,8 @@ impl Sandbox for Editor { .padding(20) .into() } + + fn theme(&self) -> Theme { + Theme::Dark + } } -- cgit From d051f21597bb333ac10183aaa3214a292e9aa365 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 16 Sep 2023 15:40:16 +0200 Subject: Implement `Copy` and `Paste` actions for `text::Editor` --- examples/editor/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 2a70b34c..11819c69 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -9,7 +9,7 @@ struct Editor { content: text_editor::Content, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), } -- cgit From d3011992a76e83e12f74402c2ade616cdc7f1497 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 19:03:58 +0200 Subject: Implement basic syntax highlighting with `syntect` in `editor` example --- examples/editor/Cargo.toml | 4 +- examples/editor/src/main.rs | 168 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 2 deletions(-) (limited to 'examples') diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 528cf23c..930ee592 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,4 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] \ No newline at end of file +iced.features = ["advanced", "debug"] + +syntect = "5.1" \ No newline at end of file diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 11819c69..a72feebc 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,6 +1,8 @@ use iced::widget::{container, text_editor}; use iced::{Element, Font, Sandbox, Settings, Theme}; +use highlighter::Highlighter; + pub fn main() -> iced::Result { Editor::run(Settings::default()) } @@ -41,7 +43,10 @@ impl Sandbox for Editor { container( text_editor(&self.content) .on_edit(Message::Edit) - .font(Font::with_name("Hasklug Nerd Font Mono")), + .font(Font::with_name("Hasklug Nerd Font Mono")) + .highlight::(highlighter::Settings { + token: String::from("md"), + }), ) .padding(20) .into() @@ -51,3 +56,164 @@ impl Sandbox for Editor { Theme::Dark } } + +mod highlighter { + use iced::advanced::text::highlighter; + use iced::widget::text_editor; + use iced::{Color, Font, Theme}; + + use std::ops::Range; + use syntect::highlighting; + use syntect::parsing; + + #[derive(Debug, Clone, Hash)] + pub struct Settings { + pub token: String, + } + + pub struct Highlight(highlighting::StyleModifier); + + impl text_editor::Highlight for Highlight { + fn format(&self, _theme: &Theme) -> highlighter::Format { + 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, + parser: parsing::ParseState, + stack: parsing::ScopeStack, + theme: highlighting::Theme, + token: String, + current_line: usize, + } + + impl highlighter::Highlighter for Highlighter { + type Settings = Settings; + type Highlight = Highlight; + + type Iterator<'a> = + Box, Self::Highlight)> + 'a>; + + fn new(settings: &Self::Settings) -> Self { + let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); + + let syntax = syntaxes + .find_syntax_by_token(&settings.token) + .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); + + let parser = parsing::ParseState::new(&syntax); + let stack = parsing::ScopeStack::new(); + + let theme = highlighting::ThemeSet::load_defaults() + .themes + .remove("base16-mocha.dark") + .unwrap(); + + Highlighter { + syntaxes, + parser, + stack, + theme, + token: settings.token.clone(), + current_line: 0, + } + } + + fn change_line(&mut self, _line: usize) { + // TODO: Caching + let syntax = self + .syntaxes + .find_syntax_by_token(&self.token) + .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()); + + self.parser = parsing::ParseState::new(&syntax); + self.stack = parsing::ScopeStack::new(); + self.current_line = 0; + } + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + self.current_line += 1; + + let ops = self + .parser + .parse_line(line, &self.syntaxes) + .unwrap_or_default(); + + Box::new( + ScopeRangeIterator { + ops, + line_length: line.len(), + index: 0, + last_str_index: 0, + } + .filter_map(move |(range, scope)| { + let highlighter = + highlighting::Highlighter::new(&self.theme); + let _ = self.stack.apply(&scope); + + if range.is_empty() { + None + } else { + Some(( + range, + Highlight( + highlighter + .style_mod_for_stack(&self.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, parsing::ScopeStackOp); + + fn next(&mut self) -> Option { + 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)) + } + } +} -- cgit From 790c0dabcf0a50a2466e47daeb4f1e149b2ede5a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 21:45:13 +0200 Subject: Implement syntax highlighting cache in `editor` example --- examples/editor/src/main.rs | 67 ++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 25 deletions(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index a72feebc..1235d38b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -64,7 +64,7 @@ mod highlighter { use std::ops::Range; use syntect::highlighting; - use syntect::parsing; + use syntect::parsing::{self, SyntaxReference}; #[derive(Debug, Clone, Hash)] pub struct Settings { @@ -91,13 +91,14 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, - parser: parsing::ParseState, - stack: parsing::ScopeStack, + syntax: SyntaxReference, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, theme: highlighting::Theme, - token: String, current_line: usize, } + const LINES_PER_SNAPSHOT: usize = 50; + impl highlighter::Highlighter for Highlighter { type Settings = Settings; type Highlight = Highlight; @@ -121,34 +122,53 @@ mod highlighter { .unwrap(); Highlighter { + syntax: syntax.clone(), syntaxes, - parser, - stack, + caches: vec![(parser, stack)], theme, - token: settings.token.clone(), current_line: 0, } } - fn change_line(&mut self, _line: usize) { - // TODO: Caching - let syntax = self - .syntaxes - .find_syntax_by_token(&self.token) - .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()); + 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.parser = parsing::ParseState::new(&syntax); - self.stack = parsing::ScopeStack::new(); - self.current_line = 0; + 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 ops = self - .parser - .parse_line(line, &self.syntaxes) - .unwrap_or_default(); + 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 { @@ -158,9 +178,7 @@ mod highlighter { last_str_index: 0, } .filter_map(move |(range, scope)| { - let highlighter = - highlighting::Highlighter::new(&self.theme); - let _ = self.stack.apply(&scope); + let _ = stack.apply(&scope); if range.is_empty() { None @@ -168,8 +186,7 @@ mod highlighter { Some(( range, Highlight( - highlighter - .style_mod_for_stack(&self.stack.scopes), + highlighter.style_mod_for_stack(&stack.scopes), ), )) } -- cgit From 8f8528a4ccee049aba779fe86cda786a52afac30 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 17 Sep 2023 23:20:15 +0200 Subject: Fix unnecessary dereference in `editor` example --- examples/editor/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 1235d38b..74649676 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -113,7 +113,7 @@ mod highlighter { .find_syntax_by_token(&settings.token) .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - let parser = parsing::ParseState::new(&syntax); + let parser = parsing::ParseState::new(syntax); let stack = parsing::ScopeStack::new(); let theme = highlighting::ThemeSet::load_defaults() -- cgit From 8446fe6de52fa68077d23d39f728f79a29b52f00 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 14:38:54 +0200 Subject: Implement theme selector in `editor` example --- examples/editor/src/main.rs | 101 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 19 deletions(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 74649676..fa35ba0f 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{container, text_editor}; -use iced::{Element, Font, Sandbox, Settings, Theme}; +use iced::widget::{column, horizontal_space, pick_list, row, text_editor}; +use iced::{Element, Font, Length, Sandbox, Settings, Theme}; use highlighter::Highlighter; @@ -9,11 +9,13 @@ pub fn main() -> iced::Result { struct Editor { content: text_editor::Content, + theme: highlighter::Theme, } #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), + ThemeSelected(highlighter::Theme), } impl Sandbox for Editor { @@ -21,9 +23,8 @@ impl Sandbox for Editor { fn new() -> Self { Self { - content: text_editor::Content::with(include_str!( - "../../../README.md" - )), + content: text_editor::Content::with(include_str!("main.rs")), + theme: highlighter::Theme::SolarizedDark, } } @@ -36,18 +37,33 @@ impl Sandbox for Editor { Message::Edit(action) => { self.content.edit(action); } + Message::ThemeSelected(theme) => { + self.theme = theme; + } } } fn view(&self) -> Element { - container( + column![ + row![ + horizontal_space(Length::Fill), + pick_list( + highlighter::Theme::ALL, + Some(self.theme), + Message::ThemeSelected + ) + .padding([5, 10]) + ] + .spacing(10), text_editor(&self.content) .on_edit(Message::Edit) .font(Font::with_name("Hasklug Nerd Font Mono")) .highlight::(highlighter::Settings { - token: String::from("md"), + theme: self.theme, + extension: String::from("rs"), }), - ) + ] + .spacing(10) .padding(20) .into() } @@ -60,21 +76,52 @@ impl Sandbox for Editor { mod highlighter { use iced::advanced::text::highlighter; use iced::widget::text_editor; - use iced::{Color, Font, Theme}; + use iced::{Color, Font}; use std::ops::Range; use syntect::highlighting; use syntect::parsing::{self, SyntaxReference}; - #[derive(Debug, Clone, Hash)] + #[derive(Debug, Clone, PartialEq)] pub struct Settings { - pub token: String, + 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: &Theme) -> highlighter::Format { + fn format(&self, _theme: &iced::Theme) -> highlighter::Format { highlighter::Format { color: self.0.foreground.map(|color| { Color::from_rgba8( @@ -92,8 +139,8 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, syntax: SyntaxReference, - caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, theme: highlighting::Theme, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, } @@ -110,26 +157,42 @@ mod highlighter { let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); let syntax = syntaxes - .find_syntax_by_token(&settings.token) + .find_syntax_by_token(&settings.extension) .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - let parser = parsing::ParseState::new(syntax); - let stack = parsing::ScopeStack::new(); - let theme = highlighting::ThemeSet::load_defaults() .themes - .remove("base16-mocha.dark") + .remove(settings.theme.key()) .unwrap(); + let parser = parsing::ParseState::new(syntax); + let stack = parsing::ScopeStack::new(); + Highlighter { syntax: syntax.clone(), syntaxes, - caches: vec![(parser, stack)], 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; -- cgit From e7326f0af6f16cf2ff04fbac93bf296a044923f4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 19:07:41 +0200 Subject: Flesh out the `editor` example a bit more --- examples/editor/Cargo.toml | 8 +- examples/editor/fonts/icons.ttf | Bin 0 -> 6352 bytes examples/editor/src/main.rs | 287 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 examples/editor/fonts/icons.ttf (limited to 'examples') diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 930ee592..eeb34aa1 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,6 +7,10 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["advanced", "debug"] +iced.features = ["advanced", "tokio", "debug"] -syntect = "5.1" \ No newline at end of file +tokio.workspace = true +tokio.features = ["fs"] + +syntect = "5.1" +rfd = "0.12" diff --git a/examples/editor/fonts/icons.ttf b/examples/editor/fonts/icons.ttf new file mode 100644 index 00000000..393c6922 Binary files /dev/null and b/examples/editor/fonts/icons.ttf differ diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index fa35ba0f..09c4b9b5 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,70 +1,218 @@ -use iced::widget::{column, horizontal_space, pick_list, row, text_editor}; -use iced::{Element, Font, Length, Sandbox, Settings, Theme}; +use iced::executor; +use iced::theme::{self, Theme}; +use iced::widget::{ + button, column, container, horizontal_space, pick_list, row, text, + text_editor, tooltip, +}; +use iced::{Application, Command, Element, Font, Length, Settings}; use highlighter::Highlighter; +use std::ffi; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + pub fn main() -> iced::Result { - Editor::run(Settings::default()) + Editor::run(Settings { + fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], + default_font: Font { + monospaced: true, + ..Font::with_name("Hasklug Nerd Font Mono") + }, + ..Settings::default() + }) } struct Editor { + file: Option, content: text_editor::Content, theme: highlighter::Theme, + is_loading: bool, + is_dirty: bool, } #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), ThemeSelected(highlighter::Theme), + NewFile, + OpenFile, + FileOpened(Result<(PathBuf, Arc), Error>), + SaveFile, + FileSaved(Result), } -impl Sandbox for Editor { +impl Application for Editor { type Message = Message; - - fn new() -> Self { - Self { - content: text_editor::Content::with(include_str!("main.rs")), - theme: highlighter::Theme::SolarizedDark, - } + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command) { + ( + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + is_loading: true, + is_dirty: false, + }, + Command::perform(load_file(default_file()), Message::FileOpened), + ) } fn title(&self) -> String { String::from("Editor - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { match message { Message::Edit(action) => { + self.is_dirty = self.is_dirty || action.is_edit(); + self.content.edit(action); + + Command::none() } Message::ThemeSelected(theme) => { self.theme = theme; + + Command::none() + } + Message::NewFile => { + if !self.is_loading { + self.file = None; + self.content = text_editor::Content::new(); + } + + Command::none() + } + Message::OpenFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + Command::perform(open_file(), Message::FileOpened) + } + } + Message::FileOpened(result) => { + self.is_loading = false; + self.is_dirty = false; + + if let Ok((path, contents)) = result { + self.file = Some(path); + self.content = text_editor::Content::with(&contents); + } + + Command::none() + } + Message::SaveFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + let mut contents = self.content.lines().enumerate().fold( + String::new(), + |mut contents, (i, line)| { + if i > 0 { + contents.push_str("\n"); + } + + contents.push_str(&line); + + contents + }, + ); + + if !contents.ends_with("\n") { + contents.push_str("\n"); + } + + Command::perform( + save_file(self.file.clone(), contents), + Message::FileSaved, + ) + } + } + Message::FileSaved(result) => { + self.is_loading = false; + + if let Ok(path) = result { + self.file = Some(path); + self.is_dirty = false; + } + + Command::none() } } } fn view(&self) -> Element { + let controls = row![ + action(new_icon(), "New file", Some(Message::NewFile)), + action( + open_icon(), + "Open file", + (!self.is_loading).then_some(Message::OpenFile) + ), + action( + save_icon(), + "Save file", + self.is_dirty.then_some(Message::SaveFile) + ), + horizontal_space(Length::Fill), + pick_list( + highlighter::Theme::ALL, + Some(self.theme), + Message::ThemeSelected + ) + .text_size(14) + .padding([5, 10]) + ] + .spacing(10); + + let status = row![ + text(if let Some(path) = &self.file { + let path = path.display().to_string(); + + if path.len() > 60 { + format!("...{}", &path[path.len() - 40..]) + } else { + path + } + } else { + String::from("New file") + }), + horizontal_space(Length::Fill), + text({ + let (line, column) = self.content.cursor_position(); + + format!("{}:{}", line + 1, column + 1) + }) + ] + .spacing(10); + column![ - row![ - horizontal_space(Length::Fill), - pick_list( - highlighter::Theme::ALL, - Some(self.theme), - Message::ThemeSelected - ) - .padding([5, 10]) - ] - .spacing(10), + controls, text_editor(&self.content) .on_edit(Message::Edit) - .font(Font::with_name("Hasklug Nerd Font Mono")) .highlight::(highlighter::Settings { theme: self.theme, - extension: String::from("rs"), + extension: self + .file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .map(str::to_string) + .unwrap_or(String::from("rs")), }), + status, ] .spacing(10) - .padding(20) + .padding(10) .into() } @@ -73,6 +221,97 @@ impl Sandbox for Editor { } } +#[derive(Debug, Clone)] +pub enum Error { + DialogClosed, + IoError(io::ErrorKind), +} + +fn default_file() -> PathBuf { + PathBuf::from(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))) +} + +async fn open_file() -> Result<(PathBuf, Arc), Error> { + let picked_file = rfd::AsyncFileDialog::new() + .set_title("Open a text file...") + .pick_file() + .await + .ok_or(Error::DialogClosed)?; + + load_file(picked_file.path().to_owned()).await +} + +async fn load_file(path: PathBuf) -> Result<(PathBuf, Arc), Error> { + let contents = tokio::fs::read_to_string(&path) + .await + .map(Arc::new) + .map_err(|error| Error::IoError(error.kind()))?; + + Ok((path, contents)) +} + +async fn save_file( + path: Option, + contents: String, +) -> Result { + let path = if let Some(path) = path { + path + } else { + rfd::AsyncFileDialog::new() + .save_file() + .await + .as_ref() + .map(rfd::FileHandle::path) + .map(Path::to_owned) + .ok_or(Error::DialogClosed)? + }; + + let _ = tokio::fs::write(&path, contents) + .await + .map_err(|error| Error::IoError(error.kind()))?; + + Ok(path) +} + +fn action<'a, Message: Clone + 'a>( + content: impl Into>, + label: &'a str, + on_press: Option, +) -> Element<'a, Message> { + let action = + button(container(content).width(Length::Fill).center_x()).width(40); + + if let Some(on_press) = on_press { + tooltip( + action.on_press(on_press), + label, + tooltip::Position::FollowCursor, + ) + .style(theme::Container::Box) + .into() + } else { + action.style(theme::Button::Secondary).into() + } +} + +fn new_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e800}') +} + +fn save_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e801}') +} + +fn open_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0f115}') +} + +fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { + const ICON_FONT: Font = Font::with_name("editor-icons"); + + text(codepoint).font(ICON_FONT).into() +} + mod highlighter { use iced::advanced::text::highlighter; use iced::widget::text_editor; -- cgit From 161a971d065b3254a2f11cb374d2c94c2d67646b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 19:08:57 +0200 Subject: Fix `clippy` lints --- examples/editor/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 09c4b9b5..785dfb3b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -118,7 +118,7 @@ impl Application for Editor { String::new(), |mut contents, (i, line)| { if i > 0 { - contents.push_str("\n"); + contents.push('\n'); } contents.push_str(&line); @@ -127,8 +127,8 @@ impl Application for Editor { }, ); - if !contents.ends_with("\n") { - contents.push_str("\n"); + if !contents.ends_with('\n') { + contents.push('\n'); } Command::perform( @@ -266,7 +266,7 @@ async fn save_file( .ok_or(Error::DialogClosed)? }; - let _ = tokio::fs::write(&path, contents) + tokio::fs::write(&path, contents) .await .map_err(|error| Error::IoError(error.kind()))?; -- cgit From 8eec0033dee816bfcc102fc4f511c8bfe08c14ee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 19:24:09 +0200 Subject: Remove unnecessary `monospaced` flag in `Font` --- examples/editor/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 785dfb3b..5018b3cb 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -16,10 +16,7 @@ use std::sync::Arc; pub fn main() -> iced::Result { Editor::run(Settings { fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - default_font: Font { - monospaced: true, - ..Font::with_name("Hasklug Nerd Font Mono") - }, + default_font: Font::with_name("Hasklug Nerd Font Mono"), ..Settings::default() }) } -- cgit From d1d0b3aaee84003278b9db3e86687e776f20b346 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 18 Sep 2023 20:14:38 +0200 Subject: Use `Font::MONOSPACE` in `editor` example --- examples/editor/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 5018b3cb..277eb3e9 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -16,7 +16,7 @@ use std::sync::Arc; pub fn main() -> iced::Result { Editor::run(Settings { fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - default_font: Font::with_name("Hasklug Nerd Font Mono"), + default_font: Font::MONOSPACE, ..Settings::default() }) } -- cgit From 06dc12bfbf75958c6534306b3d1b57ae47bdb37a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 19:35:28 +0200 Subject: Simplify `editor` example --- examples/editor/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 277eb3e9..6def2082 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Application, Command, Element, Font, Length, Settings}; +use iced::{Alignment, Application, Command, Element, Font, Length, Settings}; use highlighter::Highlighter; @@ -169,7 +169,8 @@ impl Application for Editor { .text_size(14) .padding([5, 10]) ] - .spacing(10); + .spacing(10) + .align_items(Alignment::Center); let status = row![ text(if let Some(path) = &self.file { @@ -275,8 +276,7 @@ fn action<'a, Message: Clone + 'a>( label: &'a str, on_press: Option, ) -> Element<'a, Message> { - let action = - button(container(content).width(Length::Fill).center_x()).width(40); + let action = button(container(content).width(30).center_x()); if let Some(on_press) = on_press { tooltip( @@ -316,7 +316,7 @@ mod highlighter { use std::ops::Range; use syntect::highlighting; - use syntect::parsing::{self, SyntaxReference}; + use syntect::parsing; #[derive(Debug, Clone, PartialEq)] pub struct Settings { @@ -374,7 +374,7 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, - syntax: SyntaxReference, + syntax: parsing::SyntaxReference, theme: highlighting::Theme, caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, -- cgit From c0a141ab026f5686d6bd92c8807b174396cb9105 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 19:39:23 +0200 Subject: Save file on `Cmd+S` in `editor` example --- examples/editor/src/main.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 6def2082..36d4287c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,10 +1,14 @@ use iced::executor; +use iced::keyboard; use iced::theme::{self, Theme}; use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Alignment, Application, Command, Element, Font, Length, Settings}; +use iced::{ + Alignment, Application, Command, Element, Font, Length, Settings, + Subscription, +}; use highlighter::Highlighter; @@ -147,6 +151,15 @@ impl Application for Editor { } } + fn subscription(&self) -> Subscription { + keyboard::on_key_press(|key_code, modifiers| match key_code { + keyboard::KeyCode::S if modifiers.command() => { + Some(Message::SaveFile) + } + _ => None, + }) + } + fn view(&self) -> Element { let controls = row![ action(new_icon(), "New file", Some(Message::NewFile)), -- cgit From f806d001e6fb44b5a45029ca257261e6e0d4d4b2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 20:48:50 +0200 Subject: Introduce new `iced_highlighter` subcrate --- examples/editor/Cargo.toml | 2 +- examples/editor/src/main.rs | 251 +++----------------------------------------- 2 files changed, 15 insertions(+), 238 deletions(-) (limited to 'examples') 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::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::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 { - 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, 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, parsing::ScopeStackOp); - - fn next(&mut self) -> Option { - 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)) - } - } -} -- cgit From d9fbecf0d80234d63e7e5711f28fc35ee75fa503 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 19 Sep 2023 20:58:15 +0200 Subject: Remove `syntect` dependency from `editor` example --- examples/editor/Cargo.toml | 1 - 1 file changed, 1 deletion(-) (limited to 'examples') diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index a77b1e9f..a3f6ea3b 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -12,5 +12,4 @@ iced.features = ["highlighter", "tokio", "debug"] tokio.workspace = true tokio.features = ["fs"] -syntect = "5.1" rfd = "0.12" -- cgit From ff78e97ad7df4db3b2a97b94e99854f2f9e3021a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 20 Sep 2023 01:21:42 +0200 Subject: Introduce more themes to `iced_highlighter` --- examples/editor/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index d513090f..f49ca6e8 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -230,7 +230,11 @@ impl Application for Editor { } fn theme(&self) -> Theme { - Theme::Dark + if self.theme.is_dark() { + Theme::Dark + } else { + Theme::Light + } } } -- cgit From 8cc19de254c37d3123d5ea1b6513f1f34d35c7c8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 22 Sep 2023 06:00:51 +0200 Subject: Add `text` helper method for `text_editor::Content` --- examples/editor/src/main.rs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index f49ca6e8..a69e1f54 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -114,25 +114,8 @@ impl Application for Editor { } else { self.is_loading = true; - let mut contents = self.content.lines().enumerate().fold( - String::new(), - |mut contents, (i, line)| { - if i > 0 { - contents.push('\n'); - } - - contents.push_str(&line); - - contents - }, - ); - - if !contents.ends_with('\n') { - contents.push('\n'); - } - Command::perform( - save_file(self.file.clone(), contents), + save_file(self.file.clone(), self.content.text()), Message::FileSaved, ) } -- cgit From 625cd745f38215b1cb8f629cdc6d2fa41c9a739a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 27 Oct 2023 05:04:14 +0200 Subject: Write documentation for the new text APIs --- examples/editor/src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'examples') diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index a69e1f54..03d1e283 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -34,7 +34,7 @@ struct Editor { #[derive(Debug, Clone)] enum Message { - Edit(text_editor::Action), + ActionPerformed(text_editor::Action), ThemeSelected(highlighter::Theme), NewFile, OpenFile, @@ -68,10 +68,10 @@ impl Application for Editor { fn update(&mut self, message: Message) -> Command { match message { - Message::Edit(action) => { + Message::ActionPerformed(action) => { self.is_dirty = self.is_dirty || action.is_edit(); - self.content.edit(action); + self.content.perform(action); Command::none() } @@ -103,7 +103,7 @@ impl Application for Editor { if let Ok((path, contents)) = result { self.file = Some(path); - self.content = text_editor::Content::with(&contents); + self.content = text_editor::Content::with_text(&contents); } Command::none() @@ -191,7 +191,7 @@ impl Application for Editor { column![ controls, text_editor(&self.content) - .on_edit(Message::Edit) + .on_action(Message::ActionPerformed) .highlight::( highlighter::Settings { theme: self.theme, -- cgit