From e7326f0af6f16cf2ff04fbac93bf296a044923f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 18 Sep 2023 19:07:41 +0200 Subject: [PATCH] Flesh out the `editor` example a bit more --- core/src/renderer/null.rs | 4 + core/src/text/editor.rs | 8 + examples/editor/Cargo.toml | 8 +- examples/editor/fonts/icons.ttf | Bin 0 -> 6352 bytes examples/editor/src/main.rs | 285 +++++++++++++++++++++++++++++--- graphics/src/text/editor.rs | 8 +- src/settings.rs | 8 + widget/src/text_editor.rs | 4 + winit/src/application.rs | 9 +- winit/src/settings.rs | 4 + 10 files changed, 311 insertions(+), 27 deletions(-) create mode 100644 examples/editor/fonts/icons.ttf diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 21597c8e..da0f32de 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -125,6 +125,10 @@ impl text::Editor for () { text::editor::Cursor::Caret(Point::ORIGIN) } + fn cursor_position(&self) -> (usize, usize) { + (0, 0) + } + fn selection(&self) -> Option { None } diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 2144715f..13bafc3d 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -12,6 +12,8 @@ pub trait Editor: Sized + Default { fn cursor(&self) -> Cursor; + fn cursor_position(&self) -> (usize, usize); + fn selection(&self) -> Option; fn line(&self, index: usize) -> Option<&str>; @@ -52,6 +54,12 @@ pub enum Action { Drag(Point), } +impl Action { + pub fn is_edit(&self) -> bool { + matches!(self, Self::Edit(_)) + } +} + #[derive(Debug, Clone, PartialEq)] pub enum Edit { Insert(char), 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 0000000000000000000000000000000000000000..393c6922e52f0fce49800b294962763daee5a0f3 GIT binary patch literal 6352 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 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; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); - fn new() -> Self { - Self { - content: text_editor::Content::with(include_str!("main.rs")), - theme: highlighter::Theme::SolarizedDark, - } + 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; diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 4673fce3..dfb91f34 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -221,6 +221,12 @@ impl editor::Editor for Editor { } } + fn cursor_position(&self) -> (usize, usize) { + let cursor = self.internal().editor.cursor(); + + (cursor.line, cursor.index) + } + fn perform(&mut self, action: Action) { let mut font_system = text::font_system().write().expect("Write font system"); @@ -559,7 +565,7 @@ impl editor::Editor for Editor { Some(i) } }) - .unwrap_or(buffer.lines.len()); + .unwrap_or(buffer.lines.len().saturating_sub(1)); let current_line = highlighter.current_line(); diff --git a/src/settings.rs b/src/settings.rs index d9778d7e..6b9ce095 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,6 +2,8 @@ use crate::window; use crate::{Font, Pixels}; +use std::borrow::Cow; + /// The settings of an application. #[derive(Debug, Clone)] pub struct Settings { @@ -21,6 +23,9 @@ pub struct Settings { /// [`Application`]: crate::Application pub flags: Flags, + /// The fonts to load on boot. + pub fonts: Vec>, + /// The default [`Font`] to be used. /// /// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif). @@ -62,6 +67,7 @@ impl Settings { flags, id: default_settings.id, window: default_settings.window, + fonts: default_settings.fonts, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, @@ -79,6 +85,7 @@ where id: None, window: Default::default(), flags: Default::default(), + fonts: Default::default(), default_font: Default::default(), default_text_size: Pixels(16.0), antialiasing: false, @@ -93,6 +100,7 @@ impl From> for iced_winit::Settings { id: settings.id, window: settings.window.into(), flags: settings.flags, + fonts: settings.fonts, exit_on_close_request: settings.exit_on_close_request, } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0cde2c98..970ec031 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -182,6 +182,10 @@ where pub fn selection(&self) -> Option { self.0.borrow().editor.selection() } + + pub fn cursor_position(&self) -> (usize, usize) { + self.0.borrow().editor.cursor_position() + } } impl Default for Content diff --git a/winit/src/application.rs b/winit/src/application.rs index d1689452..e80e9783 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -193,7 +193,14 @@ where }; } - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; + let (compositor, mut renderer) = + C::new(compositor_settings, Some(&window))?; + + for font in settings.fonts { + use crate::core::text::Renderer; + + renderer.load_font(font); + } let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 8d3e1b47..b4a1dd61 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -33,6 +33,7 @@ use crate::Position; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; +use std::borrow::Cow; use std::fmt; /// The settings of an application. @@ -52,6 +53,9 @@ pub struct Settings { /// [`Application`]: crate::Application pub flags: Flags, + /// The fonts to load on boot. + pub fonts: Vec>, + /// Whether the [`Application`] should exit when the user requests the /// window to close (e.g. the user presses the close button). ///