Added ability to define colors at UV coordinates along a path (#4353)

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to add commits to your PR.
* Remember to run `cargo fmt` and `cargo cranky`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

I had to make a couple types not Copy because closures, but it should'nt
be a massive deal.

I tried my best to make the API change as non breaking as possible.
Anywhere a PathStroke is used, you can just use a normal Stroke instead.
As mentioned above, the bezier paths couldn't be copy anymore, but IMO
that's a minor caveat.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Joe Sorensen 2024-04-22 10:35:09 -06:00 committed by GitHub
parent ff8cfc2aa0
commit 2ce82cce21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 550 additions and 104 deletions

View File

@ -30,7 +30,6 @@ extra_debug_asserts = []
## Always enable additional checks. ## Always enable additional checks.
extra_asserts = [] extra_asserts = []
[dependencies] [dependencies]
#! ### Optional dependencies #! ### Optional dependencies

View File

@ -7,7 +7,7 @@ use crate::{
}; };
use epaint::{ use epaint::{
text::{Fonts, Galley, LayoutJob}, text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke, CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke,
}; };
/// Helper to paint shapes and text to a specific region on a specific layer. /// Helper to paint shapes and text to a specific region on a specific layer.
@ -280,7 +280,7 @@ impl Painter {
/// # Paint different primitives /// # Paint different primitives
impl Painter { impl Painter {
/// Paints a line from the first point to the second. /// Paints a line from the first point to the second.
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) -> ShapeIdx { pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<PathStroke>) -> ShapeIdx {
self.add(Shape::LineSegment { self.add(Shape::LineSegment {
points, points,
stroke: stroke.into(), stroke: stroke.into(),
@ -288,13 +288,13 @@ impl Painter {
} }
/// Paints a horizontal line. /// Paints a horizontal line.
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx { pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> ShapeIdx {
self.add(Shape::hline(x, y, stroke)) self.add(Shape::hline(x, y, stroke.into()))
} }
/// Paints a vertical line. /// Paints a vertical line.
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx { pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> ShapeIdx {
self.add(Shape::vline(x, y, stroke)) self.add(Shape::vline(x, y, stroke.into()))
} }
pub fn circle( pub fn circle(
@ -513,7 +513,7 @@ impl Painter {
} }
fn tint_shape_towards(shape: &mut Shape, target: Color32) { fn tint_shape_towards(shape: &mut Shape, target: Color32) {
epaint::shape_transform::adjust_colors(shape, &|color| { epaint::shape_transform::adjust_colors(shape, move |color| {
if *color != Color32::PLACEHOLDER { if *color != Color32::PLACEHOLDER {
*color = crate::ecolor::tint_color_towards(*color, target); *color = crate::ecolor::tint_color_towards(*color, target);
} }
@ -521,7 +521,7 @@ fn tint_shape_towards(shape: &mut Shape, target: Color32) {
} }
fn multiply_opacity(shape: &mut Shape, opacity: f32) { fn multiply_opacity(shape: &mut Shape, opacity: f32) {
epaint::shape_transform::adjust_colors(shape, &|color| { epaint::shape_transform::adjust_colors(shape, move |color| {
if *color != Color32::PLACEHOLDER { if *color != Color32::PLACEHOLDER {
*color = color.gamma_multiply(opacity); *color = color.gamma_multiply(opacity);
} }

View File

@ -38,7 +38,7 @@ syntect = ["egui_extras/syntect"]
[dependencies] [dependencies]
egui = { workspace = true, default-features = false } egui = { workspace = true, default-features = false, features = ["color-hex"] }
egui_extras = { workspace = true, features = ["default"] } egui_extras = { workspace = true, features = ["default"] }
egui_plot = { workspace = true, features = ["default"] } egui_plot = { workspace = true, features = ["default"] }

View File

@ -1,9 +1,11 @@
use egui::{containers::*, *}; use egui::{containers::*, epaint::PathStroke, *};
#[derive(Default)] #[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct DancingStrings {} pub struct DancingStrings {
colors: bool,
}
impl super::Demo for DancingStrings { impl super::Demo for DancingStrings {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
@ -28,6 +30,9 @@ impl super::View for DancingStrings {
Color32::from_black_alpha(240) Color32::from_black_alpha(240)
}; };
ui.checkbox(&mut self.colors, "Colored")
.on_hover_text("Demonstrates how a path can have varying color across its length.");
Frame::canvas(ui.style()).show(ui, |ui| { Frame::canvas(ui.style()).show(ui, |ui| {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
let time = ui.input(|i| i.time); let time = ui.input(|i| i.time);
@ -55,7 +60,24 @@ impl super::View for DancingStrings {
.collect(); .collect();
let thickness = 10.0 / mode as f32; let thickness = 10.0 / mode as f32;
shapes.push(epaint::Shape::line(points, Stroke::new(thickness, color))); shapes.push(epaint::Shape::line(
points,
if self.colors {
PathStroke::new_uv(thickness, move |rect, p| {
let t = remap(p.x, rect.x_range(), -1.0..=1.0).abs();
let center_color = hex_color!("#5BCEFA");
let outer_color = hex_color!("#F5A9B8");
Color32::from_rgb(
lerp(center_color.r() as f32..=outer_color.r() as f32, t) as u8,
lerp(center_color.g() as f32..=outer_color.g() as f32, t) as u8,
lerp(center_color.b() as f32..=outer_color.b() as f32, t) as u8,
)
})
} else {
PathStroke::new(thickness, color)
},
));
} }
ui.painter().extend(shapes); ui.painter().extend(shapes);

View File

@ -1,6 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use epaint::*; use epaint::{tessellator::Path, *};
fn single_dashed_lines(c: &mut Criterion) { fn single_dashed_lines(c: &mut Criterion) {
c.bench_function("single_dashed_lines", move |b| { c.bench_function("single_dashed_lines", move |b| {
@ -72,10 +72,166 @@ fn tessellate_circles(c: &mut Criterion) {
}); });
} }
fn thick_line_solid(c: &mut Criterion) {
c.bench_function("thick_solid_line", move |b| {
let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)];
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh);
black_box(mesh);
});
});
}
fn thick_large_line_solid(c: &mut Criterion) {
c.bench_function("thick_large_solid_line", move |b| {
let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::<Vec<_>>();
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh);
black_box(mesh);
});
});
}
fn thin_line_solid(c: &mut Criterion) {
c.bench_function("thin_solid_line", move |b| {
let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)];
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(1.5, &Stroke::new(0.5, Color32::RED).into(), &mut mesh);
black_box(mesh);
});
});
}
fn thin_large_line_solid(c: &mut Criterion) {
c.bench_function("thin_large_solid_line", move |b| {
let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::<Vec<_>>();
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(1.5, &Stroke::new(0.5, Color32::RED).into(), &mut mesh);
black_box(mesh);
});
});
}
fn thick_line_uv(c: &mut Criterion) {
c.bench_function("thick_uv_line", move |b| {
let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)];
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(
1.5,
&PathStroke::new_uv(2.0, |_, p| {
black_box(p * 2.0);
Color32::RED
}),
&mut mesh,
);
black_box(mesh);
});
});
}
fn thick_large_line_uv(c: &mut Criterion) {
c.bench_function("thick_large_uv_line", move |b| {
let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::<Vec<_>>();
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(
1.5,
&PathStroke::new_uv(2.0, |_, p| {
black_box(p * 2.0);
Color32::RED
}),
&mut mesh,
);
black_box(mesh);
});
});
}
fn thin_line_uv(c: &mut Criterion) {
c.bench_function("thin_uv_line", move |b| {
let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)];
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(
1.5,
&PathStroke::new_uv(2.0, |_, p| {
black_box(p * 2.0);
Color32::RED
}),
&mut mesh,
);
black_box(mesh);
});
});
}
fn thin_large_line_uv(c: &mut Criterion) {
c.bench_function("thin_large_uv_line", move |b| {
let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::<Vec<_>>();
let mut path = Path::default();
path.add_open_points(&line);
b.iter(|| {
let mut mesh = Mesh::default();
path.stroke_closed(
1.5,
&PathStroke::new_uv(2.0, |_, p| {
black_box(p * 2.0);
Color32::RED
}),
&mut mesh,
);
black_box(mesh);
});
});
}
criterion_group!( criterion_group!(
benches, benches,
single_dashed_lines, single_dashed_lines,
many_dashed_lines, many_dashed_lines,
tessellate_circles tessellate_circles,
thick_line_solid,
thick_large_line_solid,
thin_line_solid,
thin_large_line_solid,
thick_line_uv,
thick_large_line_uv,
thin_line_uv,
thin_large_line_uv
); );
criterion_main!(benches); criterion_main!(benches);

