Merge branch 'master' into beacon
This commit is contained in:
commit
8bd5de72ea
371 changed files with 33138 additions and 12950 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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`].
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(¶graph.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", ¶graph.content)
|
||||
.field("font", ¶graph.font)
|
||||
.field("shaping", ¶graph.shaping)
|
||||
.field("horizontal_alignment", ¶graph.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue