summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/src/renderer/null.rs2
-rw-r--r--core/src/text/editor.rs41
-rw-r--r--examples/editor/src/main.rs10
-rw-r--r--graphics/src/text/editor.rs17
-rw-r--r--widget/src/text_editor.rs83
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`].