summaryrefslogtreecommitdiffstats
path: root/graphics
diff options
context:
space:
mode:
Diffstat (limited to 'graphics')
-rw-r--r--graphics/Cargo.toml1
-rw-r--r--graphics/src/geometry.rs1
-rw-r--r--graphics/src/geometry/fill.rs2
-rw-r--r--graphics/src/geometry/frame.rs55
-rw-r--r--graphics/src/geometry/stroke.rs2
-rw-r--r--graphics/src/geometry/style.rs2
-rw-r--r--graphics/src/geometry/text.rs5
-rw-r--r--graphics/src/gradient.rs2
-rw-r--r--graphics/src/image.rs45
-rw-r--r--graphics/src/text.rs25
-rw-r--r--graphics/src/text/cache.rs4
-rw-r--r--graphics/src/text/editor.rs304
-rw-r--r--graphics/src/text/paragraph.rs240
13 files changed, 414 insertions, 274 deletions
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index e8d27d07..7e2d767b 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -20,6 +20,7 @@ all-features = true
[features]
geometry = ["lyon_path"]
image = ["dep:image", "kamadak-exif"]
+svg = []
web-colors = []
fira-sans = []
diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs
index ab4a7a36..2b4b45a6 100644
--- a/graphics/src/geometry.rs
+++ b/graphics/src/geometry.rs
@@ -16,6 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke};
pub use style::Style;
pub use text::Text;
+pub use crate::core::{Image, Svg};
pub use crate::gradient::{self, Gradient};
use crate::cache::Cached;
diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs
index 670fbc12..b79a2582 100644
--- a/graphics/src/geometry/fill.rs
+++ b/graphics/src/geometry/fill.rs
@@ -7,7 +7,7 @@ use crate::core::Color;
use crate::gradient::{self, Gradient};
/// The style used to fill geometry.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
pub struct Fill {
/// The color or gradient of the fill.
///
diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs
index 377589d7..3dee7e75 100644
--- a/graphics/src/geometry/frame.rs
+++ b/graphics/src/geometry/frame.rs
@@ -1,6 +1,6 @@
//! Draw and generate geometry.
use crate::core::{Point, Radians, Rectangle, Size, Vector};
-use crate::geometry::{self, Fill, Path, Stroke, Text};
+use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text};
/// The region of a surface that can be used to draw geometry.
#[allow(missing_debug_implementations)]
@@ -65,6 +65,17 @@ where
self.raw.stroke(path, stroke);
}
+ /// Draws the stroke of an axis-aligned rectangle with the provided style
+ /// given its top-left corner coordinate and its `Size` on the [`Frame`] .
+ pub fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ ) {
+ self.raw.stroke_rectangle(top_left, size, stroke);
+ }
+
/// Draws the characters of the given [`Text`] on the [`Frame`], filling
/// them with the given color.
///
@@ -75,6 +86,18 @@ where
self.raw.fill_text(text);
}
+ /// Draws the given [`Image`] on the [`Frame`] inside the given bounds.
+ #[cfg(feature = "image")]
+ pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) {
+ self.raw.draw_image(bounds, image);
+ }
+
+ /// Draws the given [`Svg`] on the [`Frame`] inside the given bounds.
+ #[cfg(feature = "svg")]
+ pub fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
+ self.raw.draw_svg(bounds, svg);
+ }
+
/// Stores the current transform of the [`Frame`] and executes the given
/// drawing operations, restoring the transform afterwards.
///
@@ -116,8 +139,7 @@ where
let mut frame = self.draft(region);
let result = f(&mut frame);
-
- self.paste(frame, Point::new(region.x, region.y));
+ self.paste(frame);
result
}
@@ -134,8 +156,8 @@ where
}
/// Draws the contents of the given [`Frame`] with origin at the given [`Point`].
- fn paste(&mut self, frame: Self, at: Point) {
- self.raw.paste(frame.raw, at);
+ fn paste(&mut self, frame: Self) {
+ self.raw.paste(frame.raw);
}
/// Applies a translation to the current transform of the [`Frame`].
@@ -186,9 +208,15 @@ pub trait Backend: Sized {
fn scale_nonuniform(&mut self, scale: impl Into<Vector>);
fn draft(&mut self, clip_bounds: Rectangle) -> Self;
- fn paste(&mut self, frame: Self, at: Point);
+ fn paste(&mut self, frame: Self);
fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>);
+ fn stroke_rectangle<'a>(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ stroke: impl Into<Stroke<'a>>,
+ );
fn fill(&mut self, path: &Path, fill: impl Into<Fill>);
fn fill_text(&mut self, text: impl Into<Text>);
@@ -199,6 +227,9 @@ pub trait Backend: Sized {
fill: impl Into<Fill>,
);
+ fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>);
+ fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>);
+
fn into_geometry(self) -> Self::Geometry;
}
@@ -231,9 +262,16 @@ impl Backend for () {
fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {}
fn draft(&mut self, _clip_bounds: Rectangle) -> Self {}
- fn paste(&mut self, _frame: Self, _at: Point) {}
+ fn paste(&mut self, _frame: Self) {}
fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {}
+ fn stroke_rectangle<'a>(
+ &mut self,
+ _top_left: Point,
+ _size: Size,
+ _stroke: impl Into<Stroke<'a>>,
+ ) {
+ }
fn fill(&mut self, _path: &Path, _fill: impl Into<Fill>) {}
fn fill_text(&mut self, _text: impl Into<Text>) {}
@@ -245,5 +283,8 @@ impl Backend for () {
) {
}
+ fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into<Image>) {}
+ fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into<Svg>) {}
+
fn into_geometry(self) -> Self::Geometry {}
}
diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs
index aff49ab3..b8f4515e 100644
--- a/graphics/src/geometry/stroke.rs
+++ b/graphics/src/geometry/stroke.rs
@@ -6,7 +6,7 @@ pub use crate::geometry::Style;
use iced_core::Color;
/// The style of a stroke.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
pub struct Stroke<'a> {
/// The color or gradient of the stroke.
///
diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs
index a0f4b08a..de77eccc 100644
--- a/graphics/src/geometry/style.rs
+++ b/graphics/src/geometry/style.rs
@@ -2,7 +2,7 @@ use crate::core::Color;
use crate::geometry::Gradient;
/// The coloring style of some drawing.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Style {
/// A solid [`Color`].
Solid(Color),
diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs
index d314e85e..90147f87 100644
--- a/graphics/src/geometry/text.rs
+++ b/graphics/src/geometry/text.rs
@@ -43,6 +43,7 @@ impl Text {
let mut buffer = cosmic_text::BufferLine::new(
&self.content,
+ cosmic_text::LineEnding::default(),
cosmic_text::AttrsList::new(text::to_attributes(self.font)),
text::to_shaping(self.shaping),
);
@@ -50,8 +51,10 @@ impl Text {
let layout = buffer.layout(
font_system.raw(),
self.size.0,
- f32::MAX,
+ None,
cosmic_text::Wrap::None,
+ None,
+ 4,
);
let translation_x = match self.horizontal_alignment {
diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs
index 603f1b4a..54261721 100644
--- a/graphics/src/gradient.rs
+++ b/graphics/src/gradient.rs
@@ -9,7 +9,7 @@ use bytemuck::{Pod, Zeroable};
use half::f16;
use std::cmp::Ordering;
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq)]
/// A fill which linearly interpolates colors along a direction.
///
/// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`].
diff --git a/graphics/src/image.rs b/graphics/src/image.rs
index 318592be..67a5e0cf 100644
--- a/graphics/src/image.rs
+++ b/graphics/src/image.rs
@@ -2,57 +2,26 @@
#[cfg(feature = "image")]
pub use ::image as image_rs;
-use crate::core::{image, svg, Color, Radians, Rectangle};
+use crate::core::image;
+use crate::core::svg;
+use crate::core::Rectangle;
/// A raster or vector image.
#[derive(Debug, Clone, PartialEq)]
pub enum Image {
/// A raster image.
- Raster {
- /// The handle of a raster image.
- handle: image::Handle,
+ Raster(image::Image, Rectangle),
- /// The filter method of a raster image.
- filter_method: image::FilterMethod,
-
- /// The bounds of the image.
- bounds: Rectangle,
-
- /// The rotation of the image.
- rotation: Radians,
-
- /// The opacity of the image.
- opacity: f32,
- },
/// A vector image.
- Vector {
- /// The handle of a vector image.
- handle: svg::Handle,
-
- /// The [`Color`] filter
- color: Option<Color>,
-
- /// The bounds of the image.
- bounds: Rectangle,
-
- /// The rotation of the image.
- rotation: Radians,
-
- /// The opacity of the image.
- opacity: f32,
- },
+ Vector(svg::Svg, Rectangle),
}
impl Image {
/// Returns the bounds of the [`Image`].
pub fn bounds(&self) -> Rectangle {
match self {
- Image::Raster {
- bounds, rotation, ..
- }
- | Image::Vector {
- bounds, rotation, ..
- } => bounds.rotate(*rotation),
+ Image::Raster(image, bounds) => bounds.rotate(image.rotation),
+ Image::Vector(svg, bounds) => bounds.rotate(svg.rotation),
}
}
}
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
index 30269e69..feb9932a 100644
--- a/graphics/src/text.rs
+++ b/graphics/src/text.rs
@@ -11,7 +11,7 @@ pub use cosmic_text;
use crate::core::alignment;
use crate::core::font::{self, Font};
-use crate::core::text::Shaping;
+use crate::core::text::{Shaping, Wrapping};
use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation};
use once_cell::sync::OnceCell;
@@ -232,13 +232,14 @@ impl PartialEq for Raw {
/// Measures the dimensions of the given [`cosmic_text::Buffer`].
pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
- let (width, total_lines) = buffer
- .layout_runs()
- .fold((0.0, 0usize), |(width, total_lines), run| {
- (run.line_w.max(width), total_lines + 1)
- });
+ let (width, height) =
+ buffer
+ .layout_runs()
+ .fold((0.0, 0.0), |(width, height), run| {
+ (run.line_w.max(width), height + run.line_height)
+ });
- Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+ Size::new(width, height)
}
/// Returns the attributes of the given [`Font`].
@@ -305,6 +306,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
}
}
+/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy.
+pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap {
+ match wrapping {
+ Wrapping::None => cosmic_text::Wrap::None,
+ Wrapping::Word => cosmic_text::Wrap::Word,
+ Wrapping::Glyph => cosmic_text::Wrap::Glyph,
+ Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph,
+ }
+}
+
/// Converts some [`Color`] to a [`cosmic_text::Color`].
pub fn to_color(color: Color) -> cosmic_text::Color {
let [r, g, b, a] = color.into_rgba8();
diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs
index 822b61c4..e64d93f1 100644
--- a/graphics/src/text/cache.rs
+++ b/graphics/src/text/cache.rs
@@ -48,8 +48,8 @@ impl Cache {
buffer.set_size(
font_system,
- key.bounds.width,
- key.bounds.height.max(key.line_height),
+ Some(key.bounds.width),
+ Some(key.bounds.height.max(key.line_height)),
);
buffer.set_text(
font_system,
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
index c488a51c..1f1d0050 100644
--- a/graphics/src/text/editor.rs
+++ b/graphics/src/text/editor.rs
@@ -3,7 +3,7 @@ use crate::core::text::editor::{
self, Action, Cursor, Direction, Edit, Motion,
};
use crate::core::text::highlighter::{self, Highlighter};
-use crate::core::text::LineHeight;
+use crate::core::text::{LineHeight, Wrapping};
use crate::core::{Font, Pixels, Point, Rectangle, Size};
use crate::text;
@@ -17,7 +17,7 @@ use std::sync::{self, Arc};
pub struct Editor(Option<Arc<Internal>>);
struct Internal {
- editor: cosmic_text::Editor,
+ editor: cosmic_text::Editor<'static>,
font: Font,
bounds: Size,
topmost_line_changed: Option<usize>,
@@ -32,7 +32,7 @@ impl Editor {
/// Returns the buffer of the [`Editor`].
pub fn buffer(&self) -> &cosmic_text::Buffer {
- self.internal().editor.buffer()
+ buffer_from_editor(&self.internal().editor)
}
/// Creates a [`Weak`] reference to the [`Editor`].
@@ -82,6 +82,13 @@ impl editor::Editor for Editor {
})))
}
+ fn is_empty(&self) -> bool {
+ let buffer = self.buffer();
+
+ buffer.lines.is_empty()
+ || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty())
+ }
+
fn line(&self, index: usize) -> Option<&str> {
self.buffer()
.lines
@@ -101,16 +108,10 @@ impl editor::Editor for Editor {
let internal = self.internal();
let cursor = internal.editor.cursor();
- let buffer = internal.editor.buffer();
-
- match internal.editor.select_opt() {
- Some(selection) => {
- let (start, end) = if cursor < selection {
- (cursor, selection)
- } else {
- (selection, cursor)
- };
+ let buffer = buffer_from_editor(&internal.editor);
+ match internal.editor.selection_bounds() {
+ Some((start, end)) => {
let line_height = buffer.metrics().line_height;
let selected_lines = end.line - start.line + 1;
@@ -142,7 +143,8 @@ impl editor::Editor for Editor {
width,
y: (visual_line as i32 + visual_lines_offset)
as f32
- * line_height,
+ * line_height
+ - buffer.scroll().vertical,
height: line_height,
})
} else {
@@ -224,7 +226,8 @@ impl editor::Editor for Editor {
Cursor::Caret(Point::new(
offset,
(visual_lines_offset + visual_line as i32) as f32
- * line_height,
+ * line_height
+ - buffer.scroll().vertical,
))
}
}
@@ -252,16 +255,8 @@ impl editor::Editor for Editor {
match action {
// Motion events
Action::Move(motion) => {
- if let Some(selection) = editor.select_opt() {
- let cursor = editor.cursor();
-
- let (left, right) = if cursor < selection {
- (cursor, selection)
- } else {
- (selection, cursor)
- };
-
- editor.set_select_opt(None);
+ if let Some((start, end)) = editor.selection_bounds() {
+ editor.set_selection(cosmic_text::Selection::None);
match motion {
// These motions are performed as-is even when a selection
@@ -272,17 +267,20 @@ impl editor::Editor for Editor {
| Motion::DocumentEnd => {
editor.action(
font_system.raw(),
- motion_to_action(motion),
+ cosmic_text::Action::Motion(to_motion(motion)),
);
}
// Other motions simply move the cursor to one end of the selection
_ => editor.set_cursor(match motion.direction() {
- Direction::Left => left,
- Direction::Right => right,
+ Direction::Left => start,
+ Direction::Right => end,
}),
}
} else {
- editor.action(font_system.raw(), motion_to_action(motion));
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Motion(to_motion(motion)),
+ );
}
}
@@ -290,99 +288,58 @@ impl editor::Editor for Editor {
Action::Select(motion) => {
let cursor = editor.cursor();
- if editor.select_opt().is_none() {
- editor.set_select_opt(Some(cursor));
+ if editor.selection_bounds().is_none() {
+ editor
+ .set_selection(cosmic_text::Selection::Normal(cursor));
}
- editor.action(font_system.raw(), motion_to_action(motion));
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Motion(to_motion(motion)),
+ );
// Deselect if selection matches cursor position
- if let Some(selection) = editor.select_opt() {
- let cursor = editor.cursor();
-
- if cursor.line == selection.line
- && cursor.index == selection.index
- {
- editor.set_select_opt(None);
+ if let Some((start, end)) = editor.selection_bounds() {
+ if start.line == end.line && start.index == end.index {
+ editor.set_selection(cosmic_text::Selection::None);
}
}
}
Action::SelectWord => {
- use unicode_segmentation::UnicodeSegmentation;
-
let cursor = editor.cursor();
- if let Some(line) = editor.buffer().lines.get(cursor.line) {
- let (start, end) =
- UnicodeSegmentation::unicode_word_indices(line.text())
- // Split words with dots
- .flat_map(|(i, word)| {
- word.split('.').scan(i, |current, word| {
- let start = *current;
- *current += word.len() + 1;
-
- Some((start, word))
- })
- })
- // Turn words into ranges
- .map(|(i, word)| (i, i + word.len()))
- // Find the word at cursor
- .find(|&(start, end)| {
- start <= cursor.index && cursor.index < end
- })
- // Cursor is not in a word. Let's select its punctuation cluster.
- .unwrap_or_else(|| {
- let start = line.text()[..cursor.index]
- .char_indices()
- .rev()
- .take_while(|(_, c)| {
- c.is_ascii_punctuation()
- })
- .map(|(i, _)| i)
- .last()
- .unwrap_or(cursor.index);
-
- let end = line.text()[cursor.index..]
- .char_indices()
- .skip_while(|(_, c)| {
- c.is_ascii_punctuation()
- })
- .map(|(i, _)| i + cursor.index)
- .next()
- .unwrap_or(cursor.index);
-
- (start, end)
- });
-
- if start != end {
- editor.set_cursor(cosmic_text::Cursor {
- index: start,
- ..cursor
- });
-
- editor.set_select_opt(Some(cosmic_text::Cursor {
- index: end,
- ..cursor
- }));
- }
- }
+ editor.set_selection(cosmic_text::Selection::Word(cursor));
}
Action::SelectLine => {
let cursor = editor.cursor();
- if let Some(line_length) = editor
- .buffer()
- .lines
- .get(cursor.line)
- .map(|line| line.text().len())
+ editor.set_selection(cosmic_text::Selection::Line(cursor));
+ }
+ Action::SelectAll => {
+ let buffer = buffer_from_editor(editor);
+
+ if buffer.lines.len() > 1
+ || buffer
+ .lines
+ .first()
+ .is_some_and(|line| !line.text().is_empty())
{
- editor
- .set_cursor(cosmic_text::Cursor { index: 0, ..cursor });
+ let cursor = editor.cursor();
+
+ editor.set_selection(cosmic_text::Selection::Normal(
+ cosmic_text::Cursor {
+ line: 0,
+ index: 0,
+ ..cursor
+ },
+ ));
- editor.set_select_opt(Some(cosmic_text::Cursor {
- index: line_length,
- ..cursor
- }));
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Motion(
+ cosmic_text::Motion::BufferEnd,
+ ),
+ );
}
}
@@ -419,10 +376,12 @@ impl editor::Editor for Editor {
}
let cursor = editor.cursor();
- let selection = editor.select_opt().unwrap_or(cursor);
+ let selection_start = editor
+ .selection_bounds()
+ .map(|(start, _)| start)
+ .unwrap_or(cursor);
- internal.topmost_line_changed =
- Some(cursor.min(selection).line);
+ internal.topmost_line_changed = Some(selection_start.line);
}
// Mouse events
@@ -445,13 +404,9 @@ impl editor::Editor for Editor {
);
// Deselect if selection matches cursor position
- if let Some(selection) = editor.select_opt() {
- let cursor = editor.cursor();
-
- if cursor.line == selection.line
- && cursor.index == selection.index
- {
- editor.set_select_opt(None);
+ if let Some((start, end)) = editor.selection_bounds() {
+ if start.line == end.line && start.index == end.index {
+ editor.set_selection(cosmic_text::Selection::None);
}
}
}
@@ -473,7 +428,7 @@ impl editor::Editor for Editor {
fn min_bounds(&self) -> Size {
let internal = self.internal();
- text::measure(internal.editor.buffer())
+ text::measure(buffer_from_editor(&internal.editor))
}
fn update(
@@ -482,6 +437,7 @@ impl editor::Editor for Editor {
new_font: Font,
new_size: Pixels,
new_line_height: LineHeight,
+ new_wrapping: Wrapping,
new_highlighter: &mut impl Highlighter,
) {
let editor =
@@ -493,10 +449,12 @@ impl editor::Editor for Editor {
let mut font_system =
text::font_system().write().expect("Write font system");
+ let buffer = buffer_mut_from_editor(&mut internal.editor);
+
if font_system.version() != internal.version {
log::trace!("Updating `FontSystem` of `Editor`...");
- for line in internal.editor.buffer_mut().lines.iter_mut() {
+ for line in buffer.lines.iter_mut() {
line.reset();
}
@@ -507,7 +465,7 @@ impl editor::Editor for Editor {
if new_font != internal.font {
log::trace!("Updating font of `Editor`...");
- for line in internal.editor.buffer_mut().lines.iter_mut() {
+ for line in buffer.lines.iter_mut() {
let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
text::to_attributes(new_font),
));
@@ -517,7 +475,7 @@ impl editor::Editor for Editor {
internal.topmost_line_changed = Some(0);
}
- let metrics = internal.editor.buffer().metrics();
+ let metrics = buffer.metrics();
let new_line_height = new_line_height.to_absolute(new_size);
if new_size.0 != metrics.font_size
@@ -525,19 +483,27 @@ impl editor::Editor for Editor {
{
log::trace!("Updating `Metrics` of `Editor`...");
- internal.editor.buffer_mut().set_metrics(
+ buffer.set_metrics(
font_system.raw(),
cosmic_text::Metrics::new(new_size.0, new_line_height.0),
);
}
+ let new_wrap = text::to_wrap(new_wrapping);
+
+ if new_wrap != buffer.wrap() {
+ log::trace!("Updating `Wrap` strategy of `Editor`...");
+
+ buffer.set_wrap(font_system.raw(), new_wrap);
+ }
+
if new_bounds != internal.bounds {
log::trace!("Updating size of `Editor`...");
- internal.editor.buffer_mut().set_size(
+ buffer.set_size(
font_system.raw(),
- new_bounds.width,
- new_bounds.height,
+ Some(new_bounds.width),
+ Some(new_bounds.height),
);
internal.bounds = new_bounds;
@@ -552,7 +518,7 @@ impl editor::Editor for Editor {
new_highlighter.change_line(topmost_line_changed);
}
- internal.editor.shape_as_needed(font_system.raw());
+ internal.editor.shape_as_needed(font_system.raw(), false);
self.0 = Some(Arc::new(internal));
}
@@ -564,12 +530,13 @@ impl editor::Editor for Editor {
format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
) {
let internal = self.internal();
- let buffer = internal.editor.buffer();
+ let buffer = buffer_from_editor(&internal.editor);
- let mut window = buffer.scroll() + buffer.visible_lines();
+ let scroll = buffer.scroll();
+ let mut window = (internal.bounds.height / buffer.metrics().line_height)
+ .ceil() as i32;
- let last_visible_line = buffer
- .lines
+ let last_visible_line = buffer.lines[scroll.line..]
.iter()
.enumerate()
.find_map(|(i, line)| {
@@ -583,7 +550,7 @@ impl editor::Editor for Editor {
window -= visible_lines;
None
} else {
- Some(i)
+ Some(scroll.line + i)
}
})
.unwrap_or(buffer.lines.len().saturating_sub(1));
@@ -605,7 +572,7 @@ impl editor::Editor for Editor {
let attributes = text::to_attributes(font);
- for line in &mut internal.editor.buffer_mut().lines
+ for line in &mut buffer_mut_from_editor(&mut internal.editor).lines
[current_line..=last_visible_line]
{
let mut list = cosmic_text::AttrsList::new(attributes);
@@ -631,7 +598,7 @@ impl editor::Editor for Editor {
let _ = line.set_attrs_list(list);
}
- internal.editor.shape_as_needed(font_system.raw());
+ internal.editor.shape_as_needed(font_system.raw(), false);
self.0 = Some(Arc::new(internal));
}
@@ -647,7 +614,8 @@ impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool {
self.font == other.font
&& self.bounds == other.bounds
- && self.editor.buffer().metrics() == other.editor.buffer().metrics()
+ && buffer_from_editor(&self.editor).metrics()
+ == buffer_from_editor(&other.editor).metrics()
}
}
@@ -709,7 +677,8 @@ fn highlight_line(
let layout = line
.layout_opt()
.as_ref()
- .expect("Line layout should be cached");
+ .map(Vec::as_slice)
+ .unwrap_or_default();
layout.iter().map(move |visual_line| {
let start = visual_line
@@ -752,34 +721,61 @@ fn highlight_line(
}
fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
- let visual_lines_before_start: usize = buffer
- .lines
+ let scroll = buffer.scroll();
+
+ let start = scroll.line.min(line);
+ let end = scroll.line.max(line);
+
+ let visual_lines_offset: usize = buffer.lines[start..]
.iter()
- .take(line)
+ .take(end - start)
.map(|line| {
- line.layout_opt()
- .as_ref()
- .expect("Line layout should be cached")
- .len()
+ line.layout_opt().as_ref().map(Vec::len).unwrap_or_default()
})
.sum();
- visual_lines_before_start as i32 - buffer.scroll()
+ visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
}
-fn motion_to_action(motion: Motion) -> cosmic_text::Action {
+fn to_motion(motion: Motion) -> cosmic_text::Motion {
match motion {
- Motion::Left => cosmic_text::Action::Left,
- Motion::Right => cosmic_text::Action::Right,
- Motion::Up => cosmic_text::Action::Up,
- Motion::Down => cosmic_text::Action::Down,
- Motion::WordLeft => cosmic_text::Action::LeftWord,
- Motion::WordRight => cosmic_text::Action::RightWord,
- Motion::Home => cosmic_text::Action::Home,
- Motion::End => cosmic_text::Action::End,
- Motion::PageUp => cosmic_text::Action::PageUp,
- Motion::PageDown => cosmic_text::Action::PageDown,
- Motion::DocumentStart => cosmic_text::Action::BufferStart,
- Motion::DocumentEnd => cosmic_text::Action::BufferEnd,
+ Motion::Left => cosmic_text::Motion::Left,
+ Motion::Right => cosmic_text::Motion::Right,
+ Motion::Up => cosmic_text::Motion::Up,
+ Motion::Down => cosmic_text::Motion::Down,
+ Motion::WordLeft => cosmic_text::Motion::LeftWord,
+ Motion::WordRight => cosmic_text::Motion::RightWord,
+ Motion::Home => cosmic_text::Motion::Home,
+ Motion::End => cosmic_text::Motion::End,
+ Motion::PageUp => cosmic_text::Motion::PageUp,
+ Motion::PageDown => cosmic_text::Motion::PageDown,
+ Motion::DocumentStart => cosmic_text::Motion::BufferStart,
+ Motion::DocumentEnd => cosmic_text::Motion::BufferEnd,
+ }
+}
+
+fn buffer_from_editor<'a, 'b>(
+ editor: &'a impl cosmic_text::Edit<'b>,
+) -> &'a cosmic_text::Buffer
+where
+ 'b: 'a,
+{
+ match editor.buffer_ref() {
+ cosmic_text::BufferRef::Owned(buffer) => buffer,
+ cosmic_text::BufferRef::Borrowed(buffer) => buffer,
+ cosmic_text::BufferRef::Arc(buffer) => buffer,
+ }
+}
+
+fn buffer_mut_from_editor<'a, 'b>(
+ editor: &'a mut impl cosmic_text::Edit<'b>,
+) -> &'a mut cosmic_text::Buffer
+where
+ 'b: 'a,
+{
+ match editor.buffer_ref_mut() {
+ cosmic_text::BufferRef::Owned(buffer) => buffer,
+ cosmic_text::BufferRef::Borrowed(buffer) => buffer,
+ cosmic_text::BufferRef::Arc(_buffer) => unreachable!(),
}
}
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
index 31a323ac..07ddbb82 100644
--- a/graphics/src/text/paragraph.rs
+++ b/graphics/src/text/paragraph.rs
@@ -1,8 +1,8 @@
//! Draw paragraphs.
use crate::core;
use crate::core::alignment;
-use crate::core::text::{Hit, LineHeight, Shaping, Text};
-use crate::core::{Font, Pixels, Point, Size};
+use crate::core::text::{Hit, Shaping, Span, Text, Wrapping};
+use crate::core::{Font, Point, Rectangle, Size};
use crate::text;
use std::fmt;
@@ -10,13 +10,14 @@ use std::sync::{self, Arc};
/// A bunch of text.
#[derive(Clone, PartialEq)]
-pub struct Paragraph(Option<Arc<Internal>>);
+pub struct Paragraph(Arc<Internal>);
+#[derive(Clone)]
struct Internal {
buffer: cosmic_text::Buffer,
- content: String, // TODO: Reuse from `buffer` (?)
font: Font,
shaping: Shaping,
+ wrapping: Wrapping,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
bounds: Size,
@@ -52,9 +53,7 @@ impl Paragraph {
}
fn internal(&self) -> &Arc<Internal> {
- self.0
- .as_ref()
- .expect("paragraph should always be initialized")
+ &self.0
}
}
@@ -62,7 +61,7 @@ impl core::text::Paragraph for Paragraph {
type Font = Font;
fn with_text(text: Text<&str>) -> Self {
- log::trace!("Allocating paragraph: {}", text.content);
+ log::trace!("Allocating plain paragraph: {}", text.content);
let mut font_system =
text::font_system().write().expect("Write font system");
@@ -77,8 +76,8 @@ impl core::text::Paragraph for Paragraph {
buffer.set_size(
font_system.raw(),
- text.bounds.width,
- text.bounds.height,
+ Some(text.bounds.width),
+ Some(text.bounds.height),
);
buffer.set_text(
@@ -90,73 +89,113 @@ impl core::text::Paragraph for Paragraph {
let min_bounds = text::measure(&buffer);
- Self(Some(Arc::new(Internal {
+ Self(Arc::new(Internal {
buffer,
- content: text.content.to_owned(),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
+ wrapping: text.wrapping,
bounds: text.bounds,
min_bounds,
version: font_system.version(),
- })))
+ }))
+ }
+
+ fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self {
+ log::trace!("Allocating rich paragraph: {} spans", text.content.len());
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let mut buffer = cosmic_text::Buffer::new(
+ font_system.raw(),
+ cosmic_text::Metrics::new(
+ text.size.into(),
+ text.line_height.to_absolute(text.size).into(),
+ ),
+ );
+
+ buffer.set_size(
+ font_system.raw(),
+ Some(text.bounds.width),
+ Some(text.bounds.height),
+ );
+
+ buffer.set_rich_text(
+ font_system.raw(),
+ text.content.iter().enumerate().map(|(i, span)| {
+ let attrs = text::to_attributes(span.font.unwrap_or(text.font));
+
+ let attrs = match (span.size, span.line_height) {
+ (None, None) => attrs,
+ _ => {
+ let size = span.size.unwrap_or(text.size);
+
+ attrs.metrics(cosmic_text::Metrics::new(
+ size.into(),
+ span.line_height
+ .unwrap_or(text.line_height)
+ .to_absolute(size)
+ .into(),
+ ))
+ }
+ };
+
+ let attrs = if let Some(color) = span.color {
+ attrs.color(text::to_color(color))
+ } else {
+ attrs
+ };
+
+ (span.text.as_ref(), attrs.metadata(i))
+ }),
+ text::to_attributes(text.font),
+ text::to_shaping(text.shaping),
+ );
+
+ let min_bounds = text::measure(&buffer);
+
+ Self(Arc::new(Internal {
+ buffer,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ wrapping: text.wrapping,
+ bounds: text.bounds,
+ min_bounds,
+ version: font_system.version(),
+ }))
}
fn resize(&mut self, new_bounds: Size) {
- let paragraph = self
- .0
- .take()
- .expect("paragraph should always be initialized");
-
- match Arc::try_unwrap(paragraph) {
- Ok(mut internal) => {
- let mut font_system =
- text::font_system().write().expect("Write font system");
-
- internal.buffer.set_size(
- font_system.raw(),
- new_bounds.width,
- new_bounds.height,
- );
-
- internal.bounds = new_bounds;
- internal.min_bounds = text::measure(&internal.buffer);
-
- self.0 = Some(Arc::new(internal));
- }
- Err(internal) => {
- let metrics = internal.buffer.metrics();
-
- // If there is a strong reference somewhere, we recompute the
- // buffer from scratch
- *self = Self::with_text(Text {
- content: &internal.content,
- bounds: internal.bounds,
- size: Pixels(metrics.font_size),
- line_height: LineHeight::Absolute(Pixels(
- metrics.line_height,
- )),
- font: internal.font,
- horizontal_alignment: internal.horizontal_alignment,
- vertical_alignment: internal.vertical_alignment,
- shaping: internal.shaping,
- });
- }
- }
+ let paragraph = Arc::make_mut(&mut self.0);
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ paragraph.buffer.set_size(
+ font_system.raw(),
+ Some(new_bounds.width),
+ Some(new_bounds.height),
+ );
+
+ paragraph.bounds = new_bounds;
+ paragraph.min_bounds = text::measure(&paragraph.buffer);
}
- fn compare(&self, text: Text<&str>) -> core::text::Difference {
+ fn compare(&self, text: Text<()>) -> core::text::Difference {
let font_system = text::font_system().read().expect("Read font system");
let paragraph = self.internal();
let metrics = paragraph.buffer.metrics();
if paragraph.version != font_system.version
- || paragraph.content != text.content
|| metrics.font_size != text.size.0
|| metrics.line_height != text.line_height.to_absolute(text.size).0
|| paragraph.font != text.font
|| paragraph.shaping != text.shaping
+ || paragraph.wrapping != text.wrapping
|| paragraph.horizontal_alignment != text.horizontal_alignment
|| paragraph.vertical_alignment != text.vertical_alignment
{
@@ -186,6 +225,87 @@ impl core::text::Paragraph for Paragraph {
Some(Hit::CharOffset(cursor.index))
}
+ fn hit_span(&self, point: Point) -> Option<usize> {
+ let internal = self.internal();
+
+ let cursor = internal.buffer.hit(point.x, point.y)?;
+ let line = internal.buffer.lines.get(cursor.line)?;
+
+ let mut last_glyph = None;
+ let mut glyphs = line
+ .layout_opt()
+ .as_ref()?
+ .iter()
+ .flat_map(|line| line.glyphs.iter())
+ .peekable();
+
+ while let Some(glyph) = glyphs.peek() {
+ if glyph.start <= cursor.index && cursor.index < glyph.end {
+ break;
+ }
+
+ last_glyph = glyphs.next();
+ }
+
+ let glyph = match cursor.affinity {
+ cosmic_text::Affinity::Before => last_glyph,
+ cosmic_text::Affinity::After => glyphs.next(),
+ }?;
+
+ Some(glyph.metadata)
+ }
+
+ fn span_bounds(&self, index: usize) -> Vec<Rectangle> {
+ let internal = self.internal();
+
+ let mut bounds = Vec::new();
+ let mut current_bounds = None;
+
+ let glyphs = internal
+ .buffer
+ .layout_runs()
+ .flat_map(|run| {
+ let line_top = run.line_top;
+ let line_height = run.line_height;
+
+ run.glyphs
+ .iter()
+ .map(move |glyph| (line_top, line_height, glyph))
+ })
+ .skip_while(|(_, _, glyph)| glyph.metadata != index)
+ .take_while(|(_, _, glyph)| glyph.metadata == index);
+
+ for (line_top, line_height, glyph) in glyphs {
+ let y = line_top + glyph.y;
+
+ let new_bounds = || {
+ Rectangle::new(
+ Point::new(glyph.x, y),
+ Size::new(
+ glyph.w,
+ glyph.line_height_opt.unwrap_or(line_height),
+ ),
+ )
+ };
+
+ match current_bounds.as_mut() {
+ None => {
+ current_bounds = Some(new_bounds());
+ }
+ Some(current_bounds) if y != current_bounds.y => {
+ bounds.push(*current_bounds);
+ *current_bounds = new_bounds();
+ }
+ Some(current_bounds) => {
+ current_bounds.width += glyph.w;
+ }
+ }
+ }
+
+ bounds.extend(current_bounds);
+ bounds
+ }
+
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
use unicode_segmentation::UnicodeSegmentation;
@@ -231,7 +351,7 @@ impl core::text::Paragraph for Paragraph {
impl Default for Paragraph {
fn default() -> Self {
- Self(Some(Arc::new(Internal::default())))
+ Self(Arc::new(Internal::default()))
}
}
@@ -240,7 +360,6 @@ impl fmt::Debug for Paragraph {
let paragraph = self.internal();
f.debug_struct("Paragraph")
- .field("content", &paragraph.content)
.field("font", &paragraph.font)
.field("shaping", &paragraph.shaping)
.field("horizontal_alignment", &paragraph.horizontal_alignment)
@@ -253,8 +372,7 @@ impl fmt::Debug for Paragraph {
impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool {
- self.content == other.content
- && self.font == other.font
+ self.font == other.font
&& self.shaping == other.shaping
&& self.horizontal_alignment == other.horizontal_alignment
&& self.vertical_alignment == other.vertical_alignment
@@ -271,9 +389,9 @@ impl Default for Internal {
font_size: 1.0,
line_height: 1.0,
}),
- content: String::new(),
font: Font::default(),
shaping: Shaping::default(),
+ wrapping: Wrapping::default(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
bounds: Size::ZERO,
@@ -298,7 +416,7 @@ pub struct Weak {
impl Weak {
/// Tries to update the reference into a [`Paragraph`].
pub fn upgrade(&self) -> Option<Paragraph> {
- self.raw.upgrade().map(Some).map(Paragraph)
+ self.raw.upgrade().map(Paragraph)
}
}