diff options
author | 2025-01-28 06:23:38 +0100 | |
---|---|---|
committer | 2025-01-28 06:23:38 +0100 | |
commit | 87165ccd29f0e85a26774ec090d8400274c6d04e (patch) | |
tree | d057c83712b9228b787adb8ac327b25bbf95795c | |
parent | 00a048677fc43a5e3106dff17e0c1c490fdc5ce1 (diff) | |
download | iced-87165ccd29f0e85a26774ec090d8400274c6d04e.tar.gz iced-87165ccd29f0e85a26774ec090d8400274c6d04e.tar.bz2 iced-87165ccd29f0e85a26774ec090d8400274c6d04e.zip |
Introduce `LineEnding` to `editor` and fix inconsistencies
-rw-r--r-- | core/src/renderer/null.rs | 2 | ||||
-rw-r--r-- | core/src/text/editor.rs | 41 | ||||
-rw-r--r-- | examples/editor/src/main.rs | 10 | ||||
-rw-r--r-- | graphics/src/text/editor.rs | 17 | ||||
-rw-r--r-- | widget/src/text_editor.rs | 83 |
5 files changed, 93 insertions, 60 deletions
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index bbcdd8ff..5732c41b 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -137,7 +137,7 @@ impl text::Editor for () { None } - fn line(&self, _index: usize) -> Option<&str> { + fn line(&self, _index: usize) -> Option<text::editor::Line<'_>> { None } diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index cd30db3a..6921c61c 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -3,6 +3,7 @@ use crate::text::highlighter::{self, Highlighter}; use crate::text::{LineHeight, Wrapping}; use crate::{Pixels, Point, Rectangle, Size}; +use std::borrow::Cow; use std::sync::Arc; /// A component that can be used by widgets to edit multi-line text. @@ -28,7 +29,7 @@ pub trait Editor: Sized + Default { fn selection(&self) -> Option<String>; /// Returns the text of the given line in the [`Editor`], if it exists. - fn line(&self, index: usize) -> Option<&str>; + fn line(&self, index: usize) -> Option<Line<'_>>; /// Returns the amount of lines in the [`Editor`]. fn line_count(&self) -> usize; @@ -189,3 +190,41 @@ pub enum Cursor { /// Cursor selecting a range of text Selection(Vec<Rectangle>), } + +/// A line of an [`Editor`]. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Line<'a> { + /// The raw text of the [`Line`]. + pub text: Cow<'a, str>, + /// The line ending of the [`Line`]. + pub ending: LineEnding, +} + +/// The line ending of a [`Line`]. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum LineEnding { + /// Use `\n` for line ending (POSIX-style) + #[default] + Lf, + /// Use `\r\n` for line ending (Windows-style) + CrLf, + /// Use `\r` for line ending (many legacy systems) + Cr, + /// Use `\n\r` for line ending (some legacy systems) + LfCr, + /// No line ending + None, +} + +impl LineEnding { + /// Gets the string representation of the [`LineEnding`]. + pub fn as_str(self) -> &'static str { + match self { + Self::Lf => "\n", + Self::CrLf => "\r\n", + Self::Cr => "\r", + Self::LfCr => "\n\r", + Self::None => "", + } + } +} diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 7032324a..c039672e 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -117,8 +117,16 @@ impl Editor { } else { self.is_loading = true; + let mut text = self.content.text(); + + if let Some(ending) = self.content.line_ending() { + if !text.ends_with(ending.as_str()) { + text.push_str(ending.as_str()); + } + } + Task::perform( - save_file(self.file.clone(), self.content.text()), + save_file(self.file.clone(), text), Message::FileSaved, ) } diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 1f1d0050..c73d189c 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -9,6 +9,7 @@ use crate::text; use cosmic_text::Edit as _; +use std::borrow::Cow; use std::fmt; use std::sync::{self, Arc}; @@ -89,11 +90,17 @@ impl editor::Editor for Editor { || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) } - fn line(&self, index: usize) -> Option<&str> { - self.buffer() - .lines - .get(index) - .map(cosmic_text::BufferLine::text) + fn line(&self, index: usize) -> Option<editor::Line<'_>> { + self.buffer().lines.get(index).map(|line| editor::Line { + text: Cow::Borrowed(line.text()), + ending: match line.ending() { + cosmic_text::LineEnding::Lf => editor::LineEnding::Lf, + cosmic_text::LineEnding::CrLf => editor::LineEnding::CrLf, + cosmic_text::LineEnding::Cr => editor::LineEnding::Cr, + cosmic_text::LineEnding::LfCr => editor::LineEnding::LfCr, + cosmic_text::LineEnding::None => editor::LineEnding::None, + }, + }) } fn line_count(&self) -> usize { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ad852ce9..a3470768 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -50,12 +50,13 @@ use crate::core::{ Rectangle, Shell, Size, SmolStr, Theme, Vector, }; +use std::borrow::Cow; use std::cell::RefCell; use std::fmt; use std::ops::DerefMut; use std::sync::Arc; -pub use text::editor::{Action, Edit, Motion}; +pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; /// A multi-line text input. /// @@ -349,69 +350,47 @@ where } /// Returns the text of the line at the given index, if it exists. - pub fn line( - &self, - index: usize, - ) -> Option<impl std::ops::Deref<Target = str> + '_> { - std::cell::Ref::filter_map(self.0.borrow(), |internal| { - internal.editor.line(index) + pub fn line(&self, index: usize) -> Option<Line<'_>> { + let internal = self.0.borrow(); + let line = internal.editor.line(index)?; + + Some(Line { + text: Cow::Owned(line.text.into_owned()), + ending: line.ending, }) - .ok() } /// Returns an iterator of the text of the lines in the [`Content`]. - pub fn lines( - &self, - ) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> { - struct Lines<'a, Renderer: text::Renderer> { - internal: std::cell::Ref<'a, Internal<Renderer>>, - current: usize, - } - - impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> { - type Item = std::cell::Ref<'a, str>; - - fn next(&mut self) -> Option<Self::Item> { - let line = std::cell::Ref::filter_map( - std::cell::Ref::clone(&self.internal), - |internal| internal.editor.line(self.current), - ) - .ok()?; - - self.current += 1; - - Some(line) - } - } - - Lines { - internal: self.0.borrow(), - current: 0, - } + pub fn lines(&self) -> impl Iterator<Item = Line<'_>> { + (0..) + .map(|i| self.line(i)) + .take_while(Option::is_some) + .flatten() } /// Returns the text of the [`Content`]. - /// - /// Lines are joined with `'\n'`. pub fn text(&self) -> String { - let mut text = self.lines().enumerate().fold( - String::new(), - |mut contents, (i, line)| { - if i > 0 { - contents.push('\n'); - } + let mut contents = String::new(); + let mut lines = self.lines().peekable(); - contents.push_str(&line); + while let Some(line) = lines.next() { + contents.push_str(&line.text); - contents - }, - ); - - if !text.ends_with('\n') { - text.push('\n'); + if lines.peek().is_some() { + contents.push_str(if line.ending == LineEnding::None { + LineEnding::default().as_str() + } else { + line.ending.as_str() + }); + } } - text + contents + } + + /// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`]. + pub fn line_ending(&self) -> Option<LineEnding> { + Some(self.line(0)?.ending) } /// Returns the selected text of the [`Content`]. |