diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cfeb0c24..48c9e9ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ For small things, just go ahead an open a PR. For bigger things, please file an Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pieces connects. You can test your code locally by running `./scripts/check.sh`. -There are snapshots test that might need to be updated. +There are snapshots test that might need to be updated. Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them. For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md). @@ -102,6 +102,7 @@ While using an immediate mode gui is simple, implementing one is a lot more tric * Avoid double negatives * Flip `if !condition {} else {}` * Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`) +* Put each type in their own file, unless they are trivial (e.g. a `struct` with no `impl`) * Break the above rules when it makes sense diff --git a/Cargo.toml b/Cargo.toml index 799e4f98..1dcae33b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -266,11 +266,10 @@ use_self = "warn" useless_transmute = "warn" verbose_file_reads = "warn" wildcard_dependencies = "warn" -wildcard_imports = "warn" zero_sized_map_values = "warn" -# TODO(emilk): enable more of these lints: +# TODO(emilk): maybe enable more of these lints? iter_over_hash_type = "allow" missing_assert_message = "allow" should_panic_without_expect = "allow" @@ -284,3 +283,4 @@ let_underscore_untyped = "allow" manual_range_contains = "allow" # this one is just worse imho self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602 significant_drop_tightening = "allow" # Too many false positives +wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 1cc9e6c9..5aeb9676 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -23,7 +23,6 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] -mod bezier; pub mod color; pub mod image; mod margin; @@ -32,8 +31,8 @@ pub mod mutex; mod rounding; mod roundingf; mod shadow; -mod shape; pub mod shape_transform; +mod shapes; pub mod stats; mod stroke; pub mod tessellator; @@ -42,9 +41,9 @@ mod texture_atlas; mod texture_handle; pub mod textures; pub mod util; +mod viewport; pub use self::{ - bezier::{CubicBezierShape, QuadraticBezierShape}, color::ColorMode, image::{ColorImage, FontImage, ImageData, ImageDelta}, margin::Margin, @@ -52,9 +51,9 @@ pub use self::{ rounding::Rounding, roundingf::Roundingf, shadow::Shadow, - shape::{ - CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, Shape, - TextShape, + shapes::{ + CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, + QuadraticBezierShape, RectShape, Shape, TextShape, }, stats::PaintStats, stroke::{PathStroke, Stroke, StrokeKind}, @@ -63,6 +62,7 @@ pub use self::{ texture_atlas::TextureAtlas, texture_handle::TextureHandle, textures::TextureManager, + viewport::ViewportInPixels, }; #[allow(deprecated)] diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs deleted file mode 100644 index 1fc393c7..00000000 --- a/crates/epaint/src/shape.rs +++ /dev/null @@ -1,1102 +0,0 @@ -//! The different shapes that can be painted. - -use std::{any::Any, sync::Arc}; - -use crate::{ - stroke::PathStroke, - text::{FontId, Fonts, Galley}, - Color32, Mesh, Rounding, Stroke, TextureId, -}; -use emath::{pos2, Align2, Pos2, Rangef, Rect, TSTransform, Vec2}; - -pub use crate::{CubicBezierShape, QuadraticBezierShape}; - -/// A paint primitive such as a circle or a piece of text. -/// Coordinates are all screen space points (not physical pixels). -/// -/// You should generally recreate your [`Shape`]s each frame, -/// but storing them should also be fine with one exception: -/// [`Shape::Text`] depends on the current `pixels_per_point` (dpi scale) -/// and so must be recreated every time `pixels_per_point` changes. -#[must_use = "Add a Shape to a Painter"] -#[derive(Clone, Debug, PartialEq)] -pub enum Shape { - /// Paint nothing. This can be useful as a placeholder. - Noop, - - /// Recursively nest more shapes - sometimes a convenience to be able to do. - /// For performance reasons it is better to avoid it. - Vec(Vec), - - /// Circle with optional outline and fill. - Circle(CircleShape), - - /// Ellipse with optional outline and fill. - Ellipse(EllipseShape), - - /// A line between two points. - LineSegment { points: [Pos2; 2], stroke: Stroke }, - - /// A series of lines between points. - /// The path can have a stroke and/or fill (if closed). - Path(PathShape), - - /// Rectangle with optional outline and fill. - Rect(RectShape), - - /// Text. - /// - /// This needs to be recreated if `pixels_per_point` (dpi scale) changes. - Text(TextShape), - - /// A general triangle mesh. - /// - /// Can be used to display images. - Mesh(Mesh), - - /// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). - QuadraticBezier(QuadraticBezierShape), - - /// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). - CubicBezier(CubicBezierShape), - - /// Backend-specific painting. - Callback(PaintCallback), -} - -#[test] -fn shape_impl_send_sync() { - fn assert_send_sync() {} - assert_send_sync::(); -} - -impl From> for Shape { - #[inline(always)] - fn from(shapes: Vec) -> Self { - Self::Vec(shapes) - } -} - -impl From for Shape { - #[inline(always)] - fn from(mesh: Mesh) -> Self { - Self::Mesh(mesh) - } -} - -/// ## Constructors -impl Shape { - /// A line between two points. - /// More efficient than calling [`Self::line`]. - #[inline] - pub fn line_segment(points: [Pos2; 2], stroke: impl Into) -> Self { - Self::LineSegment { - points, - stroke: stroke.into(), - } - } - - /// A horizontal line. - pub fn hline(x: impl Into, y: f32, stroke: impl Into) -> Self { - let x = x.into(); - Self::LineSegment { - points: [pos2(x.min, y), pos2(x.max, y)], - stroke: stroke.into(), - } - } - - /// A vertical line. - pub fn vline(x: f32, y: impl Into, stroke: impl Into) -> Self { - let y = y.into(); - Self::LineSegment { - points: [pos2(x, y.min), pos2(x, y.max)], - stroke: stroke.into(), - } - } - - /// A line through many points. - /// - /// Use [`Self::line_segment`] instead if your line only connects two points. - #[inline] - pub fn line(points: Vec, stroke: impl Into) -> Self { - Self::Path(PathShape::line(points, stroke)) - } - - /// A line that closes back to the start point again. - #[inline] - pub fn closed_line(points: Vec, stroke: impl Into) -> Self { - Self::Path(PathShape::closed_line(points, stroke)) - } - - /// Turn a line into equally spaced dots. - pub fn dotted_line( - path: &[Pos2], - color: impl Into, - spacing: f32, - radius: f32, - ) -> Vec { - let mut shapes = Vec::new(); - points_from_line(path, spacing, radius, color.into(), &mut shapes); - shapes - } - - /// Turn a line into dashes. - pub fn dashed_line( - path: &[Pos2], - stroke: impl Into, - dash_length: f32, - gap_length: f32, - ) -> Vec { - let mut shapes = Vec::new(); - dashes_from_line( - path, - stroke.into(), - &[dash_length], - &[gap_length], - &mut shapes, - 0., - ); - shapes - } - - /// Turn a line into dashes with different dash/gap lengths and a start offset. - pub fn dashed_line_with_offset( - path: &[Pos2], - stroke: impl Into, - dash_lengths: &[f32], - gap_lengths: &[f32], - dash_offset: f32, - ) -> Vec { - let mut shapes = Vec::new(); - dashes_from_line( - path, - stroke.into(), - dash_lengths, - gap_lengths, - &mut shapes, - dash_offset, - ); - shapes - } - - /// Turn a line into dashes. If you need to create many dashed lines use this instead of - /// [`Self::dashed_line`]. - pub fn dashed_line_many( - points: &[Pos2], - stroke: impl Into, - dash_length: f32, - gap_length: f32, - shapes: &mut Vec, - ) { - dashes_from_line( - points, - stroke.into(), - &[dash_length], - &[gap_length], - shapes, - 0., - ); - } - - /// Turn a line into dashes with different dash/gap lengths and a start offset. If you need to - /// create many dashed lines use this instead of [`Self::dashed_line_with_offset`]. - pub fn dashed_line_many_with_offset( - points: &[Pos2], - stroke: impl Into, - dash_lengths: &[f32], - gap_lengths: &[f32], - dash_offset: f32, - shapes: &mut Vec, - ) { - dashes_from_line( - points, - stroke.into(), - dash_lengths, - gap_lengths, - shapes, - dash_offset, - ); - } - - /// A convex polygon with a fill and optional stroke. - /// - /// The most performant winding order is clockwise. - #[inline] - pub fn convex_polygon( - points: Vec, - fill: impl Into, - stroke: impl Into, - ) -> Self { - Self::Path(PathShape::convex_polygon(points, fill, stroke)) - } - - #[inline] - pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into) -> Self { - Self::Circle(CircleShape::filled(center, radius, fill_color)) - } - - #[inline] - pub fn circle_stroke(center: Pos2, radius: f32, stroke: impl Into) -> Self { - Self::Circle(CircleShape::stroke(center, radius, stroke)) - } - - #[inline] - pub fn ellipse_filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { - Self::Ellipse(EllipseShape::filled(center, radius, fill_color)) - } - - #[inline] - pub fn ellipse_stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { - Self::Ellipse(EllipseShape::stroke(center, radius, stroke)) - } - - #[inline] - pub fn rect_filled( - rect: Rect, - rounding: impl Into, - fill_color: impl Into, - ) -> Self { - Self::Rect(RectShape::filled(rect, rounding, fill_color)) - } - - /// The stroke extends _outside_ the [`Rect`]. - #[inline] - pub fn rect_stroke( - rect: Rect, - rounding: impl Into, - stroke: impl Into, - ) -> Self { - Self::Rect(RectShape::stroke(rect, rounding, stroke)) - } - - #[allow(clippy::needless_pass_by_value)] - pub fn text( - fonts: &Fonts, - pos: Pos2, - anchor: Align2, - text: impl ToString, - font_id: FontId, - color: Color32, - ) -> Self { - let galley = fonts.layout_no_wrap(text.to_string(), font_id, color); - let rect = anchor.anchor_size(pos, galley.size()); - Self::galley(rect.min, galley, color) - } - - /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color. - /// - /// Any non-placeholder color in the galley takes precedence over this fallback color. - #[inline] - pub fn galley(pos: Pos2, galley: Arc, fallback_color: Color32) -> Self { - TextShape::new(pos, galley, fallback_color).into() - } - - /// All text color in the [`Galley`] will be replaced with the given color. - #[inline] - pub fn galley_with_override_text_color( - pos: Pos2, - galley: Arc, - text_color: Color32, - ) -> Self { - TextShape::new(pos, galley, text_color) - .with_override_text_color(text_color) - .into() - } - - #[inline] - #[deprecated = "Use `Shape::galley` or `Shape::galley_with_override_text_color` instead"] - pub fn galley_with_color(pos: Pos2, galley: Arc, text_color: Color32) -> Self { - Self::galley_with_override_text_color(pos, galley, text_color) - } - - #[inline] - pub fn mesh(mesh: Mesh) -> Self { - debug_assert!(mesh.is_valid()); - Self::Mesh(mesh) - } - - /// An image at the given position. - /// - /// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))` - /// unless you want to crop or flip the image. - /// - /// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image. - pub fn image(texture_id: TextureId, rect: Rect, uv: Rect, tint: Color32) -> Self { - let mut mesh = Mesh::with_texture(texture_id); - mesh.add_rect_with_uv(rect, uv, tint); - Self::mesh(mesh) - } - - /// The visual bounding rectangle (includes stroke widths) - pub fn visual_bounding_rect(&self) -> Rect { - match self { - Self::Noop => Rect::NOTHING, - Self::Vec(shapes) => { - let mut rect = Rect::NOTHING; - for shape in shapes { - rect = rect.union(shape.visual_bounding_rect()); - } - rect - } - Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(), - Self::Ellipse(ellipse_shape) => ellipse_shape.visual_bounding_rect(), - Self::LineSegment { points, stroke } => { - if stroke.is_empty() { - Rect::NOTHING - } else { - Rect::from_two_pos(points[0], points[1]).expand(stroke.width / 2.0) - } - } - Self::Path(path_shape) => path_shape.visual_bounding_rect(), - Self::Rect(rect_shape) => rect_shape.visual_bounding_rect(), - Self::Text(text_shape) => text_shape.visual_bounding_rect(), - Self::Mesh(mesh) => mesh.calc_bounds(), - Self::QuadraticBezier(bezier) => bezier.visual_bounding_rect(), - Self::CubicBezier(bezier) => bezier.visual_bounding_rect(), - Self::Callback(custom) => custom.rect, - } - } -} - -/// ## Inspection and transforms -impl Shape { - #[inline(always)] - pub fn texture_id(&self) -> super::TextureId { - if let Self::Mesh(mesh) = self { - mesh.texture_id - } else if let Self::Rect(rect_shape) = self { - rect_shape.fill_texture_id - } else { - super::TextureId::default() - } - } - - /// Scale the shape by `factor`, in-place. - /// - /// A wrapper around [`Self::transform`]. - #[inline(always)] - pub fn scale(&mut self, factor: f32) { - self.transform(TSTransform::from_scaling(factor)); - } - - /// Move the shape by `delta`, in-place. - /// - /// A wrapper around [`Self::transform`]. - #[inline(always)] - pub fn translate(&mut self, delta: Vec2) { - self.transform(TSTransform::from_translation(delta)); - } - - /// Move the shape by this many points, in-place. - /// - /// If using a [`PaintCallback`], note that only the rect is scaled as opposed - /// to other shapes where the stroke is also scaled. - pub fn transform(&mut self, transform: TSTransform) { - match self { - Self::Noop => {} - Self::Vec(shapes) => { - for shape in shapes { - shape.transform(transform); - } - } - Self::Circle(circle_shape) => { - circle_shape.center = transform * circle_shape.center; - circle_shape.radius *= transform.scaling; - circle_shape.stroke.width *= transform.scaling; - } - Self::Ellipse(ellipse_shape) => { - ellipse_shape.center = transform * ellipse_shape.center; - ellipse_shape.radius *= transform.scaling; - ellipse_shape.stroke.width *= transform.scaling; - } - Self::LineSegment { points, stroke } => { - for p in points { - *p = transform * *p; - } - stroke.width *= transform.scaling; - } - Self::Path(path_shape) => { - for p in &mut path_shape.points { - *p = transform * *p; - } - path_shape.stroke.width *= transform.scaling; - } - Self::Rect(rect_shape) => { - rect_shape.rect = transform * rect_shape.rect; - rect_shape.stroke.width *= transform.scaling; - rect_shape.rounding *= transform.scaling; - } - Self::Text(text_shape) => { - text_shape.pos = transform * text_shape.pos; - - // Scale text: - let galley = Arc::make_mut(&mut text_shape.galley); - for row in &mut galley.rows { - row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; - for v in &mut row.visuals.mesh.vertices { - v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); - } - } - - galley.mesh_bounds = transform.scaling * galley.mesh_bounds; - galley.rect = transform.scaling * galley.rect; - } - Self::Mesh(mesh) => { - mesh.transform(transform); - } - Self::QuadraticBezier(bezier_shape) => { - bezier_shape.points[0] = transform * bezier_shape.points[0]; - bezier_shape.points[1] = transform * bezier_shape.points[1]; - bezier_shape.points[2] = transform * bezier_shape.points[2]; - bezier_shape.stroke.width *= transform.scaling; - } - Self::CubicBezier(cubic_curve) => { - for p in &mut cubic_curve.points { - *p = transform * *p; - } - cubic_curve.stroke.width *= transform.scaling; - } - Self::Callback(shape) => { - shape.rect = transform * shape.rect; - } - } - } -} - -// ---------------------------------------------------------------------------- - -/// How to paint a circle. -#[derive(Copy, Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct CircleShape { - pub center: Pos2, - pub radius: f32, - pub fill: Color32, - pub stroke: Stroke, -} - -impl CircleShape { - #[inline] - pub fn filled(center: Pos2, radius: f32, fill_color: impl Into) -> Self { - Self { - center, - radius, - fill: fill_color.into(), - stroke: Default::default(), - } - } - - #[inline] - pub fn stroke(center: Pos2, radius: f32, stroke: impl Into) -> Self { - Self { - center, - radius, - fill: Default::default(), - stroke: stroke.into(), - } - } - - /// The visual bounding rectangle (includes stroke width) - pub fn visual_bounding_rect(&self) -> Rect { - if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { - Rect::NOTHING - } else { - Rect::from_center_size( - self.center, - Vec2::splat(self.radius * 2.0 + self.stroke.width), - ) - } - } -} - -impl From for Shape { - #[inline(always)] - fn from(shape: CircleShape) -> Self { - Self::Circle(shape) - } -} - -// ---------------------------------------------------------------------------- - -/// How to paint an ellipse. -#[derive(Copy, Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct EllipseShape { - pub center: Pos2, - - /// Radius is the vector (a, b) where the width of the Ellipse is 2a and the height is 2b - pub radius: Vec2, - pub fill: Color32, - pub stroke: Stroke, -} - -impl EllipseShape { - #[inline] - pub fn filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { - Self { - center, - radius, - fill: fill_color.into(), - stroke: Default::default(), - } - } - - #[inline] - pub fn stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { - Self { - center, - radius, - fill: Default::default(), - stroke: stroke.into(), - } - } - - /// The visual bounding rectangle (includes stroke width) - pub fn visual_bounding_rect(&self) -> Rect { - if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { - Rect::NOTHING - } else { - Rect::from_center_size( - self.center, - self.radius * 2.0 + Vec2::splat(self.stroke.width), - ) - } - } -} - -impl From for Shape { - #[inline(always)] - fn from(shape: EllipseShape) -> Self { - Self::Ellipse(shape) - } -} - -// ---------------------------------------------------------------------------- - -/// A path which can be stroked and/or filled (if closed). -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct PathShape { - /// Filled paths should prefer clockwise order. - pub points: Vec, - - /// If true, connect the first and last of the points together. - /// This is required if `fill != TRANSPARENT`. - pub closed: bool, - - /// Fill is only supported for convex polygons. - pub fill: Color32, - - /// Color and thickness of the line. - pub stroke: PathStroke, - // TODO(emilk): Add texture support either by supplying uv for each point, - // or by some transform from points to uv (e.g. a callback or a linear transform matrix). -} - -impl PathShape { - /// A line through many points. - /// - /// Use [`Shape::line_segment`] instead if your line only connects two points. - #[inline] - pub fn line(points: Vec, stroke: impl Into) -> Self { - Self { - points, - closed: false, - fill: Default::default(), - stroke: stroke.into(), - } - } - - /// A line that closes back to the start point again. - #[inline] - pub fn closed_line(points: Vec, stroke: impl Into) -> Self { - Self { - points, - closed: true, - fill: Default::default(), - stroke: stroke.into(), - } - } - - /// A convex polygon with a fill and optional stroke. - /// - /// The most performant winding order is clockwise. - #[inline] - pub fn convex_polygon( - points: Vec, - fill: impl Into, - stroke: impl Into, - ) -> Self { - Self { - points, - closed: true, - fill: fill.into(), - stroke: stroke.into(), - } - } - - /// The visual bounding rectangle (includes stroke width) - #[inline] - pub fn visual_bounding_rect(&self) -> Rect { - if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { - Rect::NOTHING - } else { - Rect::from_points(&self.points).expand(self.stroke.width / 2.0) - } - } -} - -impl From for Shape { - #[inline(always)] - fn from(shape: PathShape) -> Self { - Self::Path(shape) - } -} - -// ---------------------------------------------------------------------------- - -/// How to paint a rectangle. -#[derive(Copy, Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct RectShape { - pub rect: Rect, - - /// How rounded the corners are. Use `Rounding::ZERO` for no rounding. - pub rounding: Rounding, - - /// How to fill the rectangle. - pub fill: Color32, - - /// The thickness and color of the outline. - /// - /// The stroke extends _outside_ the edge of [`Self::rect`], - /// i.e. using [`crate::StrokeKind::Outside`]. - /// - /// This means the [`Self::visual_bounding_rect`] is `rect.size() + 2.0 * stroke.width`. - pub stroke: Stroke, - - /// If larger than zero, the edges of the rectangle - /// (for both fill and stroke) will be blurred. - /// - /// This can be used to produce shadows and glow effects. - /// - /// The blur is currently implemented using a simple linear blur in sRGBA gamma space. - pub blur_width: f32, - - /// If the rect should be filled with a texture, which one? - /// - /// The texture is multiplied with [`Self::fill`]. - pub fill_texture_id: TextureId, - - /// What UV coordinates to use for the texture? - /// - /// To display a texture, set [`Self::fill_texture_id`], - /// and set this to `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`. - /// - /// Use [`Rect::ZERO`] to turn off texturing. - pub uv: Rect, -} - -impl RectShape { - /// The stroke extends _outside_ the [`Rect`]. - #[inline] - pub fn new( - rect: Rect, - rounding: impl Into, - fill_color: impl Into, - stroke: impl Into, - ) -> Self { - Self { - rect, - rounding: rounding.into(), - fill: fill_color.into(), - stroke: stroke.into(), - blur_width: 0.0, - fill_texture_id: Default::default(), - uv: Rect::ZERO, - } - } - - #[inline] - pub fn filled( - rect: Rect, - rounding: impl Into, - fill_color: impl Into, - ) -> Self { - Self { - rect, - rounding: rounding.into(), - fill: fill_color.into(), - stroke: Default::default(), - blur_width: 0.0, - fill_texture_id: Default::default(), - uv: Rect::ZERO, - } - } - - /// The stroke extends _outside_ the [`Rect`]. - #[inline] - pub fn stroke(rect: Rect, rounding: impl Into, stroke: impl Into) -> Self { - Self { - rect, - rounding: rounding.into(), - fill: Default::default(), - stroke: stroke.into(), - blur_width: 0.0, - fill_texture_id: Default::default(), - uv: Rect::ZERO, - } - } - - /// If larger than zero, the edges of the rectangle - /// (for both fill and stroke) will be blurred. - /// - /// This can be used to produce shadows and glow effects. - /// - /// The blur is currently implemented using a simple linear blur in `sRGBA` gamma space. - #[inline] - pub fn with_blur_width(mut self, blur_width: f32) -> Self { - self.blur_width = blur_width; - self - } - - /// The visual bounding rectangle (includes stroke width) - #[inline] - pub fn visual_bounding_rect(&self) -> Rect { - if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { - Rect::NOTHING - } else { - let Stroke { width, .. } = self.stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke` - self.rect.expand(width + self.blur_width / 2.0) - } - } -} - -impl From for Shape { - #[inline(always)] - fn from(shape: RectShape) -> Self { - Self::Rect(shape) - } -} - -// ---------------------------------------------------------------------------- - -/// How to paint some text on screen. -/// -/// This needs to be recreated if `pixels_per_point` (dpi scale) changes. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct TextShape { - /// Top left corner of the first character. - pub pos: Pos2, - - /// The laid out text, from [`Fonts::layout_job`]. - pub galley: Arc, - - /// Add this underline to the whole text. - /// You can also set an underline when creating the galley. - pub underline: Stroke, - - /// Any [`Color32::PLACEHOLDER`] in the galley will be replaced by the given color. - /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. - pub fallback_color: Color32, - - /// If set, the text color in the galley will be ignored and replaced - /// with the given color. - /// - /// This only affects the glyphs and will NOT replace background color nor strikethrough/underline color. - pub override_text_color: Option, - - /// If set, the text will be rendered with the given opacity in gamma space - /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. - pub opacity_factor: f32, - - /// Rotate text by this many radians clockwise. - /// The pivot is `pos` (the upper left corner of the text). - pub angle: f32, -} - -impl TextShape { - /// The given fallback color will be used for any uncolored part of the galley (using [`Color32::PLACEHOLDER`]). - /// - /// Any non-placeholder color in the galley takes precedence over this fallback color. - #[inline] - pub fn new(pos: Pos2, galley: Arc, fallback_color: Color32) -> Self { - Self { - pos, - galley, - underline: Stroke::NONE, - fallback_color, - override_text_color: None, - opacity_factor: 1.0, - angle: 0.0, - } - } - - /// The visual bounding rectangle - #[inline] - pub fn visual_bounding_rect(&self) -> Rect { - self.galley.mesh_bounds.translate(self.pos.to_vec2()) - } - - #[inline] - pub fn with_underline(mut self, underline: Stroke) -> Self { - self.underline = underline; - self - } - - /// Use the given color for the text, regardless of what color is already in the galley. - #[inline] - pub fn with_override_text_color(mut self, override_text_color: Color32) -> Self { - self.override_text_color = Some(override_text_color); - self - } - - /// Rotate text by this many radians clockwise. - /// The pivot is `pos` (the upper left corner of the text). - #[inline] - pub fn with_angle(mut self, angle: f32) -> Self { - self.angle = angle; - self - } - - /// Render text with this opacity in gamma space - #[inline] - pub fn with_opacity_factor(mut self, opacity_factor: f32) -> Self { - self.opacity_factor = opacity_factor; - self - } -} - -impl From for Shape { - #[inline(always)] - fn from(shape: TextShape) -> Self { - Self::Text(shape) - } -} - -// ---------------------------------------------------------------------------- - -/// Creates equally spaced filled circles from a line. -fn points_from_line( - path: &[Pos2], - spacing: f32, - radius: f32, - color: Color32, - shapes: &mut Vec, -) { - let mut position_on_segment = 0.0; - path.windows(2).for_each(|window| { - let (start, end) = (window[0], window[1]); - let vector = end - start; - let segment_length = vector.length(); - while position_on_segment < segment_length { - let new_point = start + vector * (position_on_segment / segment_length); - shapes.push(Shape::circle_filled(new_point, radius, color)); - position_on_segment += spacing; - } - position_on_segment -= segment_length; - }); -} - -/// Creates dashes from a line. -fn dashes_from_line( - path: &[Pos2], - stroke: Stroke, - dash_lengths: &[f32], - gap_lengths: &[f32], - shapes: &mut Vec, - dash_offset: f32, -) { - assert_eq!(dash_lengths.len(), gap_lengths.len()); - let mut position_on_segment = dash_offset; - let mut drawing_dash = false; - let mut step = 0; - let steps = dash_lengths.len(); - path.windows(2).for_each(|window| { - let (start, end) = (window[0], window[1]); - let vector = end - start; - let segment_length = vector.length(); - - let mut start_point = start; - while position_on_segment < segment_length { - let new_point = start + vector * (position_on_segment / segment_length); - if drawing_dash { - // This is the end point. - shapes.push(Shape::line_segment([start_point, new_point], stroke)); - position_on_segment += gap_lengths[step]; - // Increment step counter - step += 1; - if step >= steps { - step = 0; - } - } else { - // Start a new dash. - start_point = new_point; - position_on_segment += dash_lengths[step]; - } - drawing_dash = !drawing_dash; - } - - // If the segment ends and the dash is not finished, add the segment's end point. - if drawing_dash { - shapes.push(Shape::line_segment([start_point, end], stroke)); - } - - position_on_segment -= segment_length; - }); -} - -// ---------------------------------------------------------------------------- - -/// Information passed along with [`PaintCallback`] ([`Shape::Callback`]). -pub struct PaintCallbackInfo { - /// Viewport in points. - /// - /// This specifies where on the screen to paint, and the borders of this - /// Rect is the [-1, +1] of the Normalized Device Coordinates. - /// - /// Note than only a portion of this may be visible due to [`Self::clip_rect`]. - /// - /// This comes from [`PaintCallback::rect`]. - pub viewport: Rect, - - /// Clip rectangle in points. - pub clip_rect: Rect, - - /// Pixels per point. - pub pixels_per_point: f32, - - /// Full size of the screen, in pixels. - pub screen_size_px: [u32; 2], -} - -/// Size of the viewport in whole, physical pixels. -pub struct ViewportInPixels { - /// Physical pixel offset for left side of the viewport. - pub left_px: i32, - - /// Physical pixel offset for top side of the viewport. - pub top_px: i32, - - /// Physical pixel offset for bottom side of the viewport. - /// - /// This is what `glViewport`, `glScissor` etc expects for the y axis. - pub from_bottom_px: i32, - - /// Viewport width in physical pixels. - pub width_px: i32, - - /// Viewport height in physical pixels. - pub height_px: i32, -} - -impl ViewportInPixels { - fn from_points(rect: &Rect, pixels_per_point: f32, screen_size_px: [u32; 2]) -> Self { - // Fractional pixel values for viewports are generally valid, but may cause sampling issues - // and rounding errors might cause us to get out of bounds. - - // Round: - let left_px = (pixels_per_point * rect.min.x).round() as i32; // inclusive - let top_px = (pixels_per_point * rect.min.y).round() as i32; // inclusive - let right_px = (pixels_per_point * rect.max.x).round() as i32; // exclusive - let bottom_px = (pixels_per_point * rect.max.y).round() as i32; // exclusive - - // Clamp to screen: - let screen_width = screen_size_px[0] as i32; - let screen_height = screen_size_px[1] as i32; - let left_px = left_px.clamp(0, screen_width); - let right_px = right_px.clamp(left_px, screen_width); - let top_px = top_px.clamp(0, screen_height); - let bottom_px = bottom_px.clamp(top_px, screen_height); - - let width_px = right_px - left_px; - let height_px = bottom_px - top_px; - - Self { - left_px, - top_px, - from_bottom_px: screen_height - height_px - top_px, - width_px, - height_px, - } - } -} - -#[test] -fn test_viewport_rounding() { - for i in 0..=10_000 { - // Two adjacent viewports should never overlap: - let x = i as f32 / 97.0; - let left = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_max_x(x); - let right = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_min_x(x); - - for pixels_per_point in [0.618, 1.0, std::f32::consts::PI] { - let left = ViewportInPixels::from_points(&left, pixels_per_point, [100, 100]); - let right = ViewportInPixels::from_points(&right, pixels_per_point, [100, 100]); - assert_eq!(left.left_px + left.width_px, right.left_px); - } - } -} - -impl PaintCallbackInfo { - /// The viewport rectangle. This is what you would use in e.g. `glViewport`. - pub fn viewport_in_pixels(&self) -> ViewportInPixels { - ViewportInPixels::from_points(&self.viewport, self.pixels_per_point, self.screen_size_px) - } - - /// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`. - pub fn clip_rect_in_pixels(&self) -> ViewportInPixels { - ViewportInPixels::from_points(&self.clip_rect, self.pixels_per_point, self.screen_size_px) - } -} - -/// If you want to paint some 3D shapes inside an egui region, you can use this. -/// -/// This is advanced usage, and is backend specific. -#[derive(Clone)] -pub struct PaintCallback { - /// Where to paint. - /// - /// This will become [`PaintCallbackInfo::viewport`]. - pub rect: Rect, - - /// Paint something custom (e.g. 3D stuff). - /// - /// The concrete value of `callback` depends on the rendering backend used. For instance, the - /// `glow` backend requires that callback be an `egui_glow::CallbackFn` while the `wgpu` - /// backend requires a `egui_wgpu::Callback`. - /// - /// If the type cannot be downcast to the type expected by the current backend the callback - /// will not be drawn. - /// - /// The rendering backend is responsible for first setting the active viewport to - /// [`Self::rect`]. - /// - /// The rendering backend is also responsible for restoring any state, such as the bound shader - /// program, vertex array, etc. - /// - /// Shape has to be clone, therefore this has to be an `Arc` instead of a `Box`. - pub callback: Arc, -} - -impl std::fmt::Debug for PaintCallback { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CustomShape") - .field("rect", &self.rect) - .finish_non_exhaustive() - } -} - -impl std::cmp::PartialEq for PaintCallback { - fn eq(&self, other: &Self) -> bool { - self.rect.eq(&other.rect) && Arc::ptr_eq(&self.callback, &other.callback) - } -} - -impl From for Shape { - #[inline(always)] - fn from(shape: PaintCallback) -> Self { - Self::Callback(shape) - } -} diff --git a/crates/epaint/src/bezier.rs b/crates/epaint/src/shapes/bezier_shape.rs similarity index 99% rename from crates/epaint/src/bezier.rs rename to crates/epaint/src/shapes/bezier_shape.rs index cab0d29f..7d291c7f 100644 --- a/crates/epaint/src/bezier.rs +++ b/crates/epaint/src/shapes/bezier_shape.rs @@ -3,7 +3,7 @@ use std::ops::Range; -use crate::{shape::Shape, Color32, PathShape, PathStroke}; +use crate::{Color32, PathShape, PathStroke, Shape}; use emath::{Pos2, Rect, RectTransform}; // ---------------------------------------------------------------------------- diff --git a/crates/epaint/src/shapes/circle_shape.rs b/crates/epaint/src/shapes/circle_shape.rs new file mode 100644 index 00000000..a86ae3f6 --- /dev/null +++ b/crates/epaint/src/shapes/circle_shape.rs @@ -0,0 +1,52 @@ +use crate::{Color32, Pos2, Rect, Shape, Stroke, Vec2}; + +/// How to paint a circle. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct CircleShape { + pub center: Pos2, + pub radius: f32, + pub fill: Color32, + pub stroke: Stroke, +} + +impl CircleShape { + #[inline] + pub fn filled(center: Pos2, radius: f32, fill_color: impl Into) -> Self { + Self { + center, + radius, + fill: fill_color.into(), + stroke: Default::default(), + } + } + + #[inline] + pub fn stroke(center: Pos2, radius: f32, stroke: impl Into) -> Self { + Self { + center, + radius, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// The visual bounding rectangle (includes stroke width) + pub fn visual_bounding_rect(&self) -> Rect { + if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { + Rect::NOTHING + } else { + Rect::from_center_size( + self.center, + Vec2::splat(self.radius * 2.0 + self.stroke.width), + ) + } + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: CircleShape) -> Self { + Self::Circle(shape) + } +} diff --git a/crates/epaint/src/shapes/ellipse_shape.rs b/crates/epaint/src/shapes/ellipse_shape.rs new file mode 100644 index 00000000..310638d0 --- /dev/null +++ b/crates/epaint/src/shapes/ellipse_shape.rs @@ -0,0 +1,54 @@ +use crate::*; + +/// How to paint an ellipse. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct EllipseShape { + pub center: Pos2, + + /// Radius is the vector (a, b) where the width of the Ellipse is 2a and the height is 2b + pub radius: Vec2, + pub fill: Color32, + pub stroke: Stroke, +} + +impl EllipseShape { + #[inline] + pub fn filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { + Self { + center, + radius, + fill: fill_color.into(), + stroke: Default::default(), + } + } + + #[inline] + pub fn stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { + Self { + center, + radius, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// The visual bounding rectangle (includes stroke width) + pub fn visual_bounding_rect(&self) -> Rect { + if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { + Rect::NOTHING + } else { + Rect::from_center_size( + self.center, + self.radius * 2.0 + Vec2::splat(self.stroke.width), + ) + } + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: EllipseShape) -> Self { + Self::Ellipse(shape) + } +} diff --git a/crates/epaint/src/shapes/mod.rs b/crates/epaint/src/shapes/mod.rs new file mode 100644 index 00000000..8a42b2c9 --- /dev/null +++ b/crates/epaint/src/shapes/mod.rs @@ -0,0 +1,19 @@ +mod bezier_shape; +mod circle_shape; +mod ellipse_shape; +mod paint_callback; +mod path_shape; +mod rect_shape; +mod shape; +mod text_shape; + +pub use self::{ + bezier_shape::{CubicBezierShape, QuadraticBezierShape}, + circle_shape::CircleShape, + ellipse_shape::EllipseShape, + paint_callback::{PaintCallback, PaintCallbackInfo}, + path_shape::PathShape, + rect_shape::RectShape, + shape::Shape, + text_shape::TextShape, +}; diff --git a/crates/epaint/src/shapes/paint_callback.rs b/crates/epaint/src/shapes/paint_callback.rs new file mode 100644 index 00000000..00882f0f --- /dev/null +++ b/crates/epaint/src/shapes/paint_callback.rs @@ -0,0 +1,103 @@ +use std::{any::Any, sync::Arc}; + +use crate::*; + +/// Information passed along with [`PaintCallback`] ([`Shape::Callback`]). +pub struct PaintCallbackInfo { + /// Viewport in points. + /// + /// This specifies where on the screen to paint, and the borders of this + /// Rect is the [-1, +1] of the Normalized Device Coordinates. + /// + /// Note than only a portion of this may be visible due to [`Self::clip_rect`]. + /// + /// This comes from [`PaintCallback::rect`]. + pub viewport: Rect, + + /// Clip rectangle in points. + pub clip_rect: Rect, + + /// Pixels per point. + pub pixels_per_point: f32, + + /// Full size of the screen, in pixels. + pub screen_size_px: [u32; 2], +} + +#[test] +fn test_viewport_rounding() { + for i in 0..=10_000 { + // Two adjacent viewports should never overlap: + let x = i as f32 / 97.0; + let left = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_max_x(x); + let right = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_min_x(x); + + for pixels_per_point in [0.618, 1.0, std::f32::consts::PI] { + let left = ViewportInPixels::from_points(&left, pixels_per_point, [100, 100]); + let right = ViewportInPixels::from_points(&right, pixels_per_point, [100, 100]); + assert_eq!(left.left_px + left.width_px, right.left_px); + } + } +} + +impl PaintCallbackInfo { + /// The viewport rectangle. This is what you would use in e.g. `glViewport`. + pub fn viewport_in_pixels(&self) -> ViewportInPixels { + ViewportInPixels::from_points(&self.viewport, self.pixels_per_point, self.screen_size_px) + } + + /// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`. + pub fn clip_rect_in_pixels(&self) -> ViewportInPixels { + ViewportInPixels::from_points(&self.clip_rect, self.pixels_per_point, self.screen_size_px) + } +} + +/// If you want to paint some 3D shapes inside an egui region, you can use this. +/// +/// This is advanced usage, and is backend specific. +#[derive(Clone)] +pub struct PaintCallback { + /// Where to paint. + /// + /// This will become [`PaintCallbackInfo::viewport`]. + pub rect: Rect, + + /// Paint something custom (e.g. 3D stuff). + /// + /// The concrete value of `callback` depends on the rendering backend used. For instance, the + /// `glow` backend requires that callback be an `egui_glow::CallbackFn` while the `wgpu` + /// backend requires a `egui_wgpu::Callback`. + /// + /// If the type cannot be downcast to the type expected by the current backend the callback + /// will not be drawn. + /// + /// The rendering backend is responsible for first setting the active viewport to + /// [`Self::rect`]. + /// + /// The rendering backend is also responsible for restoring any state, such as the bound shader + /// program, vertex array, etc. + /// + /// Shape has to be clone, therefore this has to be an `Arc` instead of a `Box`. + pub callback: Arc, +} + +impl std::fmt::Debug for PaintCallback { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CustomShape") + .field("rect", &self.rect) + .finish_non_exhaustive() + } +} + +impl std::cmp::PartialEq for PaintCallback { + fn eq(&self, other: &Self) -> bool { + self.rect.eq(&other.rect) && Arc::ptr_eq(&self.callback, &other.callback) + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: PaintCallback) -> Self { + Self::Callback(shape) + } +} diff --git a/crates/epaint/src/shapes/path_shape.rs b/crates/epaint/src/shapes/path_shape.rs new file mode 100644 index 00000000..8486055a --- /dev/null +++ b/crates/epaint/src/shapes/path_shape.rs @@ -0,0 +1,81 @@ +use crate::*; + +/// A path which can be stroked and/or filled (if closed). +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PathShape { + /// Filled paths should prefer clockwise order. + pub points: Vec, + + /// If true, connect the first and last of the points together. + /// This is required if `fill != TRANSPARENT`. + pub closed: bool, + + /// Fill is only supported for convex polygons. + pub fill: Color32, + + /// Color and thickness of the line. + pub stroke: PathStroke, + // TODO(emilk): Add texture support either by supplying uv for each point, + // or by some transform from points to uv (e.g. a callback or a linear transform matrix). +} + +impl PathShape { + /// A line through many points. + /// + /// Use [`Shape::line_segment`] instead if your line only connects two points. + #[inline] + pub fn line(points: Vec, stroke: impl Into) -> Self { + Self { + points, + closed: false, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// A line that closes back to the start point again. + #[inline] + pub fn closed_line(points: Vec, stroke: impl Into) -> Self { + Self { + points, + closed: true, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// A convex polygon with a fill and optional stroke. + /// + /// The most performant winding order is clockwise. + #[inline] + pub fn convex_polygon( + points: Vec, + fill: impl Into, + stroke: impl Into, + ) -> Self { + Self { + points, + closed: true, + fill: fill.into(), + stroke: stroke.into(), + } + } + + /// The visual bounding rectangle (includes stroke width) + #[inline] + pub fn visual_bounding_rect(&self) -> Rect { + if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { + Rect::NOTHING + } else { + Rect::from_points(&self.points).expand(self.stroke.width / 2.0) + } + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: PathShape) -> Self { + Self::Path(shape) + } +} diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs new file mode 100644 index 00000000..a36ae08c --- /dev/null +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -0,0 +1,125 @@ +use crate::*; + +/// How to paint a rectangle. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct RectShape { + pub rect: Rect, + + /// How rounded the corners are. Use `Rounding::ZERO` for no rounding. + pub rounding: Rounding, + + /// How to fill the rectangle. + pub fill: Color32, + + /// The thickness and color of the outline. + /// + /// The stroke extends _outside_ the edge of [`Self::rect`], + /// i.e. using [`crate::StrokeKind::Outside`]. + /// + /// This means the [`Self::visual_bounding_rect`] is `rect.size() + 2.0 * stroke.width`. + pub stroke: Stroke, + + /// If larger than zero, the edges of the rectangle + /// (for both fill and stroke) will be blurred. + /// + /// This can be used to produce shadows and glow effects. + /// + /// The blur is currently implemented using a simple linear blur in sRGBA gamma space. + pub blur_width: f32, + + /// If the rect should be filled with a texture, which one? + /// + /// The texture is multiplied with [`Self::fill`]. + pub fill_texture_id: TextureId, + + /// What UV coordinates to use for the texture? + /// + /// To display a texture, set [`Self::fill_texture_id`], + /// and set this to `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`. + /// + /// Use [`Rect::ZERO`] to turn off texturing. + pub uv: Rect, +} + +impl RectShape { + /// The stroke extends _outside_ the [`Rect`]. + #[inline] + pub fn new( + rect: Rect, + rounding: impl Into, + fill_color: impl Into, + stroke: impl Into, + ) -> Self { + Self { + rect, + rounding: rounding.into(), + fill: fill_color.into(), + stroke: stroke.into(), + blur_width: 0.0, + fill_texture_id: Default::default(), + uv: Rect::ZERO, + } + } + + #[inline] + pub fn filled( + rect: Rect, + rounding: impl Into, + fill_color: impl Into, + ) -> Self { + Self { + rect, + rounding: rounding.into(), + fill: fill_color.into(), + stroke: Default::default(), + blur_width: 0.0, + fill_texture_id: Default::default(), + uv: Rect::ZERO, + } + } + + /// The stroke extends _outside_ the [`Rect`]. + #[inline] + pub fn stroke(rect: Rect, rounding: impl Into, stroke: impl Into) -> Self { + Self { + rect, + rounding: rounding.into(), + fill: Default::default(), + stroke: stroke.into(), + blur_width: 0.0, + fill_texture_id: Default::default(), + uv: Rect::ZERO, + } + } + + /// If larger than zero, the edges of the rectangle + /// (for both fill and stroke) will be blurred. + /// + /// This can be used to produce shadows and glow effects. + /// + /// The blur is currently implemented using a simple linear blur in `sRGBA` gamma space. + #[inline] + pub fn with_blur_width(mut self, blur_width: f32) -> Self { + self.blur_width = blur_width; + self + } + + /// The visual bounding rectangle (includes stroke width) + #[inline] + pub fn visual_bounding_rect(&self) -> Rect { + if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { + Rect::NOTHING + } else { + let Stroke { width, .. } = self.stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke` + self.rect.expand(width + self.blur_width / 2.0) + } + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: RectShape) -> Self { + Self::Rect(shape) + } +} diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs new file mode 100644 index 00000000..945e5563 --- /dev/null +++ b/crates/epaint/src/shapes/shape.rs @@ -0,0 +1,540 @@ +//! The different shapes that can be painted. + +use std::sync::Arc; + +use emath::{pos2, Align2, Pos2, Rangef, Rect, TSTransform, Vec2}; + +use crate::{ + stroke::PathStroke, + text::{FontId, Fonts, Galley}, + Color32, Mesh, Rounding, Stroke, TextureId, +}; + +use super::{ + CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PathShape, QuadraticBezierShape, + RectShape, TextShape, +}; + +/// A paint primitive such as a circle or a piece of text. +/// Coordinates are all screen space points (not physical pixels). +/// +/// You should generally recreate your [`Shape`]s each frame, +/// but storing them should also be fine with one exception: +/// [`Shape::Text`] depends on the current `pixels_per_point` (dpi scale) +/// and so must be recreated every time `pixels_per_point` changes. +#[must_use = "Add a Shape to a Painter"] +#[derive(Clone, Debug, PartialEq)] +pub enum Shape { + /// Paint nothing. This can be useful as a placeholder. + Noop, + + /// Recursively nest more shapes - sometimes a convenience to be able to do. + /// For performance reasons it is better to avoid it. + Vec(Vec), + + /// Circle with optional outline and fill. + Circle(CircleShape), + + /// Ellipse with optional outline and fill. + Ellipse(EllipseShape), + + /// A line between two points. + LineSegment { points: [Pos2; 2], stroke: Stroke }, + + /// A series of lines between points. + /// The path can have a stroke and/or fill (if closed). + Path(PathShape), + + /// Rectangle with optional outline and fill. + Rect(RectShape), + + /// Text. + /// + /// This needs to be recreated if `pixels_per_point` (dpi scale) changes. + Text(TextShape), + + /// A general triangle mesh. + /// + /// Can be used to display images. + Mesh(Mesh), + + /// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). + QuadraticBezier(QuadraticBezierShape), + + /// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). + CubicBezier(CubicBezierShape), + + /// Backend-specific painting. + Callback(PaintCallback), +} + +#[test] +fn shape_impl_send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); +} + +impl From> for Shape { + #[inline(always)] + fn from(shapes: Vec) -> Self { + Self::Vec(shapes) + } +} + +impl From for Shape { + #[inline(always)] + fn from(mesh: Mesh) -> Self { + Self::Mesh(mesh) + } +} + +/// ## Constructors +impl Shape { + /// A line between two points. + /// More efficient than calling [`Self::line`]. + #[inline] + pub fn line_segment(points: [Pos2; 2], stroke: impl Into) -> Self { + Self::LineSegment { + points, + stroke: stroke.into(), + } + } + + /// A horizontal line. + pub fn hline(x: impl Into, y: f32, stroke: impl Into) -> Self { + let x = x.into(); + Self::LineSegment { + points: [pos2(x.min, y), pos2(x.max, y)], + stroke: stroke.into(), + } + } + + /// A vertical line. + pub fn vline(x: f32, y: impl Into, stroke: impl Into) -> Self { + let y = y.into(); + Self::LineSegment { + points: [pos2(x, y.min), pos2(x, y.max)], + stroke: stroke.into(), + } + } + + /// A line through many points. + /// + /// Use [`Self::line_segment`] instead if your line only connects two points. + #[inline] + pub fn line(points: Vec, stroke: impl Into) -> Self { + Self::Path(PathShape::line(points, stroke)) + } + + /// A line that closes back to the start point again. + #[inline] + pub fn closed_line(points: Vec, stroke: impl Into) -> Self { + Self::Path(PathShape::closed_line(points, stroke)) + } + + /// Turn a line into equally spaced dots. + pub fn dotted_line( + path: &[Pos2], + color: impl Into, + spacing: f32, + radius: f32, + ) -> Vec { + let mut shapes = Vec::new(); + points_from_line(path, spacing, radius, color.into(), &mut shapes); + shapes + } + + /// Turn a line into dashes. + pub fn dashed_line( + path: &[Pos2], + stroke: impl Into, + dash_length: f32, + gap_length: f32, + ) -> Vec { + let mut shapes = Vec::new(); + dashes_from_line( + path, + stroke.into(), + &[dash_length], + &[gap_length], + &mut shapes, + 0., + ); + shapes + } + + /// Turn a line into dashes with different dash/gap lengths and a start offset. + pub fn dashed_line_with_offset( + path: &[Pos2], + stroke: impl Into, + dash_lengths: &[f32], + gap_lengths: &[f32], + dash_offset: f32, + ) -> Vec { + let mut shapes = Vec::new(); + dashes_from_line( + path, + stroke.into(), + dash_lengths, + gap_lengths, + &mut shapes, + dash_offset, + ); + shapes + } + + /// Turn a line into dashes. If you need to create many dashed lines use this instead of + /// [`Self::dashed_line`]. + pub fn dashed_line_many( + points: &[Pos2], + stroke: impl Into, + dash_length: f32, + gap_length: f32, + shapes: &mut Vec, + ) { + dashes_from_line( + points, + stroke.into(), + &[dash_length], + &[gap_length], + shapes, + 0., + ); + } + + /// Turn a line into dashes with different dash/gap lengths and a start offset. If you need to + /// create many dashed lines use this instead of [`Self::dashed_line_with_offset`]. + pub fn dashed_line_many_with_offset( + points: &[Pos2], + stroke: impl Into, + dash_lengths: &[f32], + gap_lengths: &[f32], + dash_offset: f32, + shapes: &mut Vec, + ) { + dashes_from_line( + points, + stroke.into(), + dash_lengths, + gap_lengths, + shapes, + dash_offset, + ); + } + + /// A convex polygon with a fill and optional stroke. + /// + /// The most performant winding order is clockwise. + #[inline] + pub fn convex_polygon( + points: Vec, + fill: impl Into, + stroke: impl Into, + ) -> Self { + Self::Path(PathShape::convex_polygon(points, fill, stroke)) + } + + #[inline] + pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into) -> Self { + Self::Circle(CircleShape::filled(center, radius, fill_color)) + } + + #[inline] + pub fn circle_stroke(center: Pos2, radius: f32, stroke: impl Into) -> Self { + Self::Circle(CircleShape::stroke(center, radius, stroke)) + } + + #[inline] + pub fn ellipse_filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { + Self::Ellipse(EllipseShape::filled(center, radius, fill_color)) + } + + #[inline] + pub fn ellipse_stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { + Self::Ellipse(EllipseShape::stroke(center, radius, stroke)) + } + + #[inline] + pub fn rect_filled( + rect: Rect, + rounding: impl Into, + fill_color: impl Into, + ) -> Self { + Self::Rect(RectShape::filled(rect, rounding, fill_color)) + } + + /// The stroke extends _outside_ the [`Rect`]. + #[inline] + pub fn rect_stroke( + rect: Rect, + rounding: impl Into, + stroke: impl Into, + ) -> Self { + Self::Rect(RectShape::stroke(rect, rounding, stroke)) + } + + #[allow(clippy::needless_pass_by_value)] + pub fn text( + fonts: &Fonts, + pos: Pos2, + anchor: Align2, + text: impl ToString, + font_id: FontId, + color: Color32, + ) -> Self { + let galley = fonts.layout_no_wrap(text.to_string(), font_id, color); + let rect = anchor.anchor_size(pos, galley.size()); + Self::galley(rect.min, galley, color) + } + + /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color. + /// + /// Any non-placeholder color in the galley takes precedence over this fallback color. + #[inline] + pub fn galley(pos: Pos2, galley: Arc, fallback_color: Color32) -> Self { + TextShape::new(pos, galley, fallback_color).into() + } + + /// All text color in the [`Galley`] will be replaced with the given color. + #[inline] + pub fn galley_with_override_text_color( + pos: Pos2, + galley: Arc, + text_color: Color32, + ) -> Self { + TextShape::new(pos, galley, text_color) + .with_override_text_color(text_color) + .into() + } + + #[inline] + #[deprecated = "Use `Shape::galley` or `Shape::galley_with_override_text_color` instead"] + pub fn galley_with_color(pos: Pos2, galley: Arc, text_color: Color32) -> Self { + Self::galley_with_override_text_color(pos, galley, text_color) + } + + #[inline] + pub fn mesh(mesh: Mesh) -> Self { + debug_assert!(mesh.is_valid()); + Self::Mesh(mesh) + } + + /// An image at the given position. + /// + /// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))` + /// unless you want to crop or flip the image. + /// + /// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image. + pub fn image(texture_id: TextureId, rect: Rect, uv: Rect, tint: Color32) -> Self { + let mut mesh = Mesh::with_texture(texture_id); + mesh.add_rect_with_uv(rect, uv, tint); + Self::mesh(mesh) + } + + /// The visual bounding rectangle (includes stroke widths) + pub fn visual_bounding_rect(&self) -> Rect { + match self { + Self::Noop => Rect::NOTHING, + Self::Vec(shapes) => { + let mut rect = Rect::NOTHING; + for shape in shapes { + rect = rect.union(shape.visual_bounding_rect()); + } + rect + } + Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(), + Self::Ellipse(ellipse_shape) => ellipse_shape.visual_bounding_rect(), + Self::LineSegment { points, stroke } => { + if stroke.is_empty() { + Rect::NOTHING + } else { + Rect::from_two_pos(points[0], points[1]).expand(stroke.width / 2.0) + } + } + Self::Path(path_shape) => path_shape.visual_bounding_rect(), + Self::Rect(rect_shape) => rect_shape.visual_bounding_rect(), + Self::Text(text_shape) => text_shape.visual_bounding_rect(), + Self::Mesh(mesh) => mesh.calc_bounds(), + Self::QuadraticBezier(bezier) => bezier.visual_bounding_rect(), + Self::CubicBezier(bezier) => bezier.visual_bounding_rect(), + Self::Callback(custom) => custom.rect, + } + } +} + +/// ## Inspection and transforms +impl Shape { + #[inline(always)] + pub fn texture_id(&self) -> crate::TextureId { + if let Self::Mesh(mesh) = self { + mesh.texture_id + } else if let Self::Rect(rect_shape) = self { + rect_shape.fill_texture_id + } else { + crate::TextureId::default() + } + } + + /// Scale the shape by `factor`, in-place. + /// + /// A wrapper around [`Self::transform`]. + #[inline(always)] + pub fn scale(&mut self, factor: f32) { + self.transform(TSTransform::from_scaling(factor)); + } + + /// Move the shape by `delta`, in-place. + /// + /// A wrapper around [`Self::transform`]. + #[inline(always)] + pub fn translate(&mut self, delta: Vec2) { + self.transform(TSTransform::from_translation(delta)); + } + + /// Move the shape by this many points, in-place. + /// + /// If using a [`PaintCallback`], note that only the rect is scaled as opposed + /// to other shapes where the stroke is also scaled. + pub fn transform(&mut self, transform: TSTransform) { + match self { + Self::Noop => {} + Self::Vec(shapes) => { + for shape in shapes { + shape.transform(transform); + } + } + Self::Circle(circle_shape) => { + circle_shape.center = transform * circle_shape.center; + circle_shape.radius *= transform.scaling; + circle_shape.stroke.width *= transform.scaling; + } + Self::Ellipse(ellipse_shape) => { + ellipse_shape.center = transform * ellipse_shape.center; + ellipse_shape.radius *= transform.scaling; + ellipse_shape.stroke.width *= transform.scaling; + } + Self::LineSegment { points, stroke } => { + for p in points { + *p = transform * *p; + } + stroke.width *= transform.scaling; + } + Self::Path(path_shape) => { + for p in &mut path_shape.points { + *p = transform * *p; + } + path_shape.stroke.width *= transform.scaling; + } + Self::Rect(rect_shape) => { + rect_shape.rect = transform * rect_shape.rect; + rect_shape.stroke.width *= transform.scaling; + rect_shape.rounding *= transform.scaling; + } + Self::Text(text_shape) => { + text_shape.pos = transform * text_shape.pos; + + // Scale text: + let galley = Arc::make_mut(&mut text_shape.galley); + for row in &mut galley.rows { + row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; + for v in &mut row.visuals.mesh.vertices { + v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); + } + } + + galley.mesh_bounds = transform.scaling * galley.mesh_bounds; + galley.rect = transform.scaling * galley.rect; + } + Self::Mesh(mesh) => { + mesh.transform(transform); + } + Self::QuadraticBezier(bezier_shape) => { + bezier_shape.points[0] = transform * bezier_shape.points[0]; + bezier_shape.points[1] = transform * bezier_shape.points[1]; + bezier_shape.points[2] = transform * bezier_shape.points[2]; + bezier_shape.stroke.width *= transform.scaling; + } + Self::CubicBezier(cubic_curve) => { + for p in &mut cubic_curve.points { + *p = transform * *p; + } + cubic_curve.stroke.width *= transform.scaling; + } + Self::Callback(shape) => { + shape.rect = transform * shape.rect; + } + } + } +} + +// ---------------------------------------------------------------------------- + +/// Creates equally spaced filled circles from a line. +fn points_from_line( + path: &[Pos2], + spacing: f32, + radius: f32, + color: Color32, + shapes: &mut Vec, +) { + let mut position_on_segment = 0.0; + path.windows(2).for_each(|window| { + let (start, end) = (window[0], window[1]); + let vector = end - start; + let segment_length = vector.length(); + while position_on_segment < segment_length { + let new_point = start + vector * (position_on_segment / segment_length); + shapes.push(Shape::circle_filled(new_point, radius, color)); + position_on_segment += spacing; + } + position_on_segment -= segment_length; + }); +} + +/// Creates dashes from a line. +fn dashes_from_line( + path: &[Pos2], + stroke: Stroke, + dash_lengths: &[f32], + gap_lengths: &[f32], + shapes: &mut Vec, + dash_offset: f32, +) { + assert_eq!(dash_lengths.len(), gap_lengths.len()); + let mut position_on_segment = dash_offset; + let mut drawing_dash = false; + let mut step = 0; + let steps = dash_lengths.len(); + path.windows(2).for_each(|window| { + let (start, end) = (window[0], window[1]); + let vector = end - start; + let segment_length = vector.length(); + + let mut start_point = start; + while position_on_segment < segment_length { + let new_point = start + vector * (position_on_segment / segment_length); + if drawing_dash { + // This is the end point. + shapes.push(Shape::line_segment([start_point, new_point], stroke)); + position_on_segment += gap_lengths[step]; + // Increment step counter + step += 1; + if step >= steps { + step = 0; + } + } else { + // Start a new dash. + start_point = new_point; + position_on_segment += dash_lengths[step]; + } + drawing_dash = !drawing_dash; + } + + // If the segment ends and the dash is not finished, add the segment's end point. + if drawing_dash { + shapes.push(Shape::line_segment([start_point, end], stroke)); + } + + position_on_segment -= segment_length; + }); +} diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs new file mode 100644 index 00000000..30287187 --- /dev/null +++ b/crates/epaint/src/shapes/text_shape.rs @@ -0,0 +1,97 @@ +use std::sync::Arc; + +use crate::*; + +/// How to paint some text on screen. +/// +/// This needs to be recreated if `pixels_per_point` (dpi scale) changes. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct TextShape { + /// Top left corner of the first character. + pub pos: Pos2, + + /// The laid out text, from [`Fonts::layout_job`]. + pub galley: Arc, + + /// Add this underline to the whole text. + /// You can also set an underline when creating the galley. + pub underline: Stroke, + + /// Any [`Color32::PLACEHOLDER`] in the galley will be replaced by the given color. + /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. + pub fallback_color: Color32, + + /// If set, the text color in the galley will be ignored and replaced + /// with the given color. + /// + /// This only affects the glyphs and will NOT replace background color nor strikethrough/underline color. + pub override_text_color: Option, + + /// If set, the text will be rendered with the given opacity in gamma space + /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. + pub opacity_factor: f32, + + /// Rotate text by this many radians clockwise. + /// The pivot is `pos` (the upper left corner of the text). + pub angle: f32, +} + +impl TextShape { + /// The given fallback color will be used for any uncolored part of the galley (using [`Color32::PLACEHOLDER`]). + /// + /// Any non-placeholder color in the galley takes precedence over this fallback color. + #[inline] + pub fn new(pos: Pos2, galley: Arc, fallback_color: Color32) -> Self { + Self { + pos, + galley, + underline: Stroke::NONE, + fallback_color, + override_text_color: None, + opacity_factor: 1.0, + angle: 0.0, + } + } + + /// The visual bounding rectangle + #[inline] + pub fn visual_bounding_rect(&self) -> Rect { + self.galley.mesh_bounds.translate(self.pos.to_vec2()) + } + + #[inline] + pub fn with_underline(mut self, underline: Stroke) -> Self { + self.underline = underline; + self + } + + /// Use the given color for the text, regardless of what color is already in the galley. + #[inline] + pub fn with_override_text_color(mut self, override_text_color: Color32) -> Self { + self.override_text_color = Some(override_text_color); + self + } + + /// Rotate text by this many radians clockwise. + /// The pivot is `pos` (the upper left corner of the text). + #[inline] + pub fn with_angle(mut self, angle: f32) -> Self { + self.angle = angle; + self + } + + /// Render text with this opacity in gamma space + #[inline] + pub fn with_opacity_factor(mut self, opacity_factor: f32) -> Self { + self.opacity_factor = opacity_factor; + self + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: TextShape) -> Self { + Self::Text(shape) + } +} diff --git a/crates/epaint/src/viewport.rs b/crates/epaint/src/viewport.rs new file mode 100644 index 00000000..01011b5c --- /dev/null +++ b/crates/epaint/src/viewport.rs @@ -0,0 +1,54 @@ +use crate::Rect; + +/// Size of the viewport in whole, physical pixels. +pub struct ViewportInPixels { + /// Physical pixel offset for left side of the viewport. + pub left_px: i32, + + /// Physical pixel offset for top side of the viewport. + pub top_px: i32, + + /// Physical pixel offset for bottom side of the viewport. + /// + /// This is what `glViewport`, `glScissor` etc expects for the y axis. + pub from_bottom_px: i32, + + /// Viewport width in physical pixels. + pub width_px: i32, + + /// Viewport height in physical pixels. + pub height_px: i32, +} + +impl ViewportInPixels { + /// Convert from ui points. + pub fn from_points(rect: &Rect, pixels_per_point: f32, screen_size_px: [u32; 2]) -> Self { + // Fractional pixel values for viewports are generally valid, but may cause sampling issues + // and rounding errors might cause us to get out of bounds. + + // Round: + let left_px = (pixels_per_point * rect.min.x).round() as i32; // inclusive + let top_px = (pixels_per_point * rect.min.y).round() as i32; // inclusive + let right_px = (pixels_per_point * rect.max.x).round() as i32; // exclusive + let bottom_px = (pixels_per_point * rect.max.y).round() as i32; // exclusive + + // Clamp to screen: + let screen_width = screen_size_px[0] as i32; + let screen_height = screen_size_px[1] as i32; + let left_px = left_px.clamp(0, screen_width); + let right_px = right_px.clamp(left_px, screen_width); + let top_px = top_px.clamp(0, screen_height); + let bottom_px = bottom_px.clamp(top_px, screen_height); + + let width_px = right_px - left_px; + let height_px = bottom_px - top_px; + + Self { + left_px, + top_px, + from_bottom_px: screen_height - height_px - top_px, + width_px, + height_px, + } + } +}