Merge branch 'master' into beacon

This commit is contained in:
Héctor Ramón Jiménez 2025-03-04 19:11:37 +01:00
commit 8bd5de72ea
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
371 changed files with 33138 additions and 12950 deletions

View file

@ -20,6 +20,7 @@ all-features = true
[features]
geometry = ["lyon_path"]
image = ["dep:image", "kamadak-exif"]
svg = []
web-colors = []
fira-sans = []
@ -32,7 +33,6 @@ bytemuck.workspace = true
cosmic-text.workspace = true
half.workspace = true
log.workspace = true
once_cell.workspace = true
raw-window-handle.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true

View file

@ -1,6 +1,7 @@
//! Cache computations and efficiently reuse them.
use std::cell::RefCell;
use std::fmt;
use std::mem;
use std::sync::atomic::{self, AtomicU64};
/// A simple cache that stores generated values to avoid recomputation.
@ -58,18 +59,18 @@ impl<T> Cache<T> {
}
/// Clears the [`Cache`].
pub fn clear(&self)
where
T: Clone,
{
use std::ops::Deref;
pub fn clear(&self) {
let mut state = self.state.borrow_mut();
let previous = match self.state.borrow().deref() {
State::Empty { previous } => previous.clone(),
State::Filled { current } => Some(current.clone()),
let previous =
mem::replace(&mut *state, State::Empty { previous: None });
let previous = match previous {
State::Empty { previous } => previous,
State::Filled { current } => Some(current),
};
*self.state.borrow_mut() = State::Empty { previous };
*state = State::Empty { previous };
}
}

View file

@ -8,7 +8,6 @@ use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use thiserror::Error;
use std::borrow::Cow;
use std::future::Future;
/// A graphics compositor that can draw to windows.
pub trait Compositor: Sized {
@ -89,7 +88,6 @@ pub trait Compositor: Sized {
fn screenshot(
&mut self,
renderer: &mut Self::Renderer,
surface: &mut Self::Surface,
viewport: &Viewport,
background_color: Color,
) -> Vec<u8>;
@ -119,9 +117,7 @@ pub trait Default {
#[derive(Clone, PartialEq, Eq, Debug, Error)]
pub enum SurfaceError {
/// A timeout was encountered while trying to acquire the next frame.
#[error(
"A timeout was encountered while trying to acquire the next frame"
)]
#[error("A timeout was encountered while trying to acquire the next frame")]
Timeout,
/// The underlying surface has changed, and therefore the surface must be updated.
#[error(
@ -153,7 +149,7 @@ impl Compositor for () {
async fn with_backend<W: Window + Clone>(
_settings: Settings,
_compatible_window: W,
_preffered_backend: Option<&str>,
_preferred_backend: Option<&str>,
) -> Result<Self, Error> {
Ok(())
}
@ -198,7 +194,6 @@ impl Compositor for () {
fn screenshot(
&mut self,
_renderer: &mut Self::Renderer,
_surface: &mut Self::Surface,
_viewport: &Viewport,
_background_color: Color,
) -> Vec<u8> {

View file

@ -45,15 +45,12 @@ pub fn list<T>(
/// Groups the given damage regions that are close together inside the given
/// bounds.
pub fn group(mut damage: Vec<Rectangle>, bounds: Rectangle) -> Vec<Rectangle> {
use std::cmp::Ordering;
const AREA_THRESHOLD: f32 = 20_000.0;
damage.sort_by(|a, b| {
a.center()
.distance(Point::ORIGIN)
.partial_cmp(&b.center().distance(Point::ORIGIN))
.unwrap_or(Ordering::Equal)
.total_cmp(&b.center().distance(Point::ORIGIN))
});
let mut output = Vec::new();

View file

@ -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;

View file

@ -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.
///

View file

@ -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 {}
}

View file

@ -9,7 +9,8 @@ pub use builder::Builder;
pub use lyon_path;
use iced_core::{Point, Size};
use crate::core::border;
use crate::core::{Point, Size};
/// An immutable set of points that may or may not be connected.
///
@ -47,6 +48,16 @@ impl Path {
Self::new(|p| p.rectangle(top_left, size))
}
/// Creates a new [`Path`] representing a rounded rectangle given its top-left
/// corner coordinate, its [`Size`] and [`border::Radius`].
pub fn rounded_rectangle(
top_left: Point,
size: Size,
radius: border::Radius,
) -> Self {
Self::new(|p| p.rounded_rectangle(top_left, size, radius))
}
/// Creates a new [`Path`] representing a circle given its center
/// coordinate and its radius.
pub fn circle(center: Point, radius: f32) -> Self {

View file

@ -1,6 +1,7 @@
use crate::geometry::path::{arc, Arc, Path};
use crate::geometry::path::{Arc, Path, arc};
use iced_core::{Point, Radians, Size};
use crate::core::border;
use crate::core::{Point, Radians, Size};
use lyon_path::builder::{self, SvgPathBuilder};
use lyon_path::geom;
@ -160,6 +161,75 @@ impl Builder {
self.close();
}
/// Adds a rounded rectangle to the [`Path`] given its top-left
/// corner coordinate its [`Size`] and [`border::Radius`].
#[inline]
pub fn rounded_rectangle(
&mut self,
top_left: Point,
size: Size,
radius: border::Radius,
) {
let min_size = (size.height / 2.0).min(size.width / 2.0);
let [
top_left_corner,
top_right_corner,
bottom_right_corner,
bottom_left_corner,
] = radius.into();
self.move_to(Point::new(
top_left.x + min_size.min(top_left_corner),
top_left.y,
));
self.line_to(Point::new(
top_left.x + size.width - min_size.min(top_right_corner),
top_left.y,
));
self.arc_to(
Point::new(top_left.x + size.width, top_left.y),
Point::new(
top_left.x + size.width,
top_left.y + min_size.min(top_right_corner),
),
min_size.min(top_right_corner),
);
self.line_to(Point::new(
top_left.x + size.width,
top_left.y + size.height - min_size.min(bottom_right_corner),
));
self.arc_to(
Point::new(top_left.x + size.width, top_left.y + size.height),
Point::new(
top_left.x + size.width - min_size.min(bottom_right_corner),
top_left.y + size.height,
),
min_size.min(bottom_right_corner),
);
self.line_to(Point::new(
top_left.x + min_size.min(bottom_left_corner),
top_left.y + size.height,
));
self.arc_to(
Point::new(top_left.x, top_left.y + size.height),
Point::new(
top_left.x,
top_left.y + size.height - min_size.min(bottom_left_corner),
),
min_size.min(bottom_left_corner),
);
self.line_to(Point::new(
top_left.x,
top_left.y + min_size.min(top_left_corner),
));
self.arc_to(
Point::new(top_left.x, top_left.y),
Point::new(top_left.x + min_size.min(top_left_corner), top_left.y),
min_size.min(top_left_corner),
);
self.close();
}
/// Adds a circle to the [`Path`] given its center coordinate and its
/// radius.
#[inline]

View file

@ -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.
///
@ -23,7 +23,7 @@ pub struct Stroke<'a> {
pub line_dash: LineDash<'a>,
}
impl<'a> Stroke<'a> {
impl Stroke<'_> {
/// Sets the color of the [`Stroke`].
pub fn with_color(self, color: Color) -> Self {
Stroke {
@ -48,7 +48,7 @@ impl<'a> Stroke<'a> {
}
}
impl<'a> Default for Stroke<'a> {
impl Default for Stroke<'_> {
fn default() -> Self {
Stroke {
style: Style::Solid(Color::BLACK),

View file

@ -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),

View file

@ -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 {

View file

@ -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`].

View file

@ -2,57 +2,26 @@
#[cfg(feature = "image")]
pub use ::image as image_rs;
use crate::core::{image, svg, Color, Radians, Rectangle};
use crate::core::Rectangle;
use crate::core::image;
use crate::core::svg;
/// 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),
}
}
}

View file

@ -1,5 +1,5 @@
use crate::core::{Font, Pixels};
use crate::Antialiasing;
use crate::core::{Font, Pixels};
/// The settings of a renderer.
#[derive(Debug, Clone, Copy, PartialEq)]

View file

@ -11,12 +11,12 @@ 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;
use std::borrow::Cow;
use std::sync::{Arc, RwLock, Weak};
use std::collections::HashSet;
use std::sync::{Arc, OnceLock, RwLock, Weak};
/// A text primitive.
#[derive(Debug, Clone, PartialEq)]
@ -146,16 +146,17 @@ impl Text {
/// The regular variant of the [Fira Sans] font.
///
/// It is loaded as part of the default fonts in Wasm builds.
/// It is loaded as part of the default fonts when the `fira-sans`
/// feature is enabled.
///
/// [Fira Sans]: https://mozilla.github.io/Fira/
#[cfg(all(target_arch = "wasm32", feature = "fira-sans"))]
pub const FIRA_SANS_REGULAR: &'static [u8] =
#[cfg(feature = "fira-sans")]
pub const FIRA_SANS_REGULAR: &[u8] =
include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice();
/// Returns the global [`FontSystem`].
pub fn font_system() -> &'static RwLock<FontSystem> {
static FONT_SYSTEM: OnceCell<RwLock<FontSystem>> = OnceCell::new();
static FONT_SYSTEM: OnceLock<RwLock<FontSystem>> = OnceLock::new();
FONT_SYSTEM.get_or_init(|| {
RwLock::new(FontSystem {
@ -163,11 +164,12 @@ pub fn font_system() -> &'static RwLock<FontSystem> {
cosmic_text::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
)),
#[cfg(all(target_arch = "wasm32", feature = "fira-sans"))]
#[cfg(feature = "fira-sans")]
cosmic_text::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(),
)),
]),
loaded_fonts: HashSet::new(),
version: Version::default(),
})
})
@ -177,6 +179,7 @@ pub fn font_system() -> &'static RwLock<FontSystem> {
#[allow(missing_debug_implementations)]
pub struct FontSystem {
raw: cosmic_text::FontSystem,
loaded_fonts: HashSet<usize>,
version: Version,
}
@ -188,6 +191,14 @@ impl FontSystem {
/// Loads a font from its bytes.
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
if let Cow::Borrowed(bytes) = bytes {
let address = bytes.as_ptr() as usize;
if !self.loaded_fonts.insert(address) {
return;
}
}
let _ = self.raw.db_mut().load_font_source(
cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
);
@ -232,13 +243,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 +317,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();

View file

@ -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,

View file

@ -3,21 +3,23 @@ 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;
use cosmic_text::Edit as _;
use std::borrow::Cow;
use std::fmt;
use std::sync::{self, Arc};
use std::sync::{self, Arc, RwLock};
/// A multi-line text editor.
#[derive(Debug, PartialEq)]
pub struct Editor(Option<Arc<Internal>>);
struct Internal {
editor: cosmic_text::Editor,
editor: cosmic_text::Editor<'static>,
cursor: RwLock<Option<Cursor>>,
font: Font,
bounds: Size,
topmost_line_changed: Option<usize>,
@ -32,7 +34,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,11 +84,24 @@ impl editor::Editor for Editor {
})))
}
fn line(&self, index: usize) -> Option<&str> {
self.buffer()
.lines
.get(index)
.map(cosmic_text::BufferLine::text)
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<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 {
@ -100,17 +115,15 @@ impl editor::Editor for Editor {
fn cursor(&self) -> editor::Cursor {
let internal = self.internal();
if let Ok(Some(cursor)) = internal.cursor.read().as_deref() {
return cursor.clone();
}
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);
let cursor = 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 +155,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,10 +238,16 @@ 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,
))
}
}
};
*internal.cursor.write().expect("Write to cursor cache") =
Some(cursor.clone());
cursor
}
fn cursor_position(&self) -> (usize, usize) {
@ -249,19 +269,18 @@ impl editor::Editor for Editor {
let editor = &mut internal.editor;
// Clear cursor cache
let _ = internal
.cursor
.write()
.expect("Write to cursor cache")
.take();
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 +291,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 +312,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_cursor(cosmic_text::Cursor { index: 0, ..cursor });
editor.set_selection(cosmic_text::Selection::Line(cursor));
}
Action::SelectAll => {
let buffer = buffer_from_editor(editor);
editor.set_select_opt(Some(cosmic_text::Cursor {
index: line_length,
..cursor
}));
if buffer.lines.len() > 1
|| buffer
.lines
.first()
.is_some_and(|line| !line.text().is_empty())
{
let cursor = editor.cursor();
editor.set_selection(cosmic_text::Selection::Normal(
cosmic_text::Cursor {
line: 0,
index: 0,
..cursor
},
));
editor.action(
font_system.raw(),
cosmic_text::Action::Motion(
cosmic_text::Motion::BufferEnd,
),
);
}
}
@ -419,10 +400,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,25 +428,17 @@ 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);
}
}
}
Action::Scroll { lines } => {
let (_, height) = editor.buffer().size();
if height < i32::MAX as f32 {
editor.action(
font_system.raw(),
cosmic_text::Action::Scroll { lines },
);
}
editor.action(
font_system.raw(),
cosmic_text::Action::Scroll { lines },
);
}
}
@ -477,7 +452,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(
@ -486,6 +461,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 =
@ -497,10 +473,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();
}
@ -511,7 +489,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),
));
@ -521,7 +499,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
@ -529,19 +507,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;
@ -556,7 +542,14 @@ 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);
// Clear cursor cache
let _ = internal
.cursor
.write()
.expect("Write to cursor cache")
.take();
self.0 = Some(Arc::new(internal));
}
@ -568,12 +561,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)| {
@ -587,7 +581,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));
@ -609,7 +603,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);
@ -635,7 +629,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));
}
@ -651,7 +645,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()
}
}
@ -664,6 +659,7 @@ impl Default for Internal {
line_height: 1.0,
},
)),
cursor: RwLock::new(None),
font: Font::default(),
bounds: Size::ZERO,
topmost_line_changed: None,
@ -713,7 +709,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
@ -756,34 +753,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!(),
}
}

View file

@ -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,10 +76,12 @@ 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_wrap(font_system.raw(), text::to_wrap(text.wrapping));
buffer.set_text(
font_system.raw(),
text.content,
@ -90,73 +91,115 @@ 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_wrap(font_system.raw(), text::to_wrap(text.wrapping));
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");
let paragraph = Arc::make_mut(&mut self.0);
match Arc::try_unwrap(paragraph) {
Ok(mut internal) => {
let mut font_system =
text::font_system().write().expect("Write font system");
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,
);
paragraph.buffer.set_size(
font_system.raw(),
Some(new_bounds.width),
Some(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,
});
}
}
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 +229,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 +355,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 +364,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 +376,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 +393,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 +420,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)
}
}