diff --git a/Cargo.toml b/Cargo.toml index 27b2f29..8a9de0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,9 @@ nom = "7.1" chrono = { version = "0.4", features = ["serde"] } rayon = "1.8" +[patch.crates-io] +iced_graphics = { path = "patches/iced_graphics" } + [profile.release] lto = "thin" strip = true diff --git a/crates/x-adox-core/src/scenery/sorter.rs b/crates/x-adox-core/src/scenery/sorter.rs index dd63330..1fd86e6 100644 --- a/crates/x-adox-core/src/scenery/sorter.rs +++ b/crates/x-adox-core/src/scenery/sorter.rs @@ -78,7 +78,11 @@ pub fn sort_packs( } ord => return ord, } + } else { + return std::cmp::Ordering::Less; } + } else if extract_simheaven_info(&b.name).is_some() { + return std::cmp::Ordering::Greater; } // Pure Stability: items with the same score tier stay exactly where they were diff --git a/patches/iced_graphics/Cargo.toml b/patches/iced_graphics/Cargo.toml new file mode 100644 index 0000000..2921217 --- /dev/null +++ b/patches/iced_graphics/Cargo.toml @@ -0,0 +1,133 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "iced_graphics" +version = "0.13.0" +authors = ["Héctor Ramón Jiménez "] +build = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for iced" +homepage = "https://iced.rs" +readme = false +keywords = [ + "gui", + "ui", + "graphics", + "interface", + "widgets", +] +categories = ["gui"] +license = "MIT" +repository = "https://github.com/iced-rs/iced" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[lib] +name = "iced_graphics" +path = "src/lib.rs" + +[dependencies.bitflags] +version = "2.0" + +[dependencies.bytemuck] +version = "1.0" +features = ["derive"] + +[dependencies.cosmic-text] +version = "0.12" + +[dependencies.half] +version = "2.2" + +[dependencies.iced_core] +version = "0.13.0" + +[dependencies.iced_futures] +version = "0.13.0" + +[dependencies.image] +version = "0.24" +optional = true +default-features = false + +[dependencies.kamadak-exif] +version = "0.5" +optional = true + +[dependencies.log] +version = "0.4" + +[dependencies.lyon_path] +version = "1.0" +optional = true + +[dependencies.once_cell] +version = "1.0" + +[dependencies.raw-window-handle] +version = "0.6" + +[dependencies.rustc-hash] +version = "2.0" + +[dependencies.thiserror] +version = "1.0" + +[dependencies.unicode-segmentation] +version = "1.0" + +[features] +fira-sans = [] +geometry = ["lyon_path"] +image = [ + "dep:image", + "kamadak-exif", +] +svg = [] +web-colors = [] + +[lints.clippy] +default_trait_access = "deny" +filter_map_next = "deny" +from_over_into = "deny" +manual_let_else = "deny" +match-wildcard-for-single-variants = "deny" +needless_borrow = "deny" +new_without_default = "deny" +redundant-closure-for-method-calls = "deny" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +type-complexity = "allow" +unused_async = "deny" +useless_conversion = "deny" + +[lints.rust] +missing_debug_implementations = "deny" +missing_docs = "deny" +unsafe_code = "deny" +unused_results = "deny" + +[lints.rust.rust_2018_idioms] +level = "forbid" +priority = -1 + +[lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/patches/iced_graphics/Cargo.toml.orig b/patches/iced_graphics/Cargo.toml.orig new file mode 100644 index 0000000..7e2d767 --- /dev/null +++ b/patches/iced_graphics/Cargo.toml.orig @@ -0,0 +1,49 @@ +[package] +name = "iced_graphics" +description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for iced" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true + +[features] +geometry = ["lyon_path"] +image = ["dep:image", "kamadak-exif"] +svg = [] +web-colors = [] +fira-sans = [] + +[dependencies] +iced_core.workspace = true +iced_futures.workspace = true + +bitflags.workspace = true +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 +unicode-segmentation.workspace = true + +image.workspace = true +image.optional = true + +kamadak-exif.workspace = true +kamadak-exif.optional = true + +lyon_path.workspace = true +lyon_path.optional = true diff --git a/patches/iced_graphics/fonts/FiraSans-Regular.ttf b/patches/iced_graphics/fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000..6f80647 Binary files /dev/null and b/patches/iced_graphics/fonts/FiraSans-Regular.ttf differ diff --git a/patches/iced_graphics/fonts/Iced-Icons.ttf b/patches/iced_graphics/fonts/Iced-Icons.ttf new file mode 100644 index 0000000..e327314 Binary files /dev/null and b/patches/iced_graphics/fonts/Iced-Icons.ttf differ diff --git a/patches/iced_graphics/src/antialiasing.rs b/patches/iced_graphics/src/antialiasing.rs new file mode 100644 index 0000000..7631c97 --- /dev/null +++ b/patches/iced_graphics/src/antialiasing.rs @@ -0,0 +1,24 @@ +/// An antialiasing strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Antialiasing { + /// Multisample AA with 2 samples + MSAAx2, + /// Multisample AA with 4 samples + MSAAx4, + /// Multisample AA with 8 samples + MSAAx8, + /// Multisample AA with 16 samples + MSAAx16, +} + +impl Antialiasing { + /// Returns the amount of samples of the [`Antialiasing`]. + pub fn sample_count(self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/patches/iced_graphics/src/cache.rs b/patches/iced_graphics/src/cache.rs new file mode 100644 index 0000000..7db80a0 --- /dev/null +++ b/patches/iced_graphics/src/cache.rs @@ -0,0 +1,190 @@ +//! 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. +/// +/// Keeps track of the last generated value after clearing. +pub struct Cache { + group: Group, + state: RefCell>, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + group: Group::singleton(), + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + assert!( + !group.is_singleton(), + "The group {group:?} cannot be shared!" + ); + + Cache { + group, + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Returns the [`Group`] of the [`Cache`]. + pub fn group(&self) -> Group { + self.group + } + + /// Puts the given value in the [`Cache`]. + /// + /// Notice that, given this is a cache, a mutable reference is not + /// necessary to call this method. You can safely update the cache in + /// rendering code. + pub fn put(&self, value: T) { + *self.state.borrow_mut() = State::Filled { current: value }; + } + + /// Returns a reference cell to the internal [`State`] of the [`Cache`]. + pub fn state(&self) -> &RefCell> { + &self.state + } + + /// Clears the [`Cache`]. + pub fn clear(&self) { + let mut state = self.state.borrow_mut(); + + let previous = + mem::replace(&mut *state, State::Empty { previous: None }); + + let previous = match previous { + State::Empty { previous } => previous, + State::Filled { current } => Some(current), + }; + + *state = State::Empty { previous }; + } +} + +/// A cache group. +/// +/// Caches that share the same group generally change together. +/// +/// A cache group can be used to implement certain performance +/// optimizations during rendering, like batching or sharing atlases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Group { + id: u64, + is_singleton: bool, +} + +impl Group { + /// Generates a new unique cache [`Group`]. + pub fn unique() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + + Self { + id: NEXT.fetch_add(1, atomic::Ordering::Relaxed), + is_singleton: false, + } + } + + /// Returns `true` if the [`Group`] can only ever have a + /// single [`Cache`] in it. + /// + /// This is the default kind of [`Group`] assigned when using + /// [`Cache::new`]. + /// + /// Knowing that a [`Group`] will never be shared may be + /// useful for rendering backends to perform additional + /// optimizations. + pub fn is_singleton(self) -> bool { + self.is_singleton + } + + fn singleton() -> Self { + Self { + is_singleton: true, + ..Self::unique() + } + } +} + +impl fmt::Debug for Cache +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::ops::Deref; + + let state = self.state.borrow(); + + match state.deref() { + State::Empty { previous } => { + write!(f, "Cache::Empty {{ previous: {previous:?} }}") + } + State::Filled { current } => { + write!(f, "Cache::Filled {{ current: {current:?} }}") + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} + +/// The state of a [`Cache`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum State { + /// The [`Cache`] is empty. + Empty { + /// The previous value of the [`Cache`]. + previous: Option, + }, + /// The [`Cache`] is filled. + Filled { + /// The current value of the [`Cache`] + current: T, + }, +} + +/// A piece of data that can be cached. +pub trait Cached: Sized { + /// The type of cache produced. + type Cache: Clone; + + /// Loads the [`Cache`] into a proper instance. + /// + /// [`Cache`]: Self::Cache + fn load(cache: &Self::Cache) -> Self; + + /// Caches this value, producing its corresponding [`Cache`]. + /// + /// [`Cache`]: Self::Cache + fn cache(self, group: Group, previous: Option) -> Self::Cache; +} + +#[cfg(debug_assertions)] +impl Cached for () { + type Cache = (); + + fn load(_cache: &Self::Cache) -> Self {} + + fn cache( + self, + _group: Group, + _previous: Option, + ) -> Self::Cache { + } +} diff --git a/patches/iced_graphics/src/color.rs b/patches/iced_graphics/src/color.rs new file mode 100644 index 0000000..92448a6 --- /dev/null +++ b/patches/iced_graphics/src/color.rs @@ -0,0 +1,46 @@ +//! Manage colors for shaders. +use crate::core::Color; + +use bytemuck::{Pod, Zeroable}; + +/// A color packed as 4 floats representing RGBA channels. +#[derive(Debug, Clone, Copy, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct Packed([f32; 4]); + +impl Packed { + /// Returns the internal components of the [`Packed`] color. + pub fn components(self) -> [f32; 4] { + self.0 + } +} + +/// A flag that indicates whether the renderer should perform gamma correction. +pub const GAMMA_CORRECTION: bool = internal::GAMMA_CORRECTION; + +/// Packs a [`Color`]. +pub fn pack(color: impl Into) -> Packed { + Packed(internal::pack(color.into())) +} + +#[cfg(not(feature = "web-colors"))] +mod internal { + use crate::core::Color; + + pub const GAMMA_CORRECTION: bool = true; + + pub fn pack(color: Color) -> [f32; 4] { + color.into_linear() + } +} + +#[cfg(feature = "web-colors")] +mod internal { + use crate::core::Color; + + pub const GAMMA_CORRECTION: bool = false; + + pub fn pack(color: Color) -> [f32; 4] { + [color.r, color.g, color.b, color.a] + } +} diff --git a/patches/iced_graphics/src/compositor.rs b/patches/iced_graphics/src/compositor.rs new file mode 100644 index 0000000..47521eb --- /dev/null +++ b/patches/iced_graphics/src/compositor.rs @@ -0,0 +1,216 @@ +//! A compositor is responsible for initializing a renderer and managing window +//! surfaces. +use crate::core::Color; +use crate::futures::{MaybeSend, MaybeSync}; +use crate::{Error, Settings, Viewport}; + +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 { + /// The iced renderer of the backend. + type Renderer; + + /// The surface of the backend. + type Surface; + + /// Creates a new [`Compositor`]. + fn new( + settings: Settings, + compatible_window: W, + ) -> impl Future> { + Self::with_backend(settings, compatible_window, None) + } + + /// Creates a new [`Compositor`] with a backend preference. + /// + /// If the backend does not match the preference, it will return + /// [`Error::GraphicsAdapterNotFound`]. + fn with_backend( + _settings: Settings, + _compatible_window: W, + _backend: Option<&str>, + ) -> impl Future>; + + /// Creates a [`Self::Renderer`] for the [`Compositor`]. + fn create_renderer(&self) -> Self::Renderer; + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: Self::Surface + fn create_surface( + &mut self, + window: W, + width: u32, + height: u32, + ) -> Self::Surface; + + /// Configures a new [`Surface`] with the given dimensions. + /// + /// [`Surface`]: Self::Surface + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ); + + /// Returns [`Information`] used by this [`Compositor`]. + fn fetch_information(&self) -> Information; + + /// Loads a font from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>) { + crate::text::font_system() + .write() + .expect("Write to font system") + .load_font(font); + } + + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. + /// + /// [`Renderer`]: Self::Renderer + /// [`Surface`]: Self::Surface + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), SurfaceError>; + + /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of + /// the texture ordered as `RGBA` in the `sRGB` color space. + /// + /// [`Renderer`]: Self::Renderer + fn screenshot>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec; +} + +/// A window that can be used in a [`Compositor`]. +/// +/// This is just a convenient super trait of the `raw-window-handle` +/// traits. +pub trait Window: + HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + +impl Window for T where + T: HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + +/// Defines the default compositor of a renderer. +pub trait Default { + /// The compositor of the renderer. + type Compositor: Compositor; +} + +/// Result of an unsuccessful call to [`Compositor::present`]. +#[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" + )] + Timeout, + /// The underlying surface has changed, and therefore the surface must be updated. + #[error( + "The underlying surface has changed, and therefore the surface must be updated." + )] + Outdated, + /// The swap chain has been lost and needs to be recreated. + #[error("The surface has been lost and needs to be recreated")] + Lost, + /// There is no more memory left to allocate a new frame. + #[error("There is no more memory left to allocate a new frame")] + OutOfMemory, +} + +/// Contains information about the graphics (e.g. graphics adapter, graphics backend). +#[derive(Debug)] +pub struct Information { + /// Contains the graphics adapter. + pub adapter: String, + /// Contains the graphics backend. + pub backend: String, +} + +#[cfg(debug_assertions)] +impl Compositor for () { + type Renderer = (); + type Surface = (); + + async fn with_backend( + _settings: Settings, + _compatible_window: W, + _preffered_backend: Option<&str>, + ) -> Result { + Ok(()) + } + + fn create_renderer(&self) -> Self::Renderer {} + + fn create_surface( + &mut self, + _window: W, + _width: u32, + _height: u32, + ) -> Self::Surface { + } + + fn configure_surface( + &mut self, + _surface: &mut Self::Surface, + _width: u32, + _height: u32, + ) { + } + + fn load_font(&mut self, _font: Cow<'static, [u8]>) {} + + fn fetch_information(&self) -> Information { + Information { + adapter: String::from("Null Renderer"), + backend: String::from("Null"), + } + } + + fn present>( + &mut self, + _renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + _viewport: &Viewport, + _background_color: Color, + _overlay: &[T], + ) -> Result<(), SurfaceError> { + Ok(()) + } + + fn screenshot>( + &mut self, + _renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + _viewport: &Viewport, + _background_color: Color, + _overlay: &[T], + ) -> Vec { + vec![] + } +} + +#[cfg(debug_assertions)] +impl Default for () { + type Compositor = (); +} diff --git a/patches/iced_graphics/src/damage.rs b/patches/iced_graphics/src/damage.rs new file mode 100644 index 0000000..8aa4279 --- /dev/null +++ b/patches/iced_graphics/src/damage.rs @@ -0,0 +1,78 @@ +//! Compute the damage between frames. +use crate::core::{Point, Rectangle}; + +/// Diffs the damage regions given some previous and current primitives. +pub fn diff( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + diff: impl Fn(&T, &T) -> Vec, +) -> Vec { + let damage = previous.iter().zip(current).flat_map(|(a, b)| diff(a, b)); + + if previous.len() == current.len() { + damage.collect() + } else { + let (smaller, bigger) = if previous.len() < current.len() { + (previous, current) + } else { + (current, previous) + }; + + // Extend damage by the added/removed primitives + damage + .chain(bigger[smaller.len()..].iter().flat_map(bounds)) + .collect() + } +} + +/// Computes the damage regions given some previous and current primitives. +pub fn list( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + are_equal: impl Fn(&T, &T) -> bool, +) -> Vec { + diff(previous, current, &bounds, |a, b| { + if are_equal(a, b) { + vec![] + } else { + bounds(a).into_iter().chain(bounds(b)).collect() + } + }) +} + +/// Groups the given damage regions that are close together inside the given +/// bounds. +pub fn group(mut damage: Vec, bounds: Rectangle) -> Vec { + const AREA_THRESHOLD: f32 = 20_000.0; + + damage.sort_by(|a, b| { + a.center() + .distance(Point::ORIGIN) + .total_cmp(&b.center().distance(Point::ORIGIN)) + }); + + let mut output = Vec::new(); + let mut scaled = damage + .into_iter() + .filter_map(|region| region.intersection(&bounds)) + .filter(|region| region.width >= 1.0 && region.height >= 1.0); + + if let Some(mut current) = scaled.next() { + for region in scaled { + let union = current.union(®ion); + + if union.area() - current.area() - region.area() <= AREA_THRESHOLD { + current = union; + } else { + output.push(current); + current = region; + } + } + + output.push(current); + } + + output +} diff --git a/patches/iced_graphics/src/error.rs b/patches/iced_graphics/src/error.rs new file mode 100644 index 0000000..6ea1d3a --- /dev/null +++ b/patches/iced_graphics/src/error.rs @@ -0,0 +1,42 @@ +//! See what can go wrong when creating graphical backends. + +/// An error that occurred while creating an application's graphical context. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum Error { + /// The requested backend version is not supported. + #[error("the requested backend version is not supported")] + VersionNotSupported, + + /// Failed to find any pixel format that matches the criteria. + #[error("failed to find any pixel format that matches the criteria")] + NoAvailablePixelFormat, + + /// A suitable graphics adapter or device could not be found. + #[error("a suitable graphics adapter or device could not be found")] + GraphicsAdapterNotFound { + /// The name of the backend where the error happened + backend: &'static str, + /// The reason why this backend could not be used + reason: Reason, + }, + + /// An error occurred in the context's internal backend + #[error("an error occurred in the context's internal backend")] + BackendError(String), + + /// Multiple errors occurred + #[error("multiple errors occurred: {0:?}")] + List(Vec), +} + +/// The reason why a graphics adapter could not be found +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Reason { + /// The backend did not match the preference + DidNotMatch { + /// The preferred backend + preferred_backend: String, + }, + /// The request to create the backend failed + RequestFailed(String), +} diff --git a/patches/iced_graphics/src/geometry.rs b/patches/iced_graphics/src/geometry.rs new file mode 100644 index 0000000..2b4b45a --- /dev/null +++ b/patches/iced_graphics/src/geometry.rs @@ -0,0 +1,48 @@ +//! Build and draw geometry. +pub mod fill; +pub mod frame; +pub mod path; +pub mod stroke; + +mod cache; +mod style; +mod text; + +pub use cache::Cache; +pub use fill::Fill; +pub use frame::Frame; +pub use path::Path; +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; +use crate::core::{self, Size}; + +/// A renderer capable of drawing some [`Self::Geometry`]. +pub trait Renderer: core::Renderer { + /// The kind of geometry this renderer can draw. + type Geometry: Cached; + + /// The kind of [`Frame`] this renderer supports. + type Frame: frame::Backend; + + /// Creates a new [`Self::Frame`]. + fn new_frame(&self, size: Size) -> Self::Frame; + + /// Draws the given [`Self::Geometry`]. + fn draw_geometry(&mut self, geometry: Self::Geometry); +} + +#[cfg(debug_assertions)] +impl Renderer for () { + type Geometry = (); + type Frame = (); + + fn new_frame(&self, _size: Size) -> Self::Frame {} + + fn draw_geometry(&mut self, _geometry: Self::Geometry) {} +} diff --git a/patches/iced_graphics/src/geometry/cache.rs b/patches/iced_graphics/src/geometry/cache.rs new file mode 100644 index 0000000..d70cee0 --- /dev/null +++ b/patches/iced_graphics/src/geometry/cache.rs @@ -0,0 +1,116 @@ +use crate::cache::{self, Cached}; +use crate::core::Size; +use crate::geometry::{self, Frame}; + +pub use cache::Group; + +/// A simple cache that stores generated geometry to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +pub struct Cache +where + Renderer: geometry::Renderer, +{ + raw: crate::Cache::Cache>>, +} + +#[derive(Debug, Clone)] +struct Data { + bounds: Size, + geometry: T, +} + +impl Cache +where + Renderer: geometry::Renderer, +{ + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + raw: cache::Cache::new(), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + raw: crate::Cache::with_group(group), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + self.raw.clear(); + } + + /// Draws geometry using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored geometry will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored geometry if needed. + pub fn draw( + &self, + renderer: &Renderer, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Renderer::Geometry { + use std::ops::Deref; + + let state = self.raw.state(); + + let previous = match state.borrow().deref() { + cache::State::Empty { previous } => { + previous.as_ref().map(|data| data.geometry.clone()) + } + cache::State::Filled { current } => { + if current.bounds == bounds { + return Cached::load(¤t.geometry); + } + + Some(current.geometry.clone()) + } + }; + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let geometry = frame.into_geometry().cache(self.raw.group(), previous); + let result = Cached::load(&geometry); + + *state.borrow_mut() = cache::State::Filled { + current: Data { bounds, geometry }, + }; + + result + } +} + +impl std::fmt::Debug for Cache +where + Renderer: geometry::Renderer, + ::Cache: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &self.raw) + } +} + +impl Default for Cache +where + Renderer: geometry::Renderer, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/patches/iced_graphics/src/geometry/fill.rs b/patches/iced_graphics/src/geometry/fill.rs new file mode 100644 index 0000000..b79a258 --- /dev/null +++ b/patches/iced_graphics/src/geometry/fill.rs @@ -0,0 +1,75 @@ +//! Fill [`Geometry`] with a certain style. +//! +//! [`Geometry`]: super::Renderer::Geometry +pub use crate::geometry::Style; + +use crate::core::Color; +use crate::gradient::{self, Gradient}; + +/// The style used to fill geometry. +#[derive(Debug, Clone, Copy)] +pub struct Fill { + /// The color or gradient of the fill. + /// + /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: Rule, +} + +impl Default for Fill { + fn default() -> Self { + Self { + style: Style::Solid(Color::BLACK), + rule: Rule::NonZero, + } + } +} + +impl From for Fill { + fn from(color: Color) -> Fill { + Fill { + style: Style::Solid(color), + ..Fill::default() + } + } +} + +impl From for Fill { + fn from(gradient: Gradient) -> Self { + Fill { + style: Style::Gradient(gradient), + ..Default::default() + } + } +} + +impl From for Fill { + fn from(gradient: gradient::Linear) -> Self { + Fill { + style: Style::Gradient(Gradient::Linear(gradient)), + ..Default::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum Rule { + NonZero, + EvenOdd, +} diff --git a/patches/iced_graphics/src/geometry/frame.rs b/patches/iced_graphics/src/geometry/frame.rs new file mode 100644 index 0000000..3dee7e7 --- /dev/null +++ b/patches/iced_graphics/src/geometry/frame.rs @@ -0,0 +1,290 @@ +//! Draw and generate geometry. +use crate::core::{Point, Radians, Rectangle, Size, Vector}; +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)] +pub struct Frame +where + Renderer: geometry::Renderer, +{ + raw: Renderer::Frame, +} + +impl Frame +where + Renderer: geometry::Renderer, +{ + /// Creates a new [`Frame`] with the given dimensions. + pub fn new(renderer: &Renderer, size: Size) -> Self { + Self { + raw: renderer.new_frame(size), + } + } + + /// Returns the width of the [`Frame`]. + pub fn width(&self) -> f32 { + self.raw.width() + } + + /// Returns the height of the [`Frame`]. + pub fn height(&self) -> f32 { + self.raw.height() + } + + /// Returns the dimensions of the [`Frame`]. + pub fn size(&self) -> Size { + self.raw.size() + } + + /// Returns the coordinate of the center of the [`Frame`]. + pub fn center(&self) -> Point { + self.raw.center() + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + self.raw.fill(path, fill); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + self.raw.fill_rectangle(top_left, size, fill); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + 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>, + ) { + self.raw.stroke_rectangle(top_left, size, stroke); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ All text will be rendered on top of all the layers of + /// a `Canvas`. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + pub fn fill_text(&mut self, text: impl Into) { + 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) { + 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) { + self.raw.draw_svg(bounds, svg); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.push_transform(); + + let result = f(self); + + self.pop_transform(); + + result + } + + /// Pushes the current transform in the transform stack. + pub fn push_transform(&mut self) { + self.raw.push_transform(); + } + + /// Pops a transform from the transform stack and sets it as the current transform. + pub fn pop_transform(&mut self) { + self.raw.pop_transform(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip( + &mut self, + region: Rectangle, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + let mut frame = self.draft(region); + + let result = f(&mut frame); + self.paste(frame); + + result + } + + /// Creates a new [`Frame`] with the given [`Size`]. + /// + /// Draw its contents back to this [`Frame`] with [`paste`]. + /// + /// [`paste`]: Self::paste + fn draft(&mut self, clip_bounds: Rectangle) -> Self { + Self { + raw: self.raw.draft(clip_bounds), + } + } + + /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. + fn paste(&mut self, frame: Self) { + self.raw.paste(frame.raw); + } + + /// Applies a translation to the current transform of the [`Frame`]. + pub fn translate(&mut self, translation: Vector) { + self.raw.translate(translation); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + pub fn rotate(&mut self, angle: impl Into) { + self.raw.rotate(angle); + } + + /// Applies a uniform scaling to the current transform of the [`Frame`]. + pub fn scale(&mut self, scale: impl Into) { + self.raw.scale(scale); + } + + /// Applies a non-uniform scaling to the current transform of the [`Frame`]. + pub fn scale_nonuniform(&mut self, scale: impl Into) { + self.raw.scale_nonuniform(scale); + } + + /// Turns the [`Frame`] into its underlying geometry. + pub fn into_geometry(self) -> Renderer::Geometry { + self.raw.into_geometry() + } +} + +/// The internal implementation of a [`Frame`]. +/// +/// Analogous to [`Frame`]. See [`Frame`] for the documentation +/// of each method. +#[allow(missing_docs)] +pub trait Backend: Sized { + type Geometry; + + fn width(&self) -> f32; + fn height(&self) -> f32; + fn size(&self) -> Size; + fn center(&self) -> Point; + + fn push_transform(&mut self); + fn pop_transform(&mut self); + + fn translate(&mut self, translation: Vector); + fn rotate(&mut self, angle: impl Into); + fn scale(&mut self, scale: impl Into); + fn scale_nonuniform(&mut self, scale: impl Into); + + fn draft(&mut self, clip_bounds: Rectangle) -> Self; + fn paste(&mut self, frame: Self); + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>); + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ); + + fn fill(&mut self, path: &Path, fill: impl Into); + fn fill_text(&mut self, text: impl Into); + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ); + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into); + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into); + + fn into_geometry(self) -> Self::Geometry; +} + +#[cfg(debug_assertions)] +impl Backend for () { + type Geometry = (); + + fn width(&self) -> f32 { + 0.0 + } + + fn height(&self) -> f32 { + 0.0 + } + + fn size(&self) -> Size { + Size::ZERO + } + + fn center(&self) -> Point { + Point::ORIGIN + } + + fn push_transform(&mut self) {} + fn pop_transform(&mut self) {} + + fn translate(&mut self, _translation: Vector) {} + fn rotate(&mut self, _angle: impl Into) {} + fn scale(&mut self, _scale: impl Into) {} + fn scale_nonuniform(&mut self, _scale: impl Into) {} + + fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} + fn paste(&mut self, _frame: Self) {} + + fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into>) {} + fn stroke_rectangle<'a>( + &mut self, + _top_left: Point, + _size: Size, + _stroke: impl Into>, + ) { + } + + fn fill(&mut self, _path: &Path, _fill: impl Into) {} + fn fill_text(&mut self, _text: impl Into) {} + fn fill_rectangle( + &mut self, + _top_left: Point, + _size: Size, + _fill: impl Into, + ) { + } + + fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into) {} + fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into) {} + + fn into_geometry(self) -> Self::Geometry {} +} diff --git a/patches/iced_graphics/src/geometry/path.rs b/patches/iced_graphics/src/geometry/path.rs new file mode 100644 index 0000000..c4f5159 --- /dev/null +++ b/patches/iced_graphics/src/geometry/path.rs @@ -0,0 +1,80 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +#[doc(no_inline)] +pub use arc::Arc; +pub use builder::Builder; + +pub use lyon_path; + +use crate::core::border; +use crate::core::{Point, Size}; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon_path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + /// Creates a new [`Path`] representing a line segment given its starting + /// and end points. + pub fn line(from: Point, to: Point) -> Self { + Self::new(|p| { + p.move_to(from); + p.line_to(to); + }) + } + + /// Creates a new [`Path`] representing a rectangle given its top-left + /// corner coordinate and its `Size`. + pub fn rectangle(top_left: Point, size: Size) -> Self { + 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 { + Self::new(|p| p.circle(center, radius)) + } + + /// Returns the internal [`lyon_path::Path`]. + #[inline] + pub fn raw(&self) -> &lyon_path::Path { + &self.raw + } + + /// Returns the current [`Path`] with the given transform applied to it. + #[inline] + pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { + Path { + raw: self.raw.clone().transformed(transform), + } + } +} diff --git a/patches/iced_graphics/src/geometry/path/arc.rs b/patches/iced_graphics/src/geometry/path/arc.rs new file mode 100644 index 0000000..2600497 --- /dev/null +++ b/patches/iced_graphics/src/geometry/path/arc.rs @@ -0,0 +1,42 @@ +//! Build and draw curves. +use iced_core::{Point, Radians, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation from positive x-axis. + pub start_angle: Radians, + /// The end of the segment's angle, clockwise rotation from positive x-axis. + pub end_angle: Radians, +} + +/// An elliptical [`Arc`]. +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse. The horizontal and vertical half-dimensions of the ellipse will match the x and y values of the radii vector. + pub radii: Vector, + /// The clockwise rotation of the arc's ellipse. + pub rotation: Radians, + /// The start of the segment's angle, clockwise rotation from positive x-axis. + pub start_angle: Radians, + /// The end of the segment's angle, clockwise rotation from positive x-axis. + pub end_angle: Radians, +} + +impl From for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: Radians(0.0), + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/patches/iced_graphics/src/geometry/path/builder.rs b/patches/iced_graphics/src/geometry/path/builder.rs new file mode 100644 index 0000000..44410f6 --- /dev/null +++ b/patches/iced_graphics/src/geometry/path/builder.rs @@ -0,0 +1,261 @@ +use crate::geometry::path::{arc, Arc, Path}; + +use crate::core::border; +use crate::core::{Point, Radians, Size}; + +use lyon_path::builder::{self, SvgPathBuilder}; +use lyon_path::geom; +use lyon_path::math; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: builder::WithSvg, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new() -> Builder { + Builder { + raw: lyon_path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// This essentially draws a straight line segment from the current + /// position to `a`, but fits a circular arc of `radius` tangent to that + /// segment and tangent to the line between `a` and `b`. + /// + /// With another `.line_to(b)`, the result will be a path connecting the + /// starting point and `b` with straight line segments towards `a` and a + /// circular arc smoothing out the corner at `a`. + /// + /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) + /// for more details and examples. + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + let start = self.raw.current_position(); + let mid = math::Point::new(a.x, a.y); + let end = math::Point::new(b.x, b.y); + + if start == mid || mid == end || radius == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let double_area = start.x * (mid.y - end.y) + + mid.x * (end.y - start.y) + + end.x * (start.y - mid.y); + + if double_area == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let to_start = (start - mid).normalize(); + let to_end = (end - mid).normalize(); + + let inner_angle = to_start.dot(to_end).acos(); + + let origin_angle = inner_angle / 2.0; + + let origin_adjacent = radius / origin_angle.tan(); + + let arc_start = mid + to_start * origin_adjacent; + let arc_end = mid + to_end * origin_adjacent; + + let sweep = to_start.cross(to_end) < 0.0; + + let _ = self.raw.line_to(arc_start); + + self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + lyon_path::ArcFlags { + large_arc: false, + sweep, + }, + arc_end, + ); + } + + /// Adds an ellipse to the [`Path`] using a clockwise direction. + pub fn ellipse(&mut self, arc: arc::Elliptical) { + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation.0), + start_angle: math::Angle::radians(arc.start_angle.0), + sweep_angle: math::Angle::radians( + (arc.end_angle - arc.start_angle).0, + ), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + #[inline] + pub fn rectangle(&mut self, top_left: Point, size: Size) { + self.move_to(top_left); + self.line_to(Point::new(top_left.x + size.width, top_left.y)); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height, + )); + self.line_to(Point::new(top_left.x, top_left.y + size.height)); + 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] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: Radians(0.0), + end_angle: Radians(2.0 * std::f32::consts::PI), + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + #[inline] + pub fn close(&mut self) { + self.raw.close(); + } + + /// Builds the [`Path`] of this [`Builder`]. + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/patches/iced_graphics/src/geometry/stroke.rs b/patches/iced_graphics/src/geometry/stroke.rs new file mode 100644 index 0000000..b8f4515 --- /dev/null +++ b/patches/iced_graphics/src/geometry/stroke.rs @@ -0,0 +1,98 @@ +//! Create lines from a [`Path`] and assigns them various attributes/styles. +//! +//! [`Path`]: super::Path +pub use crate::geometry::Style; + +use iced_core::Color; + +/// The style of a stroke. +#[derive(Debug, Clone, Copy)] +pub struct Stroke<'a> { + /// The color or gradient of the stroke. + /// + /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, + /// The dash pattern used when stroking the line. + pub line_dash: LineDash<'a>, +} + +impl<'a> Stroke<'a> { + /// Sets the color of the [`Stroke`]. + pub fn with_color(self, color: Color) -> Self { + Stroke { + style: Style::Solid(color), + ..self + } + } + + /// Sets the width of the [`Stroke`]. + pub fn with_width(self, width: f32) -> Self { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + pub fn with_line_cap(self, line_cap: LineCap) -> Self { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + pub fn with_line_join(self, line_join: LineJoin) -> Self { + Stroke { line_join, ..self } + } +} + +impl<'a> Default for Stroke<'a> { + fn default() -> Self { + Stroke { + style: Style::Solid(Color::BLACK), + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + line_dash: LineDash::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy, Default)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + #[default] + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy, Default)] +pub enum LineJoin { + /// A sharp corner. + #[default] + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +/// The dash pattern used when stroking the line. +#[derive(Debug, Clone, Copy, Default)] +pub struct LineDash<'a> { + /// The alternating lengths of lines and gaps which describe the pattern. + pub segments: &'a [f32], + + /// The offset of [`LineDash::segments`] to start the pattern. + pub offset: usize, +} diff --git a/patches/iced_graphics/src/geometry/style.rs b/patches/iced_graphics/src/geometry/style.rs new file mode 100644 index 0000000..de77ecc --- /dev/null +++ b/patches/iced_graphics/src/geometry/style.rs @@ -0,0 +1,24 @@ +use crate::core::Color; +use crate::geometry::Gradient; + +/// The coloring style of some drawing. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Style { + /// A solid [`Color`]. + Solid(Color), + + /// A [`Gradient`] color. + Gradient(Gradient), +} + +impl From for Style { + fn from(color: Color) -> Self { + Self::Solid(color) + } +} + +impl From for Style { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} diff --git a/patches/iced_graphics/src/geometry/text.rs b/patches/iced_graphics/src/geometry/text.rs new file mode 100644 index 0000000..90147f8 --- /dev/null +++ b/patches/iced_graphics/src/geometry/text.rs @@ -0,0 +1,200 @@ +use crate::core::alignment; +use crate::core::text::{LineHeight, Shaping}; +use crate::core::{Color, Font, Pixels, Point, Size, Vector}; +use crate::geometry::Path; +use crate::text; + +/// A bunch of text that can be drawn to a canvas +#[derive(Debug, Clone)] +pub struct Text { + /// The contents of the text + pub content: String, + /// The position of the text relative to the alignment properties. + /// By default, this position will be relative to the top-left corner coordinate meaning that + /// if the horizontal and vertical alignments are unchanged, this property will tell where the + /// top-left corner of the text should be placed. + /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to + /// change what part of text is placed at this positions. + /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the + /// center of the text will be placed at the given position NOT the top-left coordinate. + pub position: Point, + /// The color of the text + pub color: Color, + /// The size of the text + pub size: Pixels, + /// The line height of the text. + pub line_height: LineHeight, + /// The font of the text + pub font: Font, + /// The horizontal alignment of the text + pub horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the text + pub vertical_alignment: alignment::Vertical, + /// The shaping strategy of the text. + pub shaping: Shaping, +} + +impl Text { + /// Computes the [`Path`]s of the [`Text`] and draws them using + /// the given closure. + pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + 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), + ); + + let layout = buffer.layout( + font_system.raw(), + self.size.0, + None, + cosmic_text::Wrap::None, + None, + 4, + ); + + let translation_x = match self.horizontal_alignment { + alignment::Horizontal::Left => self.position.x, + alignment::Horizontal::Center | alignment::Horizontal::Right => { + let mut line_width = 0.0f32; + + for line in layout.iter() { + line_width = line_width.max(line.w); + } + + if self.horizontal_alignment == alignment::Horizontal::Center { + self.position.x - line_width / 2.0 + } else { + self.position.x - line_width + } + } + }; + + let translation_y = { + let line_height = self.line_height.to_absolute(self.size); + + match self.vertical_alignment { + alignment::Vertical::Top => self.position.y, + alignment::Vertical::Center => { + self.position.y - line_height.0 / 2.0 + } + alignment::Vertical::Bottom => self.position.y - line_height.0, + } + }; + + let mut swash_cache = cosmic_text::SwashCache::new(); + + for run in layout.iter() { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0.0, 0.0), 1.0); + + let start_x = translation_x + glyph.x + glyph.x_offset; + let start_y = translation_y + glyph.y_offset + self.size.0; + let offset = Vector::new(start_x, start_y); + + if let Some(commands) = swash_cache.get_outline_commands( + font_system.raw(), + physical_glyph.cache_key, + ) { + let glyph = Path::new(|path| { + use cosmic_text::Command; + + for command in commands { + match command { + Command::MoveTo(p) => { + path.move_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::LineTo(p) => { + path.line_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::CurveTo(control_a, control_b, to) => { + path.bezier_curve_to( + Point::new(control_a.x, -control_a.y) + + offset, + Point::new(control_b.x, -control_b.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::QuadTo(control, to) => { + path.quadratic_curve_to( + Point::new(control.x, -control.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::Close => { + path.close(); + } + } + } + }); + + f(glyph, self.color); + } else { + // TODO: Raster image support for `Canvas` + let [r, g, b, a] = self.color.into_rgba8(); + + swash_cache.with_pixels( + font_system.raw(), + physical_glyph.cache_key, + cosmic_text::Color::rgba(r, g, b, a), + |x, y, color| { + f( + Path::rectangle( + Point::new(x as f32, y as f32) + offset, + Size::new(1.0, 1.0), + ), + Color::from_rgba8( + color.r(), + color.g(), + color.b(), + color.a() as f32 / 255.0, + ), + ); + }, + ); + } + } + } + } +} + +impl Default for Text { + fn default() -> Text { + Text { + content: String::new(), + position: Point::ORIGIN, + color: Color::BLACK, + size: Pixels(16.0), + line_height: LineHeight::Relative(1.2), + font: Font::default(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: Shaping::Basic, + } + } +} + +impl From for Text { + fn from(content: String) -> Text { + Text { + content, + ..Default::default() + } + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Text { + String::from(content).into() + } +} diff --git a/patches/iced_graphics/src/gradient.rs b/patches/iced_graphics/src/gradient.rs new file mode 100644 index 0000000..5426172 --- /dev/null +++ b/patches/iced_graphics/src/gradient.rs @@ -0,0 +1,191 @@ +//! A gradient that can be used as a fill for some geometry. +//! +//! For a gradient that you can use as a background variant for a widget, see [`Gradient`]. +use crate::color; +use crate::core::gradient::ColorStop; +use crate::core::{self, Color, Point, Rectangle}; + +use bytemuck::{Pod, Zeroable}; +use half::f16; +use std::cmp::Ordering; + +#[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`]. +pub enum Gradient { + /// A linear gradient interpolates colors along a direction from its `start` to its `end` + /// point. + Linear(Linear), +} + +impl From for Gradient { + fn from(gradient: Linear) -> Self { + Self::Linear(gradient) + } +} + +impl Gradient { + /// Packs the [`Gradient`] for use in shader code. + pub fn pack(&self) -> Packed { + match self { + Gradient::Linear(linear) => linear.pack(), + } + } +} + +/// A linear gradient. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Linear { + /// The absolute starting position of the gradient. + pub start: Point, + + /// The absolute ending position of the gradient. + pub end: Point, + + /// [`ColorStop`]s along the linear gradient direction. + pub stops: [Option; 8], +} + +impl Linear { + /// Creates a new [`Linear`] builder. + pub fn new(start: Point, end: Point) -> Self { + Self { + start, + end, + stops: [None; 8], + } + } + + /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient. + /// + /// Any `offset` that is not within `0.0..=1.0` will be silently ignored. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + let (Ok(index) | Err(index)) = + self.stops.binary_search_by(|stop| match stop { + None => Ordering::Greater, + Some(stop) => stop.offset.partial_cmp(&offset).unwrap(), + }); + + if index < 8 { + self.stops[index] = Some(ColorStop { offset, color }); + } + } else { + log::warn!("Gradient: ColorStop must be within 0.0..=1.0 range."); + }; + + self + } + + /// Adds multiple [`ColorStop`]s to the gradient. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stops( + mut self, + stops: impl IntoIterator, + ) -> Self { + for stop in stops { + self = self.add_stop(stop.offset, stop.color); + } + + self + } + + /// Packs the [`Gradient`] for use in shader code. + pub fn pack(&self) -> Packed { + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; + + for (index, stop) in self.stops.iter().enumerate() { + let [r, g, b, a] = + color::pack(stop.map_or(Color::default(), |s| s.color)) + .components(); + + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; + + offsets[index] = + stop.map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); + } + + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; + + let direction = [self.start.x, self.start.y, self.end.x, self.end.y]; + + Packed { + colors, + offsets, + direction, + } + } +} + +/// Packed [`Gradient`] data for use in shader code. +#[derive(Debug, Copy, Clone, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct Packed { + // 8 colors, each channel = 16 bit float, 2 colors packed into 1 u32 + colors: [[u32; 2]; 8], + // 8 offsets, 8x 16 bit floats packed into 4 u32s + offsets: [u32; 4], + direction: [f32; 4], +} + +/// Creates a new [`Packed`] gradient for use in shader code. +pub fn pack(gradient: &core::Gradient, bounds: Rectangle) -> Packed { + match gradient { + core::Gradient::Linear(linear) => { + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; + + for (index, stop) in linear.stops.iter().enumerate() { + let [r, g, b, a] = + color::pack(stop.map_or(Color::default(), |s| s.color)) + .components(); + + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; + + offsets[index] = stop + .map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); + } + + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; + + let (start, end) = linear.angle.to_distance(&bounds); + + let direction = [start.x, start.y, end.x, end.y]; + + Packed { + colors, + offsets, + direction, + } + } + } +} + +/// Packs two f16s into one u32. +fn pack_f16s(f: [f16; 2]) -> u32 { + let one = (f[0].to_bits() as u32) << 16; + let two = f[1].to_bits() as u32; + + one | two +} diff --git a/patches/iced_graphics/src/image.rs b/patches/iced_graphics/src/image.rs new file mode 100644 index 0000000..67a5e0c --- /dev/null +++ b/patches/iced_graphics/src/image.rs @@ -0,0 +1,136 @@ +//! Load and operate on images. +#[cfg(feature = "image")] +pub use ::image as image_rs; + +use crate::core::image; +use crate::core::svg; +use crate::core::Rectangle; + +/// A raster or vector image. +#[derive(Debug, Clone, PartialEq)] +pub enum Image { + /// A raster image. + Raster(image::Image, Rectangle), + + /// A vector image. + Vector(svg::Svg, Rectangle), +} + +impl Image { + /// Returns the bounds of the [`Image`]. + pub fn bounds(&self) -> Rectangle { + match self { + Image::Raster(image, bounds) => bounds.rotate(image.rotation), + Image::Vector(svg, bounds) => bounds.rotate(svg.rotation), + } + } +} + +#[cfg(feature = "image")] +/// Tries to load an image by its [`Handle`]. +/// +/// [`Handle`]: image::Handle +pub fn load( + handle: &image::Handle, +) -> ::image::ImageResult<::image::ImageBuffer<::image::Rgba, image::Bytes>> +{ + use bitflags::bitflags; + + bitflags! { + struct Operation: u8 { + const FLIP_HORIZONTALLY = 0b001; + const ROTATE_180 = 0b010; + const FLIP_DIAGONALLY = 0b100; + } + } + + impl Operation { + // Meaning of the returned value is described e.g. at: + // https://magnushoff.com/articles/jpeg-orientation/ + fn from_exif(reader: &mut R) -> Result + where + R: std::io::BufRead + std::io::Seek, + { + let exif = exif::Reader::new().read_from_container(reader)?; + + Ok(exif + .get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|field| field.value.get_uint(0)) + .and_then(|value| u8::try_from(value).ok()) + .and_then(|value| Self::from_bits(value.saturating_sub(1))) + .unwrap_or_else(Self::empty)) + } + + fn perform( + self, + mut image: ::image::DynamicImage, + ) -> ::image::DynamicImage { + use ::image::imageops; + + if self.contains(Self::FLIP_DIAGONALLY) { + imageops::flip_vertical_in_place(&mut image); + } + + if self.contains(Self::ROTATE_180) { + imageops::rotate180_in_place(&mut image); + } + + if self.contains(Self::FLIP_HORIZONTALLY) { + imageops::flip_horizontal_in_place(&mut image); + } + + image + } + } + + let (width, height, pixels) = match handle { + image::Handle::Path(_, path) => { + let image = ::image::open(path)?; + + let operation = std::fs::File::open(path) + .ok() + .map(std::io::BufReader::new) + .and_then(|mut reader| Operation::from_exif(&mut reader).ok()) + .unwrap_or_else(Operation::empty); + + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) + } + image::Handle::Bytes(_, bytes) => { + let image = ::image::load_from_memory(bytes)?; + let operation = + Operation::from_exif(&mut std::io::Cursor::new(bytes)) + .ok() + .unwrap_or_else(Operation::empty); + + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) + } + image::Handle::Rgba { + width, + height, + pixels, + .. + } => (*width, *height, pixels.clone()), + }; + + if let Some(image) = ::image::ImageBuffer::from_raw(width, height, pixels) { + Ok(image) + } else { + Err(::image::error::ImageError::Limits( + ::image::error::LimitError::from_kind( + ::image::error::LimitErrorKind::DimensionError, + ), + )) + } +} diff --git a/patches/iced_graphics/src/image/storage.rs b/patches/iced_graphics/src/image/storage.rs new file mode 100644 index 0000000..4caa614 --- /dev/null +++ b/patches/iced_graphics/src/image/storage.rs @@ -0,0 +1,31 @@ +//! Store images. +use iced_core::Size; + +use std::fmt::Debug; + +/// Stores cached image data for use in rendering +pub trait Storage { + /// The type of an [`Entry`] in the [`Storage`]. + type Entry: Entry; + + /// State provided to upload or remove a [`Self::Entry`]. + type State<'a>; + + /// Upload the image data of a [`Self::Entry`]. + fn upload( + &mut self, + width: u32, + height: u32, + data: &[u8], + state: &mut Self::State<'_>, + ) -> Option; + + /// Remove a [`Self::Entry`] from the [`Storage`]. + fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>); +} + +/// An entry in some [`Storage`], +pub trait Entry: Debug { + /// The [`Size`] of the [`Entry`]. + fn size(&self) -> Size; +} diff --git a/patches/iced_graphics/src/layer.rs b/patches/iced_graphics/src/layer.rs new file mode 100644 index 0000000..c9a818f --- /dev/null +++ b/patches/iced_graphics/src/layer.rs @@ -0,0 +1,144 @@ +//! Draw and stack layers of graphical primitives. +use crate::core::{Rectangle, Transformation}; + +/// A layer of graphical primitives. +/// +/// Layers normally dictate a set of primitives that are +/// rendered in a specific order. +pub trait Layer: Default { + /// Creates a new [`Layer`] with the given bounds. + fn with_bounds(bounds: Rectangle) -> Self; + + /// Flushes and settles any pending group of primitives in the [`Layer`]. + /// + /// This will be called when a [`Layer`] is finished. It allows layers to efficiently + /// record primitives together and defer grouping until the end. + fn flush(&mut self); + + /// Resizes the [`Layer`] to the given bounds. + fn resize(&mut self, bounds: Rectangle); + + /// Clears all the layers contents and resets its bounds. + fn reset(&mut self); +} + +/// A stack of layers used for drawing. +#[derive(Debug)] +pub struct Stack { + layers: Vec, + transformations: Vec, + previous: Vec, + current: usize, + active_count: usize, +} + +impl Stack { + /// Creates a new empty [`Stack`]. + pub fn new() -> Self { + Self { + layers: vec![T::default()], + transformations: vec![Transformation::IDENTITY], + previous: vec![], + current: 0, + active_count: 1, + } + } + + /// Returns a mutable reference to the current [`Layer`] of the [`Stack`], together with + /// the current [`Transformation`]. + #[inline] + pub fn current_mut(&mut self) -> (&mut T, Transformation) { + let transformation = self.transformation(); + + (&mut self.layers[self.current], transformation) + } + + /// Returns the current [`Transformation`] of the [`Stack`]. + #[inline] + pub fn transformation(&self) -> Transformation { + self.transformations.last().copied().unwrap() + } + + /// Pushes a new clipping region in the [`Stack`]; creating a new layer in the + /// process. + pub fn push_clip(&mut self, bounds: Rectangle) { + self.previous.push(self.current); + + self.current = self.active_count; + self.active_count += 1; + + let bounds = bounds * self.transformation(); + + if self.current == self.layers.len() { + self.layers.push(T::with_bounds(bounds)); + } else { + self.layers[self.current].resize(bounds); + } + } + + /// Pops the current clipping region from the [`Stack`] and restores the previous one. + /// + /// The current layer will be recorded for drawing. + pub fn pop_clip(&mut self) { + self.flush(); + + self.current = self.previous.pop().unwrap(); + } + + /// Pushes a new [`Transformation`] in the [`Stack`]. + /// + /// Future drawing operations will be affected by this new [`Transformation`] until + /// it is popped using [`pop_transformation`]. + /// + /// [`pop_transformation`]: Self::pop_transformation + pub fn push_transformation(&mut self, transformation: Transformation) { + self.transformations + .push(self.transformation() * transformation); + } + + /// Pops the current [`Transformation`] in the [`Stack`]. + pub fn pop_transformation(&mut self) { + let _ = self.transformations.pop(); + } + + /// Returns an iterator over mutable references to the layers in the [`Stack`]. + pub fn iter_mut(&mut self) -> impl Iterator { + self.flush(); + + self.layers[..self.active_count].iter_mut() + } + + /// Returns an iterator over immutable references to the layers in the [`Stack`]. + pub fn iter(&self) -> impl Iterator { + self.layers[..self.active_count].iter() + } + + /// Returns the slice of layers in the [`Stack`]. + pub fn as_slice(&self) -> &[T] { + &self.layers[..self.active_count] + } + + /// Flushes and settles any primitives in the current layer of the [`Stack`]. + pub fn flush(&mut self) { + self.layers[self.current].flush(); + } + + /// Clears the layers of the [`Stack`], allowing reuse. + /// + /// This will normally keep layer allocations for future drawing operations. + pub fn clear(&mut self) { + for layer in self.layers[..self.active_count].iter_mut() { + layer.reset(); + } + + self.current = 0; + self.active_count = 1; + self.previous.clear(); + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} diff --git a/patches/iced_graphics/src/lib.rs b/patches/iced_graphics/src/lib.rs new file mode 100644 index 0000000..b5ef55e --- /dev/null +++ b/patches/iced_graphics/src/lib.rs @@ -0,0 +1,42 @@ +//! A bunch of backend-agnostic types that can be leveraged to build a renderer +//! for [`iced`]. +//! +//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true) +//! +//! [`iced`]: https://github.com/iced-rs/iced +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +mod antialiasing; +mod settings; +mod viewport; + +pub mod cache; +pub mod color; +pub mod compositor; +pub mod damage; +pub mod error; +pub mod gradient; +pub mod image; +pub mod layer; +pub mod mesh; +pub mod text; + +#[cfg(feature = "geometry")] +pub mod geometry; + +pub use antialiasing::Antialiasing; +pub use cache::Cache; +pub use compositor::Compositor; +pub use error::Error; +pub use gradient::Gradient; +pub use image::Image; +pub use layer::Layer; +pub use mesh::Mesh; +pub use settings::Settings; +pub use text::Text; +pub use viewport::Viewport; + +pub use iced_core as core; +pub use iced_futures as futures; diff --git a/patches/iced_graphics/src/mesh.rs b/patches/iced_graphics/src/mesh.rs new file mode 100644 index 0000000..7660231 --- /dev/null +++ b/patches/iced_graphics/src/mesh.rs @@ -0,0 +1,148 @@ +//! Draw triangles! +use crate::color; +use crate::core::{Rectangle, Transformation}; +use crate::gradient; + +use bytemuck::{Pod, Zeroable}; + +/// A low-level primitive to render a mesh of triangles. +#[derive(Debug, Clone, PartialEq)] +pub enum Mesh { + /// A mesh with a solid color. + Solid { + /// The vertices and indices of the mesh. + buffers: Indexed, + + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, + /// A mesh with a gradient. + Gradient { + /// The vertices and indices of the mesh. + buffers: Indexed, + + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, +} + +impl Mesh { + /// Returns the indices of the [`Mesh`]. + pub fn indices(&self) -> &[u32] { + match self { + Self::Solid { buffers, .. } => &buffers.indices, + Self::Gradient { buffers, .. } => &buffers.indices, + } + } + + /// Returns the [`Transformation`] of the [`Mesh`]. + pub fn transformation(&self) -> Transformation { + match self { + Self::Solid { transformation, .. } + | Self::Gradient { transformation, .. } => *transformation, + } + } + + /// Returns the clip bounds of the [`Mesh`]. + pub fn clip_bounds(&self) -> Rectangle { + match self { + Self::Solid { + clip_bounds, + transformation, + .. + } + | Self::Gradient { + clip_bounds, + transformation, + .. + } => *clip_bounds * *transformation, + } + } +} + +/// A set of vertices and indices representing a list of triangles. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Indexed { + /// The vertices of the mesh + pub vertices: Vec, + + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec, +} + +/// A two-dimensional vertex with a color. +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct SolidVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The color of the vertex in __linear__ RGBA. + pub color: color::Packed, +} + +/// A vertex which contains 2D position & packed gradient data. +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct GradientVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The packed vertex data of the gradient. + pub gradient: gradient::Packed, +} + +/// The result of counting the attributes of a set of meshes. +#[derive(Debug, Clone, Copy, Default)] +pub struct AttributeCount { + /// The total amount of solid vertices. + pub solid_vertices: usize, + + /// The total amount of solid meshes. + pub solids: usize, + + /// The total amount of gradient vertices. + pub gradient_vertices: usize, + + /// The total amount of gradient meshes. + pub gradients: usize, + + /// The total amount of indices. + pub indices: usize, +} + +/// Returns the number of total vertices & total indices of all [`Mesh`]es. +pub fn attribute_count_of(meshes: &[Mesh]) -> AttributeCount { + meshes + .iter() + .fold(AttributeCount::default(), |mut count, mesh| { + match mesh { + Mesh::Solid { buffers, .. } => { + count.solids += 1; + count.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradients += 1; + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} + +/// A renderer capable of drawing a [`Mesh`]. +pub trait Renderer { + /// Draws the given [`Mesh`]. + fn draw_mesh(&mut self, mesh: Mesh); +} diff --git a/patches/iced_graphics/src/settings.rs b/patches/iced_graphics/src/settings.rs new file mode 100644 index 0000000..2e8275c --- /dev/null +++ b/patches/iced_graphics/src/settings.rs @@ -0,0 +1,29 @@ +use crate::core::{Font, Pixels}; +use crate::Antialiasing; + +/// The settings of a renderer. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: Pixels, + + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. + pub antialiasing: Option, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: Font::default(), + default_text_size: Pixels(16.0), + antialiasing: None, + } + } +} diff --git a/patches/iced_graphics/src/text.rs b/patches/iced_graphics/src/text.rs new file mode 100644 index 0000000..feb9932 --- /dev/null +++ b/patches/iced_graphics/src/text.rs @@ -0,0 +1,324 @@ +//! Draw text. +pub mod cache; +pub mod editor; +pub mod paragraph; + +pub use cache::Cache; +pub use editor::Editor; +pub use paragraph::Paragraph; + +pub use cosmic_text; + +use crate::core::alignment; +use crate::core::font::{self, Font}; +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}; + +/// A text primitive. +#[derive(Debug, Clone, PartialEq)] +pub enum Text { + /// A paragraph. + #[allow(missing_docs)] + Paragraph { + paragraph: paragraph::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// An editor. + #[allow(missing_docs)] + Editor { + editor: editor::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// Some cached text. + Cached { + /// The contents of the text. + content: String, + /// The bounds of the text. + bounds: Rectangle, + /// The color of the text. + color: Color, + /// The size of the text in logical pixels. + size: Pixels, + /// The line height of the text. + line_height: Pixels, + /// The font of the text. + font: Font, + /// The horizontal alignment of the text. + horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the text. + vertical_alignment: alignment::Vertical, + /// The shaping strategy of the text. + shaping: Shaping, + /// The clip bounds of the text. + clip_bounds: Rectangle, + }, + /// Some raw text. + #[allow(missing_docs)] + Raw { + raw: Raw, + transformation: Transformation, + }, +} + +impl Text { + /// Returns the visible bounds of the [`Text`]. + pub fn visible_bounds(&self) -> Option { + let (bounds, horizontal_alignment, vertical_alignment) = match self { + Text::Paragraph { + position, + paragraph, + clip_bounds, + transformation, + .. + } => ( + Rectangle::new(*position, paragraph.min_bounds) + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), + Some(paragraph.horizontal_alignment), + Some(paragraph.vertical_alignment), + ), + Text::Editor { + editor, + position, + clip_bounds, + transformation, + .. + } => ( + Rectangle::new(*position, editor.bounds) + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), + None, + None, + ), + Text::Cached { + bounds, + clip_bounds, + horizontal_alignment, + vertical_alignment, + .. + } => ( + bounds.intersection(clip_bounds), + Some(*horizontal_alignment), + Some(*vertical_alignment), + ), + Text::Raw { raw, .. } => (Some(raw.clip_bounds), None, None), + }; + + let mut bounds = bounds?; + + if let Some(alignment) = horizontal_alignment { + match alignment { + alignment::Horizontal::Left => {} + alignment::Horizontal::Center => { + bounds.x -= bounds.width / 2.0; + } + alignment::Horizontal::Right => { + bounds.x -= bounds.width; + } + } + } + + if let Some(alignment) = vertical_alignment { + match alignment { + alignment::Vertical::Top => {} + alignment::Vertical::Center => { + bounds.y -= bounds.height / 2.0; + } + alignment::Vertical::Bottom => { + bounds.y -= bounds.height; + } + } + } + + Some(bounds) + } +} + +/// The regular variant of the [Fira Sans] font. +/// +/// It is loaded as part of the default fonts in Wasm builds. +/// +/// [Fira Sans]: https://mozilla.github.io/Fira/ +#[cfg(all(target_arch = "wasm32", feature = "fira-sans"))] +pub const FIRA_SANS_REGULAR: &'static [u8] = + include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(); + +/// Returns the global [`FontSystem`]. +pub fn font_system() -> &'static RwLock { + static FONT_SYSTEM: OnceCell> = OnceCell::new(); + + FONT_SYSTEM.get_or_init(|| { + RwLock::new(FontSystem { + raw: cosmic_text::FontSystem::new_with_fonts([ + cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), + )), + #[cfg(all(target_arch = "wasm32", feature = "fira-sans"))] + cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(), + )), + ]), + version: Version::default(), + }) + }) +} + +/// A set of system fonts. +#[allow(missing_debug_implementations)] +pub struct FontSystem { + raw: cosmic_text::FontSystem, + version: Version, +} + +impl FontSystem { + /// Returns the raw [`cosmic_text::FontSystem`]. + pub fn raw(&mut self) -> &mut cosmic_text::FontSystem { + &mut self.raw + } + + /// Loads a font from its bytes. + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + let _ = self.raw.db_mut().load_font_source( + cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), + ); + + self.version = Version(self.version.0 + 1); + } + + /// Returns the current [`Version`] of the [`FontSystem`]. + /// + /// Loading a font will increase the version of a [`FontSystem`]. + pub fn version(&self) -> Version { + self.version + } +} + +/// A version number. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct Version(u32); + +/// A weak reference to a [`cosmic-text::Buffer`] that can be drawn. +#[derive(Debug, Clone)] +pub struct Raw { + /// A weak reference to a [`cosmic_text::Buffer`]. + pub buffer: Weak, + /// The position of the text. + pub position: Point, + /// The color of the text. + pub color: Color, + /// The clip bounds of the text. + pub clip_bounds: Rectangle, +} + +impl PartialEq for Raw { + fn eq(&self, _other: &Self) -> bool { + // TODO: There is no proper way to compare raw buffers + // For now, no two instances of `Raw` text will be equal. + // This should be fine, but could trigger unnecessary redraws + // in the future. + false + } +} + +/// Measures the dimensions of the given [`cosmic_text::Buffer`]. +pub fn measure(buffer: &cosmic_text::Buffer) -> Size { + 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, height) +} + +/// Returns the attributes of the given [`Font`]. +pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> { + cosmic_text::Attrs::new() + .family(to_family(font.family)) + .weight(to_weight(font.weight)) + .stretch(to_stretch(font.stretch)) + .style(to_style(font.style)) +} + +fn to_family(family: font::Family) -> cosmic_text::Family<'static> { + match family { + font::Family::Name(name) => cosmic_text::Family::Name(name), + font::Family::SansSerif => cosmic_text::Family::SansSerif, + font::Family::Serif => cosmic_text::Family::Serif, + font::Family::Cursive => cosmic_text::Family::Cursive, + font::Family::Fantasy => cosmic_text::Family::Fantasy, + font::Family::Monospace => cosmic_text::Family::Monospace, + } +} + +fn to_weight(weight: font::Weight) -> cosmic_text::Weight { + match weight { + font::Weight::Thin => cosmic_text::Weight::THIN, + font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT, + font::Weight::Light => cosmic_text::Weight::LIGHT, + font::Weight::Normal => cosmic_text::Weight::NORMAL, + font::Weight::Medium => cosmic_text::Weight::MEDIUM, + font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD, + font::Weight::Bold => cosmic_text::Weight::BOLD, + font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD, + font::Weight::Black => cosmic_text::Weight::BLACK, + } +} + +fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch { + match stretch { + font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed, + font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed, + font::Stretch::Condensed => cosmic_text::Stretch::Condensed, + font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed, + font::Stretch::Normal => cosmic_text::Stretch::Normal, + font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded, + font::Stretch::Expanded => cosmic_text::Stretch::Expanded, + font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded, + font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded, + } +} + +fn to_style(style: font::Style) -> cosmic_text::Style { + match style { + font::Style::Normal => cosmic_text::Style::Normal, + font::Style::Italic => cosmic_text::Style::Italic, + font::Style::Oblique => cosmic_text::Style::Oblique, + } +} + +/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. +pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { + match shaping { + Shaping::Basic => cosmic_text::Shaping::Basic, + Shaping::Advanced => cosmic_text::Shaping::Advanced, + } +} + +/// 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(); + + cosmic_text::Color::rgba(r, g, b, a) +} diff --git a/patches/iced_graphics/src/text/cache.rs b/patches/iced_graphics/src/text/cache.rs new file mode 100644 index 0000000..e64d93f --- /dev/null +++ b/patches/iced_graphics/src/text/cache.rs @@ -0,0 +1,143 @@ +//! Cache text. +use crate::core::{Font, Size}; +use crate::text; + +use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; +use std::collections::hash_map; +use std::hash::{Hash, Hasher}; + +/// A store of recently used sections of text. +#[derive(Debug, Default)] +pub struct Cache { + entries: FxHashMap, + aliases: FxHashMap, + recently_used: FxHashSet, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Self::default() + } + + /// Gets the text [`Entry`] with the given [`KeyHash`]. + pub fn get(&self, key: &KeyHash) -> Option<&Entry> { + self.entries.get(key) + } + + /// Allocates a text [`Entry`] if it is not already present in the [`Cache`]. + pub fn allocate( + &mut self, + font_system: &mut cosmic_text::FontSystem, + key: Key<'_>, + ) -> (KeyHash, &mut Entry) { + let hash = key.hash(FxHasher::default()); + + if let Some(hash) = self.aliases.get(&hash) { + let _ = self.recently_used.insert(*hash); + + return (*hash, self.entries.get_mut(hash).unwrap()); + } + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { + let metrics = cosmic_text::Metrics::new( + key.size, + key.line_height.max(f32::MIN_POSITIVE), + ); + let mut buffer = cosmic_text::Buffer::new(font_system, metrics); + + buffer.set_size( + font_system, + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), + ); + buffer.set_text( + font_system, + key.content, + text::to_attributes(key.font), + text::to_shaping(key.shaping), + ); + + let bounds = text::measure(&buffer); + let _ = entry.insert(Entry { + buffer, + min_bounds: bounds, + }); + + for bounds in [ + bounds, + Size { + width: key.bounds.width, + ..bounds + }, + ] { + if key.bounds != bounds { + let _ = self.aliases.insert( + Key { bounds, ..key }.hash(FxHasher::default()), + hash, + ); + } + } + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + /// Trims the [`Cache`]. + /// + /// This will clear the sections of text that have not been used since the last `trim`. + pub fn trim(&mut self) { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.aliases + .retain(|_, value| self.recently_used.contains(value)); + + self.recently_used.clear(); + } +} + +/// A cache key representing a section of text. +#[derive(Debug, Clone, Copy)] +pub struct Key<'a> { + /// The content of the text. + pub content: &'a str, + /// The size of the text. + pub size: f32, + /// The line height of the text. + pub line_height: f32, + /// The [`Font`] of the text. + pub font: Font, + /// The bounds of the text. + pub bounds: Size, + /// The shaping strategy of the text. + pub shaping: text::Shaping, +} + +impl Key<'_> { + fn hash(self, mut hasher: H) -> KeyHash { + self.content.hash(&mut hasher); + self.size.to_bits().hash(&mut hasher); + self.line_height.to_bits().hash(&mut hasher); + self.font.hash(&mut hasher); + self.bounds.width.to_bits().hash(&mut hasher); + self.bounds.height.to_bits().hash(&mut hasher); + self.shaping.hash(&mut hasher); + + hasher.finish() + } +} + +/// The hash of a [`Key`]. +pub type KeyHash = u64; + +/// A cache entry. +#[derive(Debug)] +pub struct Entry { + /// The buffer of text, ready for drawing. + pub buffer: cosmic_text::Buffer, + /// The minimum bounds of the text. + pub min_bounds: Size, +} diff --git a/patches/iced_graphics/src/text/editor.rs b/patches/iced_graphics/src/text/editor.rs new file mode 100644 index 0000000..1f1d005 --- /dev/null +++ b/patches/iced_graphics/src/text/editor.rs @@ -0,0 +1,781 @@ +//! Draw and edit text. +use crate::core::text::editor::{ + self, Action, Cursor, Direction, Edit, Motion, +}; +use crate::core::text::highlighter::{self, Highlighter}; +use crate::core::text::{LineHeight, Wrapping}; +use crate::core::{Font, Pixels, Point, Rectangle, Size}; +use crate::text; + +use cosmic_text::Edit as _; + +use std::fmt; +use std::sync::{self, Arc}; + +/// A multi-line text editor. +#[derive(Debug, PartialEq)] +pub struct Editor(Option>); + +struct Internal { + editor: cosmic_text::Editor<'static>, + font: Font, + bounds: Size, + topmost_line_changed: Option, + version: text::Version, +} + +impl Editor { + /// Creates a new empty [`Editor`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns the buffer of the [`Editor`]. + pub fn buffer(&self) -> &cosmic_text::Buffer { + buffer_from_editor(&self.internal().editor) + } + + /// Creates a [`Weak`] reference to the [`Editor`]. + /// + /// This is useful to avoid cloning the [`Editor`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. + pub fn downgrade(&self) -> Weak { + let editor = self.internal(); + + Weak { + raw: Arc::downgrade(editor), + bounds: editor.bounds, + } + } + + fn internal(&self) -> &Arc { + self.0 + .as_ref() + .expect("Editor should always be initialized") + } +} + +impl editor::Editor for Editor { + type Font = Font; + + fn with_text(text: &str) -> Self { + let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + buffer.set_text( + font_system.raw(), + text, + cosmic_text::Attrs::new(), + cosmic_text::Shaping::Advanced, + ); + + Editor(Some(Arc::new(Internal { + editor: cosmic_text::Editor::new(buffer), + version: font_system.version(), + ..Default::default() + }))) + } + + fn is_empty(&self) -> bool { + let buffer = self.buffer(); + + buffer.lines.is_empty() + || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) + } + + fn line(&self, index: usize) -> Option<&str> { + self.buffer() + .lines + .get(index) + .map(cosmic_text::BufferLine::text) + } + + fn line_count(&self) -> usize { + self.buffer().lines.len() + } + + fn selection(&self) -> Option { + self.internal().editor.copy_selection() + } + + fn cursor(&self) -> editor::Cursor { + let internal = self.internal(); + + let cursor = internal.editor.cursor(); + let buffer = buffer_from_editor(&internal.editor); + + match internal.editor.selection_bounds() { + Some((start, end)) => { + let line_height = buffer.metrics().line_height; + let selected_lines = end.line - start.line + 1; + + let visual_lines_offset = + visual_lines_offset(start.line, buffer); + + let regions = buffer + .lines + .iter() + .skip(start.line) + .take(selected_lines) + .enumerate() + .flat_map(|(i, line)| { + highlight_line( + line, + if i == 0 { start.index } else { 0 }, + if i == selected_lines - 1 { + end.index + } else { + line.text().len() + }, + ) + }) + .enumerate() + .filter_map(|(visual_line, (x, width))| { + if width > 0.0 { + Some(Rectangle { + x, + width, + y: (visual_line as i32 + visual_lines_offset) + as f32 + * line_height + - buffer.scroll().vertical, + height: line_height, + }) + } else { + None + } + }) + .collect(); + + Cursor::Selection(regions) + } + _ => { + let line_height = buffer.metrics().line_height; + + let visual_lines_offset = + visual_lines_offset(cursor.line, buffer); + + let line = buffer + .lines + .get(cursor.line) + .expect("Cursor line should be present"); + + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + let mut lines = layout.iter().enumerate(); + + let (visual_line, offset) = lines + .find_map(|(i, line)| { + let start = line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let is_cursor_before_start = start > cursor.index; + + let is_cursor_before_end = match cursor.affinity { + cosmic_text::Affinity::Before => { + cursor.index <= end + } + cosmic_text::Affinity::After => cursor.index < end, + }; + + if is_cursor_before_start { + // Sometimes, the glyph we are looking for is right + // between lines. This can happen when a line wraps + // on a space. + // In that case, we can assume the cursor is at the + // end of the previous line. + // i is guaranteed to be > 0 because `start` is always + // 0 for the first line, so there is no way for the + // cursor to be before it. + Some((i - 1, layout[i - 1].w)) + } else if is_cursor_before_end { + let offset = line + .glyphs + .iter() + .take_while(|glyph| cursor.index > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + Some((i, offset)) + } else { + None + } + }) + .unwrap_or(( + layout.len().saturating_sub(1), + layout.last().map(|line| line.w).unwrap_or(0.0), + )); + + Cursor::Caret(Point::new( + offset, + (visual_lines_offset + visual_line as i32) as f32 + * line_height + - buffer.scroll().vertical, + )) + } + } + } + + fn cursor_position(&self) -> (usize, usize) { + let cursor = self.internal().editor.cursor(); + + (cursor.line, cursor.index) + } + + fn perform(&mut self, action: Action) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let editor = + self.0.take().expect("Editor should always be initialized"); + + // TODO: Handle multiple strong references somehow + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let editor = &mut internal.editor; + + match action { + // Motion events + Action::Move(motion) => { + 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 + // is present + Motion::Home + | Motion::End + | Motion::DocumentStart + | Motion::DocumentEnd => { + editor.action( + font_system.raw(), + 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 => start, + Direction::Right => end, + }), + } + } else { + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); + } + } + + // Selection events + Action::Select(motion) => { + let cursor = editor.cursor(); + + if editor.selection_bounds().is_none() { + editor + .set_selection(cosmic_text::Selection::Normal(cursor)); + } + + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); + + // Deselect if selection matches cursor position + 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 => { + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Word(cursor)); + } + Action::SelectLine => { + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Line(cursor)); + } + Action::SelectAll => { + let buffer = buffer_from_editor(editor); + + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) + { + 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, + ), + ); + } + } + + // Editing events + Action::Edit(edit) => { + match edit { + Edit::Insert(c) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Insert(c), + ); + } + Edit::Paste(text) => { + editor.insert_string(&text, None); + } + Edit::Enter => { + editor.action( + font_system.raw(), + cosmic_text::Action::Enter, + ); + } + Edit::Backspace => { + editor.action( + font_system.raw(), + cosmic_text::Action::Backspace, + ); + } + Edit::Delete => { + editor.action( + font_system.raw(), + cosmic_text::Action::Delete, + ); + } + } + + let cursor = editor.cursor(); + let selection_start = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or(cursor); + + internal.topmost_line_changed = Some(selection_start.line); + } + + // Mouse events + Action::Click(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }, + ); + } + Action::Drag(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }, + ); + + // Deselect if selection matches cursor position + 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 } => { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); + } + } + + self.0 = Some(Arc::new(internal)); + } + + fn bounds(&self) -> Size { + self.internal().bounds + } + + fn min_bounds(&self) -> Size { + let internal = self.internal(); + + text::measure(buffer_from_editor(&internal.editor)) + } + + fn update( + &mut self, + new_bounds: Size, + new_font: Font, + new_size: Pixels, + new_line_height: LineHeight, + new_wrapping: Wrapping, + new_highlighter: &mut impl Highlighter, + ) { + let editor = + self.0.take().expect("Editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + 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 buffer.lines.iter_mut() { + line.reset(); + } + + internal.version = font_system.version(); + internal.topmost_line_changed = Some(0); + } + + if new_font != internal.font { + log::trace!("Updating font of `Editor`..."); + + for line in buffer.lines.iter_mut() { + let _ = line.set_attrs_list(cosmic_text::AttrsList::new( + text::to_attributes(new_font), + )); + } + + internal.font = new_font; + internal.topmost_line_changed = Some(0); + } + + let metrics = buffer.metrics(); + let new_line_height = new_line_height.to_absolute(new_size); + + if new_size.0 != metrics.font_size + || new_line_height.0 != metrics.line_height + { + log::trace!("Updating `Metrics` of `Editor`..."); + + 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`..."); + + buffer.set_size( + font_system.raw(), + Some(new_bounds.width), + Some(new_bounds.height), + ); + + internal.bounds = new_bounds; + } + + if let Some(topmost_line_changed) = internal.topmost_line_changed.take() + { + log::trace!( + "Notifying highlighter of line change: {topmost_line_changed}" + ); + + new_highlighter.change_line(topmost_line_changed); + } + + internal.editor.shape_as_needed(font_system.raw(), false); + + self.0 = Some(Arc::new(internal)); + } + + fn highlight( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, + ) { + let internal = self.internal(); + let buffer = buffer_from_editor(&internal.editor); + + let scroll = buffer.scroll(); + let mut window = (internal.bounds.height / buffer.metrics().line_height) + .ceil() as i32; + + let last_visible_line = buffer.lines[scroll.line..] + .iter() + .enumerate() + .find_map(|(i, line)| { + let visible_lines = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() as i32; + + if window > visible_lines { + window -= visible_lines; + None + } else { + Some(scroll.line + i) + } + }) + .unwrap_or(buffer.lines.len().saturating_sub(1)); + + let current_line = highlighter.current_line(); + + if current_line > last_visible_line { + return; + } + + let editor = + self.0.take().expect("Editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let attributes = text::to_attributes(font); + + 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); + + for (range, highlight) in highlighter.highlight_line(line.text()) { + let format = format_highlight(&highlight); + + if format.color.is_some() || format.font.is_some() { + list.add_span( + range, + cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes + } + }, + ); + } + } + + let _ = line.set_attrs_list(list); + } + + internal.editor.shape_as_needed(font_system.raw(), false); + + self.0 = Some(Arc::new(internal)); + } +} + +impl Default for Editor { + fn default() -> Self { + Self(Some(Arc::new(Internal::default()))) + } +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + self.font == other.font + && self.bounds == other.bounds + && buffer_from_editor(&self.editor).metrics() + == buffer_from_editor(&other.editor).metrics() + } +} + +impl Default for Internal { + fn default() -> Self { + Self { + editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty( + cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }, + )), + font: Font::default(), + bounds: Size::ZERO, + topmost_line_changed: None, + version: text::Version::default(), + } + } +} + +impl fmt::Debug for Internal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Internal") + .field("font", &self.font) + .field("bounds", &self.bounds) + .finish() + } +} + +/// A weak reference to an [`Editor`]. +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak, + /// The bounds of the [`Editor`]. + pub bounds: Size, +} + +impl Weak { + /// Tries to update the reference into an [`Editor`]. + pub fn upgrade(&self) -> Option { + self.raw.upgrade().map(Some).map(Editor) + } +} + +impl PartialEq for Weak { + fn eq(&self, other: &Self) -> bool { + match (self.raw.upgrade(), other.raw.upgrade()) { + (Some(p1), Some(p2)) => p1 == p2, + _ => false, + } + } +} + +fn highlight_line( + line: &cosmic_text::BufferLine, + from: usize, + to: usize, +) -> impl Iterator + '_ { + let layout = line + .layout_opt() + .as_ref() + .map(Vec::as_slice) + .unwrap_or_default(); + + layout.iter().map(move |visual_line| { + let start = visual_line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = visual_line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let range = start.max(from)..end.min(to); + + if range.is_empty() { + (0.0, 0.0) + } else if range.start == start && range.end == end { + (0.0, visual_line.w) + } else { + let first_glyph = visual_line + .glyphs + .iter() + .position(|glyph| range.start <= glyph.start) + .unwrap_or(0); + + let mut glyphs = visual_line.glyphs.iter(); + + let x = + glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum(); + + let width: f32 = glyphs + .take_while(|glyph| range.end > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + (x, width) + } + }) +} + +fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { + 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(end - start) + .map(|line| { + line.layout_opt().as_ref().map(Vec::len).unwrap_or_default() + }) + .sum(); + + visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 } +} + +fn to_motion(motion: Motion) -> cosmic_text::Motion { + match motion { + Motion::Left => cosmic_text::Motion::Left, + Motion::Right => cosmic_text::Motion::Right, + Motion::Up => cosmic_text::Motion::Up, + Motion::Down => cosmic_text::Motion::Down, + Motion::WordLeft => cosmic_text::Motion::LeftWord, + Motion::WordRight => cosmic_text::Motion::RightWord, + Motion::Home => cosmic_text::Motion::Home, + Motion::End => cosmic_text::Motion::End, + Motion::PageUp => cosmic_text::Motion::PageUp, + Motion::PageDown => cosmic_text::Motion::PageDown, + Motion::DocumentStart => cosmic_text::Motion::BufferStart, + Motion::DocumentEnd => cosmic_text::Motion::BufferEnd, + } +} + +fn buffer_from_editor<'a, 'b>( + editor: &'a impl cosmic_text::Edit<'b>, +) -> &'a cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } +} + +fn buffer_mut_from_editor<'a, 'b>( + editor: &'a mut impl cosmic_text::Edit<'b>, +) -> &'a mut cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref_mut() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(_buffer) => unreachable!(), + } +} diff --git a/patches/iced_graphics/src/text/paragraph.rs b/patches/iced_graphics/src/text/paragraph.rs new file mode 100644 index 0000000..330eb66 --- /dev/null +++ b/patches/iced_graphics/src/text/paragraph.rs @@ -0,0 +1,430 @@ +//! Draw paragraphs. +use crate::core; +use crate::core::alignment; +use crate::core::text::{Hit, Shaping, Span, Text, Wrapping}; +use crate::core::{Font, Point, Rectangle, Size}; +use crate::text; + +use std::fmt; +use std::sync::{self, Arc}; + +/// A bunch of text. +#[derive(Clone, PartialEq)] +pub struct Paragraph(Arc); + +#[derive(Clone)] +struct Internal { + buffer: cosmic_text::Buffer, + font: Font, + shaping: Shaping, + wrapping: Wrapping, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + bounds: Size, + min_bounds: Size, + version: text::Version, +} + +impl Paragraph { + /// Creates a new empty [`Paragraph`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns the buffer of the [`Paragraph`]. + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().buffer + } + + /// Creates a [`Weak`] reference to the [`Paragraph`]. + /// + /// This is useful to avoid cloning the [`Paragraph`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. + pub fn downgrade(&self) -> Weak { + let paragraph = self.internal(); + + Weak { + raw: Arc::downgrade(paragraph), + min_bounds: paragraph.min_bounds, + horizontal_alignment: paragraph.horizontal_alignment, + vertical_alignment: paragraph.vertical_alignment, + } + } + + fn internal(&self) -> &Arc { + &self.0 + } +} + +impl core::text::Paragraph for Paragraph { + type Font = Font; + + fn with_text(text: Text<&str>) -> Self { + log::trace!("Allocating plain paragraph"); + + 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_text( + font_system.raw(), + text.content, + 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 with_spans(text: Text<&[Span<'_, Link>]>) -> Self { + log::trace!("Allocating rich paragraph: {} spans", text.content.len()); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::Buffer::new( + font_system.raw(), + cosmic_text::Metrics::new( + text.size.into(), + text.line_height.to_absolute(text.size).into(), + ), + ); + + buffer.set_size( + font_system.raw(), + Some(text.bounds.width), + Some(text.bounds.height), + ); + + buffer.set_rich_text( + font_system.raw(), + text.content.iter().enumerate().map(|(i, span)| { + let attrs = text::to_attributes(span.font.unwrap_or(text.font)); + + let attrs = match (span.size, span.line_height) { + (None, None) => attrs, + _ => { + let size = span.size.unwrap_or(text.size); + + attrs.metrics(cosmic_text::Metrics::new( + size.into(), + span.line_height + .unwrap_or(text.line_height) + .to_absolute(size) + .into(), + )) + } + }; + + let attrs = if let Some(color) = span.color { + attrs.color(text::to_color(color)) + } else { + attrs + }; + + (span.text.as_ref(), attrs.metadata(i)) + }), + text::to_attributes(text.font), + text::to_shaping(text.shaping), + ); + + let min_bounds = text::measure(&buffer); + + Self(Arc::new(Internal { + buffer, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + wrapping: text.wrapping, + bounds: text.bounds, + min_bounds, + version: font_system.version(), + })) + } + + fn resize(&mut self, new_bounds: Size) { + let paragraph = Arc::make_mut(&mut self.0); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + paragraph.buffer.set_size( + font_system.raw(), + Some(new_bounds.width), + Some(new_bounds.height), + ); + + paragraph.bounds = new_bounds; + paragraph.min_bounds = text::measure(¶graph.buffer); + } + + 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 + || 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 + { + core::text::Difference::Shape + } else if paragraph.bounds != text.bounds { + core::text::Difference::Bounds + } else { + core::text::Difference::None + } + } + + fn horizontal_alignment(&self) -> alignment::Horizontal { + self.internal().horizontal_alignment + } + + fn vertical_alignment(&self) -> alignment::Vertical { + self.internal().vertical_alignment + } + + fn min_bounds(&self) -> Size { + self.internal().min_bounds + } + + fn hit_test(&self, point: Point) -> Option { + let cursor = self.internal().buffer.hit(point.x, point.y)?; + + Some(Hit::CharOffset(cursor.index)) + } + + fn hit_span(&self, point: Point) -> Option { + 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 { + 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 { + use unicode_segmentation::UnicodeSegmentation; + + let run = self.internal().buffer.layout_runs().nth(line)?; + + // index represents a grapheme, not a glyph + // Let's find the first glyph for the given grapheme cluster + let mut last_start = None; + let mut last_grapheme_count = 0; + let mut graphemes_seen = 0; + + let glyph = run + .glyphs + .iter() + .find(|glyph| { + if Some(glyph.start) != last_start { + last_grapheme_count = run.text[glyph.start..glyph.end] + .graphemes(false) + .count(); + last_start = Some(glyph.start); + graphemes_seen += last_grapheme_count; + } + + graphemes_seen >= index + }) + .or_else(|| run.glyphs.last())?; + + let advance = if index == 0 { + 0.0 + } else { + glyph.w + * (1.0 + - graphemes_seen.saturating_sub(index) as f32 + / last_grapheme_count.max(1) as f32) + }; + + Some(Point::new( + glyph.x + glyph.x_offset * glyph.font_size + advance, + glyph.y - glyph.y_offset * glyph.font_size, + )) + } +} + +impl Default for Paragraph { + fn default() -> Self { + Self(Arc::new(Internal::default())) + } +} + +impl fmt::Debug for Paragraph { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let paragraph = self.internal(); + + f.debug_struct("Paragraph") + .field("font", ¶graph.font) + .field("shaping", ¶graph.shaping) + .field("horizontal_alignment", ¶graph.horizontal_alignment) + .field("vertical_alignment", ¶graph.vertical_alignment) + .field("bounds", ¶graph.bounds) + .field("min_bounds", ¶graph.min_bounds) + .finish() + } +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + self.font == other.font + && self.shaping == other.shaping + && self.horizontal_alignment == other.horizontal_alignment + && self.vertical_alignment == other.vertical_alignment + && self.bounds == other.bounds + && self.min_bounds == other.min_bounds + && self.buffer.metrics() == other.buffer.metrics() + } +} + +impl Default for Internal { + fn default() -> Self { + Self { + buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }), + font: Font::default(), + shaping: Shaping::default(), + wrapping: Wrapping::default(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + bounds: Size::ZERO, + min_bounds: Size::ZERO, + version: text::Version::default(), + } + } +} + +/// A weak reference to a [`Paragraph`]. +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak, + /// The minimum bounds of the [`Paragraph`]. + pub min_bounds: Size, + /// The horizontal alignment of the [`Paragraph`]. + pub horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the [`Paragraph`]. + pub vertical_alignment: alignment::Vertical, +} + +impl Weak { + /// Tries to update the reference into a [`Paragraph`]. + pub fn upgrade(&self) -> Option { + self.raw.upgrade().map(Paragraph) + } +} + +impl PartialEq for Weak { + fn eq(&self, other: &Self) -> bool { + match (self.raw.upgrade(), other.raw.upgrade()) { + (Some(p1), Some(p2)) => p1 == p2, + _ => false, + } + } +} diff --git a/patches/iced_graphics/src/viewport.rs b/patches/iced_graphics/src/viewport.rs new file mode 100644 index 0000000..dc8e21d --- /dev/null +++ b/patches/iced_graphics/src/viewport.rs @@ -0,0 +1,56 @@ +use crate::core::{Size, Transformation}; + +/// A viewing region for displaying computer graphics. +#[derive(Debug, Clone)] +pub struct Viewport { + physical_size: Size, + logical_size: Size, + scale_factor: f64, + projection: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given physical dimensions and scale + /// factor. + pub fn with_physical_size(size: Size, scale_factor: f64) -> Viewport { + Viewport { + physical_size: size, + logical_size: Size::new( + (size.width as f64 / scale_factor) as f32, + (size.height as f64 / scale_factor) as f32, + ), + scale_factor, + projection: Transformation::orthographic(size.width, size.height), + } + } + + /// Returns the physical size of the [`Viewport`]. + pub fn physical_size(&self) -> Size { + self.physical_size + } + + /// Returns the physical width of the [`Viewport`]. + pub fn physical_width(&self) -> u32 { + self.physical_size.width + } + + /// Returns the physical height of the [`Viewport`]. + pub fn physical_height(&self) -> u32 { + self.physical_size.height + } + + /// Returns the logical size of the [`Viewport`]. + pub fn logical_size(&self) -> Size { + self.logical_size + } + + /// Returns the scale factor of the [`Viewport`]. + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + /// Returns the projection transformation of the [`Viewport`]. + pub fn projection(&self) -> Transformation { + self.projection + } +}