View File

@ -3,7 +3,7 @@
use std::ops::Range; use std::ops::Range;
use crate::{shape::Shape, Color32, PathShape, Stroke}; use crate::{shape::Shape, Color32, PathShape, PathStroke};
use emath::*; use emath::*;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -11,7 +11,7 @@ use emath::*;
/// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). /// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
/// ///
/// See also [`QuadraticBezierShape`]. /// See also [`QuadraticBezierShape`].
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct CubicBezierShape { pub struct CubicBezierShape {
/// The first point is the starting point and the last one is the ending point of the curve. /// The first point is the starting point and the last one is the ending point of the curve.
@ -20,7 +20,7 @@ pub struct CubicBezierShape {
pub closed: bool, pub closed: bool,
pub fill: Color32, pub fill: Color32,
pub stroke: Stroke, pub stroke: PathStroke,
} }
impl CubicBezierShape { impl CubicBezierShape {
@ -32,7 +32,7 @@ impl CubicBezierShape {
points: [Pos2; 4], points: [Pos2; 4],
closed: bool, closed: bool,
fill: Color32, fill: Color32,
stroke: impl Into<Stroke>, stroke: impl Into<PathStroke>,
) -> Self { ) -> Self {
Self { Self {
points, points,
@ -52,7 +52,7 @@ impl CubicBezierShape {
points, points,
closed: self.closed, closed: self.closed,
fill: self.fill, fill: self.fill,
stroke: self.stroke, stroke: self.stroke.clone(),
} }
} }
@ -69,7 +69,7 @@ impl CubicBezierShape {
points, points,
closed: self.closed, closed: self.closed,
fill: self.fill, fill: self.fill,
stroke: self.stroke, stroke: self.stroke.clone(),
}; };
pathshapes.push(pathshape); pathshapes.push(pathshape);
} }
@ -156,7 +156,7 @@ impl CubicBezierShape {
points: [d_from, d_ctrl, d_to], points: [d_from, d_ctrl, d_to],
closed: self.closed, closed: self.closed,
fill: self.fill, fill: self.fill,
stroke: self.stroke, stroke: self.stroke.clone(),
}; };
let delta_t = t_range.end - t_range.start; let delta_t = t_range.end - t_range.start;
let q_start = q.sample(t_range.start); let q_start = q.sample(t_range.start);
@ -168,7 +168,7 @@ impl CubicBezierShape {
points: [from, ctrl1, ctrl2, to], points: [from, ctrl1, ctrl2, to],
closed: self.closed, closed: self.closed,
fill: self.fill, fill: self.fill,
stroke: self.stroke, stroke: self.stroke.clone(),
} }
} }
@ -375,7 +375,7 @@ impl From<CubicBezierShape> for Shape {
/// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). /// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
/// ///
/// See also [`CubicBezierShape`]. /// See also [`CubicBezierShape`].
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct QuadraticBezierShape { pub struct QuadraticBezierShape {
/// The first point is the starting point and the last one is the ending point of the curve. /// The first point is the starting point and the last one is the ending point of the curve.
@ -384,7 +384,7 @@ pub struct QuadraticBezierShape {
pub closed: bool, pub closed: bool,
pub fill: Color32, pub fill: Color32,
pub stroke: Stroke, pub stroke: PathStroke,
} }
impl QuadraticBezierShape { impl QuadraticBezierShape {
@ -397,7 +397,7 @@ impl QuadraticBezierShape {
points: [Pos2; 3], points: [Pos2; 3],
closed: bool, closed: bool,
fill: Color32, fill: Color32,
stroke: impl Into<Stroke>, stroke: impl Into<PathStroke>,
) -> Self { ) -> Self {
Self { Self {
points, points,
@ -417,7 +417,7 @@ impl QuadraticBezierShape {
points, points,
closed: self.closed, closed: self.closed,
fill: self.fill, fill: self.fill,
stroke: self.stroke, stroke: self.stroke.clone(),
} }
} }
@ -429,7 +429,7 @@ impl QuadraticBezierShape {
points, points,
closed: self.closed, closed: self.closed,
fill: self.fill, fill: self.fill,
stroke: self.stroke, stroke: self.stroke.clone(),
} }
} }
@ -688,7 +688,7 @@ fn single_curve_approximation(curve: &CubicBezierShape) -> QuadraticBezierShape
points: [curve.points[0], c, curve.points[3]], points: [curve.points[0], c, curve.points[3]],
closed: curve.closed, closed: curve.closed,
fill: curve.fill, fill: curve.fill,
stroke: curve.stroke, stroke: curve.stroke.clone(),
} }
} }

View File

@ -0,0 +1,48 @@
use std::{fmt::Debug, sync::Arc};
use ecolor::Color32;
use emath::{Pos2, Rect};
/// How paths will be colored.
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ColorMode {
/// The entire path is one solid color, this is the default.
Solid(Color32),
/// Provide a callback which takes in the path's bounding box and a position and converts it to a color.
/// When used with a path, the bounding box will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`)
///
/// **This cannot be serialized**
#[cfg_attr(feature = "serde", serde(skip))]
UV(Arc<dyn Fn(Rect, Pos2) -> Color32 + Send + Sync>),
}
impl Default for ColorMode {
fn default() -> Self {
Self::Solid(Color32::TRANSPARENT)
}
}
impl Debug for ColorMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(),
Self::UV(_arg0) => f.debug_tuple("UV").field(&"<closure>").finish(),
}
}
}
impl PartialEq for ColorMode {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Solid(l0), Self::Solid(r0)) => l0 == r0,
(Self::UV(_l0), Self::UV(_r0)) => false,
_ => false,
}
}
}
impl ColorMode {
pub const TRANSPARENT: Self = Self::Solid(Color32::TRANSPARENT);
}

View File

@ -26,6 +26,7 @@
#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] #![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))]
mod bezier; mod bezier;
pub mod color;
pub mod image; pub mod image;
mod margin; mod margin;
mod mesh; mod mesh;
@ -44,6 +45,7 @@ pub mod util;
pub use self::{ pub use self::{
bezier::{CubicBezierShape, QuadraticBezierShape}, bezier::{CubicBezierShape, QuadraticBezierShape},
color::ColorMode,
image::{ColorImage, FontImage, ImageData, ImageDelta}, image::{ColorImage, FontImage, ImageData, ImageDelta},
margin::Margin, margin::Margin,
mesh::{Mesh, Mesh16, Vertex}, mesh::{Mesh, Mesh16, Vertex},
@ -53,7 +55,7 @@ pub use self::{
Rounding, Shape, TextShape, Rounding, Shape, TextShape,
}, },
stats::PaintStats, stats::PaintStats,
stroke::Stroke, stroke::{PathStroke, Stroke},
tessellator::{TessellationOptions, Tessellator}, tessellator::{TessellationOptions, Tessellator},
text::{FontFamily, FontId, Fonts, Galley}, text::{FontFamily, FontId, Fonts, Galley},
texture_atlas::TextureAtlas, texture_atlas::TextureAtlas,

View File

@ -3,6 +3,7 @@
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use crate::{ use crate::{
stroke::PathStroke,
text::{FontId, Fonts, Galley}, text::{FontId, Fonts, Galley},
Color32, Mesh, Stroke, TextureId, Color32, Mesh, Stroke, TextureId,
}; };
@ -34,7 +35,10 @@ pub enum Shape {
Ellipse(EllipseShape), Ellipse(EllipseShape),
/// A line between two points. /// A line between two points.
LineSegment { points: [Pos2; 2], stroke: Stroke }, LineSegment {
points: [Pos2; 2],
stroke: PathStroke,
},
/// A series of lines between points. /// A series of lines between points.
/// The path can have a stroke and/or fill (if closed). /// The path can have a stroke and/or fill (if closed).
@ -88,7 +92,7 @@ impl Shape {
/// A line between two points. /// A line between two points.
/// More efficient than calling [`Self::line`]. /// More efficient than calling [`Self::line`].
#[inline] #[inline]
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<Stroke>) -> Self { pub fn line_segment(points: [Pos2; 2], stroke: impl Into<PathStroke>) -> Self {
Self::LineSegment { Self::LineSegment {
points, points,
stroke: stroke.into(), stroke: stroke.into(),
@ -96,7 +100,7 @@ impl Shape {
} }
/// A horizontal line. /// A horizontal line.
pub fn hline(x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> Self { pub fn hline(x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> Self {
let x = x.into(); let x = x.into();
Self::LineSegment { Self::LineSegment {
points: [pos2(x.min, y), pos2(x.max, y)], points: [pos2(x.min, y), pos2(x.max, y)],
@ -105,7 +109,7 @@ impl Shape {
} }
/// A vertical line. /// A vertical line.
pub fn vline(x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> Self { pub fn vline(x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> Self {
let y = y.into(); let y = y.into();
Self::LineSegment { Self::LineSegment {
points: [pos2(x, y.min), pos2(x, y.max)], points: [pos2(x, y.min), pos2(x, y.max)],
@ -117,13 +121,13 @@ impl Shape {
/// ///
/// Use [`Self::line_segment`] instead if your line only connects two points. /// Use [`Self::line_segment`] instead if your line only connects two points.
#[inline] #[inline]
pub fn line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self { pub fn line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self::Path(PathShape::line(points, stroke)) Self::Path(PathShape::line(points, stroke))
} }
/// A line that closes back to the start point again. /// A line that closes back to the start point again.
#[inline] #[inline]
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self { pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self::Path(PathShape::closed_line(points, stroke)) Self::Path(PathShape::closed_line(points, stroke))
} }
@ -224,7 +228,7 @@ impl Shape {
pub fn convex_polygon( pub fn convex_polygon(
points: Vec<Pos2>, points: Vec<Pos2>,
fill: impl Into<Color32>, fill: impl Into<Color32>,
stroke: impl Into<Stroke>, stroke: impl Into<PathStroke>,
) -> Self { ) -> Self {
Self::Path(PathShape::convex_polygon(points, fill, stroke)) Self::Path(PathShape::convex_polygon(points, fill, stroke))
} }
@ -586,7 +590,7 @@ pub struct PathShape {
pub fill: Color32, pub fill: Color32,
/// Color and thickness of the line. /// Color and thickness of the line.
pub stroke: Stroke, pub stroke: PathStroke,
// TODO(emilk): Add texture support either by supplying uv for each point, // 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). // or by some transform from points to uv (e.g. a callback or a linear transform matrix).
} }
@ -596,7 +600,7 @@ impl PathShape {
/// ///
/// Use [`Shape::line_segment`] instead if your line only connects two points. /// Use [`Shape::line_segment`] instead if your line only connects two points.
#[inline] #[inline]
pub fn line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self { pub fn line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self { Self {
points, points,
closed: false, closed: false,
@ -607,7 +611,7 @@ impl PathShape {
/// A line that closes back to the start point again. /// A line that closes back to the start point again.
#[inline] #[inline]
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self { pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self { Self {
points, points,
closed: true, closed: true,
@ -623,7 +627,7 @@ impl PathShape {
pub fn convex_polygon( pub fn convex_polygon(
points: Vec<Pos2>, points: Vec<Pos2>,
fill: impl Into<Color32>, fill: impl Into<Color32>,
stroke: impl Into<Stroke>, stroke: impl Into<PathStroke>,
) -> Self { ) -> Self {
Self { Self {
points, points,

View File

@ -1,7 +1,12 @@
use std::sync::Arc;
use crate::*; use crate::*;
/// Remember to handle [`Color32::PLACEHOLDER`] specially! /// Remember to handle [`Color32::PLACEHOLDER`] specially!
pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { pub fn adjust_colors(
shape: &mut Shape,
adjust_color: impl Fn(&mut Color32) + Send + Sync + Copy + 'static,
) {
#![allow(clippy::match_same_arms)] #![allow(clippy::match_same_arms)]
match shape { match shape {
Shape::Noop => {} Shape::Noop => {}
@ -10,8 +15,48 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
adjust_colors(shape, adjust_color); adjust_colors(shape, adjust_color);
} }
} }
Shape::LineSegment { stroke, points: _ } => { Shape::LineSegment { stroke, points: _ } => match &stroke.color {
adjust_color(&mut stroke.color); color::ColorMode::Solid(mut col) => adjust_color(&mut col),
color::ColorMode::UV(callback) => {
let callback = callback.clone();
stroke.color = color::ColorMode::UV(Arc::new(Box::new(move |rect, pos| {
let mut col = callback(rect, pos);
adjust_color(&mut col);
col
})));
}
},
Shape::Path(PathShape {
points: _,
closed: _,
fill,
stroke,
})
| Shape::QuadraticBezier(QuadraticBezierShape {
points: _,
closed: _,
fill,
stroke,
})
| Shape::CubicBezier(CubicBezierShape {
points: _,
closed: _,
fill,
stroke,
}) => {
adjust_color(fill);
match &stroke.color {
color::ColorMode::Solid(mut col) => adjust_color(&mut col),
color::ColorMode::UV(callback) => {
let callback = callback.clone();
stroke.color = color::ColorMode::UV(Arc::new(Box::new(move |rect, pos| {
let mut col = callback(rect, pos);
adjust_color(&mut col);
col
})));
}
}
} }
Shape::Circle(CircleShape { Shape::Circle(CircleShape {
@ -26,12 +71,6 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
fill, fill,
stroke, stroke,
}) })
| Shape::Path(PathShape {
points: _,
closed: _,
fill,
stroke,
})
| Shape::Rect(RectShape { | Shape::Rect(RectShape {
rect: _, rect: _,
rounding: _, rounding: _,
@ -40,18 +79,6 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
blur_width: _, blur_width: _,
fill_texture_id: _, fill_texture_id: _,
uv: _, uv: _,
})
| Shape::QuadraticBezier(QuadraticBezierShape {
points: _,
closed: _,
fill,
stroke,
})
| Shape::CubicBezier(CubicBezierShape {
points: _,
closed: _,
fill,
stroke,
}) => { }) => {
adjust_color(fill); adjust_color(fill);
adjust_color(&mut stroke.color); adjust_color(&mut stroke.color);

View File

@ -1,5 +1,7 @@
#![allow(clippy::derived_hash_with_manual_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine #![allow(clippy::derived_hash_with_manual_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
use std::{fmt::Debug, sync::Arc};
use super::*; use super::*;
/// Describes the width and color of a line. /// Describes the width and color of a line.
@ -52,3 +54,68 @@ impl std::hash::Hash for Stroke {
color.hash(state); color.hash(state);
} }
} }
/// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`]
///
/// The default stroke is the same as [`Stroke::NONE`].
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PathStroke {
pub width: f32,
pub color: ColorMode,
}
impl PathStroke {
/// Same as [`PathStroke::default`].
pub const NONE: Self = Self {
width: 0.0,
color: ColorMode::TRANSPARENT,
};
#[inline]
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
Self {
width: width.into(),
color: ColorMode::Solid(color.into()),
}
}
/// Create a new `PathStroke` with a UV function
///
/// The bounding box passed to the callback will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`)
#[inline]
pub fn new_uv(
width: impl Into<f32>,
callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static,
) -> Self {
Self {
width: width.into(),
color: ColorMode::UV(Arc::new(callback)),
}
}
/// True if width is zero or color is solid and transparent
#[inline]
pub fn is_empty(&self) -> bool {
self.width <= 0.0 || self.color == ColorMode::TRANSPARENT
}
}
impl<Color> From<(f32, Color)> for PathStroke
where
Color: Into<Color32>,
{
#[inline(always)]
fn from((width, color): (f32, Color)) -> Self {
Self::new(width, color)
}
}
impl From<Stroke> for PathStroke {
fn from(value: Stroke) -> Self {
Self {
width: value.width,
color: ColorMode::Solid(value.color),
}
}
}

View File

@ -9,6 +9,9 @@ use crate::texture_atlas::PreparedDisc;
use crate::*; use crate::*;
use emath::*; use emath::*;
use self::color::ColorMode;
use self::stroke::PathStroke;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[allow(clippy::approx_constant)] #[allow(clippy::approx_constant)]
@ -471,16 +474,22 @@ impl Path {
} }
/// Open-ended. /// Open-ended.
pub fn stroke_open(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { pub fn stroke_open(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) {
stroke_path(feathering, &self.0, PathType::Open, stroke, out); stroke_path(feathering, &self.0, PathType::Open, stroke, out);
} }
/// A closed path (returning to the first point). /// A closed path (returning to the first point).
pub fn stroke_closed(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { pub fn stroke_closed(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) {
stroke_path(feathering, &self.0, PathType::Closed, stroke, out); stroke_path(feathering, &self.0, PathType::Closed, stroke, out);
} }
pub fn stroke(&self, feathering: f32, path_type: PathType, stroke: Stroke, out: &mut Mesh) { pub fn stroke(
&self,
feathering: f32,
path_type: PathType,
stroke: &PathStroke,
out: &mut Mesh,
) {
stroke_path(feathering, &self.0, path_type, stroke, out); stroke_path(feathering, &self.0, path_type, stroke, out);
} }
@ -864,19 +873,28 @@ fn stroke_path(
feathering: f32, feathering: f32,
path: &[PathPoint], path: &[PathPoint],
path_type: PathType, path_type: PathType,
stroke: Stroke, stroke: &PathStroke,
out: &mut Mesh, out: &mut Mesh,
) { ) {
let n = path.len() as u32; let n = path.len() as u32;
if stroke.width <= 0.0 || stroke.color == Color32::TRANSPARENT || n < 2 { if stroke.width <= 0.0 || stroke.color == ColorMode::TRANSPARENT || n < 2 {
return; return;
} }
let idx = out.vertices.len() as u32; let idx = out.vertices.len() as u32;
// expand the bounding box to include the thickness of the path
let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::<Vec<Pos2>>())
.expand((stroke.width / 2.0) + feathering);
let get_color = |col: &ColorMode, pos: Pos2| match col {
ColorMode::Solid(col) => *col,
ColorMode::UV(fun) => fun(bbox, pos),
};
if feathering > 0.0 { if feathering > 0.0 {
let color_inner = stroke.color; let color_inner = &stroke.color;
let color_outer = Color32::TRANSPARENT; let color_outer = Color32::TRANSPARENT;
let thin_line = stroke.width <= feathering; let thin_line = stroke.width <= feathering;
@ -889,9 +907,11 @@ fn stroke_path(
*/ */
// Fade out as it gets thinner: // Fade out as it gets thinner:
let color_inner = mul_color(color_inner, stroke.width / feathering); if let ColorMode::Solid(col) = color_inner {
if color_inner == Color32::TRANSPARENT { let color_inner = mul_color(*col, stroke.width / feathering);
return; if color_inner == Color32::TRANSPARENT {
return;
}
} }
out.reserve_triangles(4 * n as usize); out.reserve_triangles(4 * n as usize);
@ -904,7 +924,10 @@ fn stroke_path(
let p = p1.pos; let p = p1.pos;
let n = p1.normal; let n = p1.normal;
out.colored_vertex(p + n * feathering, color_outer); out.colored_vertex(p + n * feathering, color_outer);
out.colored_vertex(p, color_inner); out.colored_vertex(
p,
mul_color(get_color(color_inner, p), stroke.width / feathering),
);
out.colored_vertex(p - n * feathering, color_outer); out.colored_vertex(p - n * feathering, color_outer);
if connect_with_previous { if connect_with_previous {
@ -943,8 +966,14 @@ fn stroke_path(
let p = p1.pos; let p = p1.pos;
let n = p1.normal; let n = p1.normal;
out.colored_vertex(p + n * outer_rad, color_outer); out.colored_vertex(p + n * outer_rad, color_outer);
out.colored_vertex(p + n * inner_rad, color_inner); out.colored_vertex(
out.colored_vertex(p - n * inner_rad, color_inner); p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad, color_outer); out.colored_vertex(p - n * outer_rad, color_outer);
out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
@ -983,8 +1012,14 @@ fn stroke_path(
let n = end.normal; let n = end.normal;
let back_extrude = n.rot90() * feathering; let back_extrude = n.rot90() * feathering;
out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex(p + n * outer_rad + back_extrude, color_outer);
out.colored_vertex(p + n * inner_rad, color_inner); out.colored_vertex(
out.colored_vertex(p - n * inner_rad, color_inner); p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer);
out.add_triangle(idx + 0, idx + 1, idx + 2); out.add_triangle(idx + 0, idx + 1, idx + 2);
@ -997,8 +1032,14 @@ fn stroke_path(
let p = point.pos; let p = point.pos;
let n = point.normal; let n = point.normal;
out.colored_vertex(p + n * outer_rad, color_outer); out.colored_vertex(p + n * outer_rad, color_outer);
out.colored_vertex(p + n * inner_rad, color_inner); out.colored_vertex(
out.colored_vertex(p - n * inner_rad, color_inner); p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad, color_outer); out.colored_vertex(p - n * outer_rad, color_outer);
out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
@ -1020,8 +1061,14 @@ fn stroke_path(
let n = end.normal; let n = end.normal;
let back_extrude = -n.rot90() * feathering; let back_extrude = -n.rot90() * feathering;
out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex(p + n * outer_rad + back_extrude, color_outer);
out.colored_vertex(p + n * inner_rad, color_inner); out.colored_vertex(
out.colored_vertex(p - n * inner_rad, color_inner); p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer);
out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
@ -1067,19 +1114,39 @@ fn stroke_path(
if thin_line { if thin_line {
// Fade out thin lines rather than making them thinner // Fade out thin lines rather than making them thinner
let radius = feathering / 2.0; let radius = feathering / 2.0;
let color = mul_color(stroke.color, stroke.width / feathering); if let ColorMode::Solid(color) = stroke.color {
if color == Color32::TRANSPARENT { let color = mul_color(color, stroke.width / feathering);
return; if color == Color32::TRANSPARENT {
return;
}
} }
for p in path { for p in path {
out.colored_vertex(p.pos + radius * p.normal, color); out.colored_vertex(
out.colored_vertex(p.pos - radius * p.normal, color); p.pos + radius * p.normal,
mul_color(
get_color(&stroke.color, p.pos + radius * p.normal),
stroke.width / feathering,
),
);
out.colored_vertex(
p.pos - radius * p.normal,
mul_color(
get_color(&stroke.color, p.pos - radius * p.normal),
stroke.width / feathering,
),
);
} }
} else { } else {
let radius = stroke.width / 2.0; let radius = stroke.width / 2.0;
for p in path { for p in path {
out.colored_vertex(p.pos + radius * p.normal, stroke.color); out.colored_vertex(
out.colored_vertex(p.pos - radius * p.normal, stroke.color); p.pos + radius * p.normal,
get_color(&stroke.color, p.pos + radius * p.normal),
);
out.colored_vertex(
p.pos - radius * p.normal,
get_color(&stroke.color, p.pos - radius * p.normal),
);
} }
} }
} }
@ -1275,9 +1342,9 @@ impl Tessellator {
self.tessellate_text(&text_shape, out); self.tessellate_text(&text_shape, out);
} }
Shape::QuadraticBezier(quadratic_shape) => { Shape::QuadraticBezier(quadratic_shape) => {
self.tessellate_quadratic_bezier(quadratic_shape, out); self.tessellate_quadratic_bezier(&quadratic_shape, out);
} }
Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(cubic_shape, out), Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(&cubic_shape, out),
Shape::Callback(_) => { Shape::Callback(_) => {
panic!("Shape::Callback passed to Tessellator"); panic!("Shape::Callback passed to Tessellator");
} }
@ -1337,7 +1404,7 @@ impl Tessellator {
self.scratchpad_path.add_circle(center, radius); self.scratchpad_path.add_circle(center, radius);
self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path.fill(self.feathering, fill, out);
self.scratchpad_path self.scratchpad_path
.stroke_closed(self.feathering, stroke, out); .stroke_closed(self.feathering, &stroke.into(), out);
} }
/// Tessellate a single [`EllipseShape`] into a [`Mesh`]. /// Tessellate a single [`EllipseShape`] into a [`Mesh`].
@ -1404,7 +1471,7 @@ impl Tessellator {
self.scratchpad_path.add_line_loop(&points); self.scratchpad_path.add_line_loop(&points);
self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path.fill(self.feathering, fill, out);
self.scratchpad_path self.scratchpad_path
.stroke_closed(self.feathering, stroke, out); .stroke_closed(self.feathering, &stroke.into(), out);
} }
/// Tessellate a single [`Mesh`] into a [`Mesh`]. /// Tessellate a single [`Mesh`] into a [`Mesh`].
@ -1430,7 +1497,13 @@ impl Tessellator {
/// ///
/// * `shape`: the mesh to tessellate. /// * `shape`: the mesh to tessellate.
/// * `out`: triangles are appended to this. /// * `out`: triangles are appended to this.
pub fn tessellate_line(&mut self, points: [Pos2; 2], stroke: Stroke, out: &mut Mesh) { pub fn tessellate_line(
&mut self,
points: [Pos2; 2],
stroke: impl Into<PathStroke>,
out: &mut Mesh,
) {
let stroke = stroke.into();
if stroke.is_empty() { if stroke.is_empty() {
return; return;
} }
@ -1446,7 +1519,7 @@ impl Tessellator {
self.scratchpad_path.clear(); self.scratchpad_path.clear();
self.scratchpad_path.add_line_segment(points); self.scratchpad_path.add_line_segment(points);
self.scratchpad_path self.scratchpad_path
.stroke_open(self.feathering, stroke, out); .stroke_open(self.feathering, &stroke, out);
} }
/// Tessellate a single [`PathShape`] into a [`Mesh`]. /// Tessellate a single [`PathShape`] into a [`Mesh`].
@ -1493,7 +1566,7 @@ impl Tessellator {
PathType::Open PathType::Open
}; };
self.scratchpad_path self.scratchpad_path
.stroke(self.feathering, typ, *stroke, out); .stroke(self.feathering, typ, stroke, out);
} }
/// Tessellate a single [`Rect`] into a [`Mesh`]. /// Tessellate a single [`Rect`] into a [`Mesh`].
@ -1588,7 +1661,7 @@ impl Tessellator {
path.fill(self.feathering, fill, out); path.fill(self.feathering, fill, out);
} }
path.stroke_closed(self.feathering, stroke, out); path.stroke_closed(self.feathering, &stroke.into(), out);
} }
self.feathering = old_feathering; // restore self.feathering = old_feathering; // restore
@ -1707,8 +1780,11 @@ impl Tessellator {
self.scratchpad_path.clear(); self.scratchpad_path.clear();
self.scratchpad_path self.scratchpad_path
.add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]); .add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]);
self.scratchpad_path self.scratchpad_path.stroke_open(
.stroke_open(self.feathering, *underline, out); self.feathering,
&PathStroke::from(*underline),
out,
);
} }
} }
} }
@ -1719,7 +1795,7 @@ impl Tessellator {
/// * `out`: triangles are appended to this. /// * `out`: triangles are appended to this.
pub fn tessellate_quadratic_bezier( pub fn tessellate_quadratic_bezier(
&mut self, &mut self,
quadratic_shape: QuadraticBezierShape, quadratic_shape: &QuadraticBezierShape,
out: &mut Mesh, out: &mut Mesh,
) { ) {
let options = &self.options; let options = &self.options;
@ -1737,7 +1813,7 @@ impl Tessellator {
&points, &points,
quadratic_shape.fill, quadratic_shape.fill,
quadratic_shape.closed, quadratic_shape.closed,
quadratic_shape.stroke, &quadratic_shape.stroke,
out, out,
); );
} }
@ -1746,7 +1822,7 @@ impl Tessellator {
/// ///
/// * `cubic_shape`: the shape to tessellate. /// * `cubic_shape`: the shape to tessellate.
/// * `out`: triangles are appended to this. /// * `out`: triangles are appended to this.
pub fn tessellate_cubic_bezier(&mut self, cubic_shape: CubicBezierShape, out: &mut Mesh) { pub fn tessellate_cubic_bezier(&mut self, cubic_shape: &CubicBezierShape, out: &mut Mesh) {
let options = &self.options; let options = &self.options;
let clip_rect = self.clip_rect; let clip_rect = self.clip_rect;
if options.coarse_tessellation_culling if options.coarse_tessellation_culling
@ -1763,7 +1839,7 @@ impl Tessellator {
&points, &points,
cubic_shape.fill, cubic_shape.fill,
cubic_shape.closed, cubic_shape.closed,
cubic_shape.stroke, &cubic_shape.stroke,
out, out,
); );
} }
@ -1774,7 +1850,7 @@ impl Tessellator {
points: &[Pos2], points: &[Pos2],
fill: Color32, fill: Color32,
closed: bool, closed: bool,
stroke: Stroke, stroke: &PathStroke,
out: &mut Mesh, out: &mut Mesh,
) { ) {
if points.len() < 2 { if points.len() < 2 {
@ -1985,3 +2061,48 @@ fn test_tessellator() {
assert_eq!(primitives.len(), 2); assert_eq!(primitives.len(), 2);
} }
#[test]
fn path_bounding_box() {
use crate::*;
for i in 1..=100 {
let width = i as f32;
let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(10.0, 10.0));
let expected_rect = rect.expand((width / 2.0) + 1.5);
let mut mesh = Mesh::default();
let mut path = Path::default();
path.add_open_points(&[
pos2(0.0, 0.0),
pos2(2.0, 0.0),
pos2(5.0, 5.0),
pos2(0.0, 5.0),
pos2(0.0, 7.0),
pos2(10.0, 10.0),
]);
path.stroke(
1.5,
PathType::Closed,
&PathStroke::new_uv(width, move |r, p| {
assert_eq!(r, expected_rect);
// see https://github.com/emilk/egui/pull/4353#discussion_r1573879940 for why .contains() isn't used here.
// TL;DR rounding errors.
assert!(
r.distance_to_pos(p) <= 0.55,
"passed rect {r:?} didn't contain point {p:?} (distance: {})",
r.distance_to_pos(p)
);
assert!(
expected_rect.distance_to_pos(p) <= 0.55,
"expected rect {expected_rect:?} didn't contain point {p:?}"
);
Color32::WHITE
}),
&mut mesh,
);
}
}

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use emath::*; use emath::*;
use crate::{text::font::Font, Color32, Mesh, Stroke, Vertex}; use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex};
use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals};
@ -853,7 +853,7 @@ fn add_hline(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke,
let mut path = crate::tessellator::Path::default(); // TODO(emilk): reuse this to avoid re-allocations. let mut path = crate::tessellator::Path::default(); // TODO(emilk): reuse this to avoid re-allocations.
path.add_line_segment([start, stop]); path.add_line_segment([start, stop]);
let feathering = 1.0 / point_scale.pixels_per_point(); let feathering = 1.0 / point_scale.pixels_per_point();
path.stroke_open(feathering, stroke, mesh); path.stroke_open(feathering, &PathStroke::from(stroke), mesh);
} else { } else {
// Thin lines often lost, so this is a bad idea // Thin lines often lost, so this is a bad idea