From 62b1a2658f344a06362bce51ba081e14cd3656b2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 1 Sep 2020 20:03:50 +0200 Subject: [PATCH] [tesselator] hide Path from view and improve thin rounded rectangles --- egui/src/containers/collapsing_header.rs | 6 +- egui/src/containers/window.rs | 32 ++++--- egui/src/demos/app.rs | 2 +- egui/src/paint.rs | 2 +- egui/src/paint/command.rs | 6 +- egui/src/paint/tessellator.rs | 113 +++++++++++------------ egui/src/widgets.rs | 4 +- 7 files changed, 86 insertions(+), 79 deletions(-) diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 599f14402..75cb077d7 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -2,7 +2,7 @@ use std::hash::Hash; use crate::{ layout::Direction, - paint::{LineStyle, PaintCmd, Path, TextStyle}, + paint::{LineStyle, PaintCmd, TextStyle}, widgets::Label, *, }; @@ -112,7 +112,7 @@ pub fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) { // Draw a pointy triangle arrow: let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75); - let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()]; + let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()]; let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0)); for p in &mut points { let v = *p - rect.center(); @@ -121,7 +121,7 @@ pub fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) { } ui.painter().add(PaintCmd::Path { - path: Path::from_point_loop(&points), + points, closed: true, fill: Default::default(), outline: LineStyle::new(stroke_width, stroke_color), diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 9b232d640..c9af75fb9 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -531,39 +531,47 @@ fn paint_frame_interaction( interaction: WindowInteraction, visuals: style::WidgetVisuals, ) { + use paint::tessellator::path::add_circle_quadrant; + let cr = ui.style().visuals.window_corner_radius; let Rect { min, max } = rect; - let mut path = Path::default(); + let mut points = Vec::new(); if interaction.right && !interaction.bottom && !interaction.top { - path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); + points.push(pos2(max.x, min.y + cr)); + points.push(pos2(max.x, max.y - cr)); } if interaction.right && interaction.bottom { - path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); - path.add_circle_quadrant(pos2(max.x - cr, max.y - cr), cr, 0.0); + points.push(pos2(max.x, min.y + cr)); + points.push(pos2(max.x, max.y - cr)); + add_circle_quadrant(&mut points, pos2(max.x - cr, max.y - cr), cr, 0.0); } if interaction.bottom { - path.add_line_segment([pos2(max.x - cr, max.y), pos2(min.x + cr, max.y)]); + points.push(pos2(max.x - cr, max.y)); + points.push(pos2(min.x + cr, max.y)); } if interaction.left && interaction.bottom { - path.add_circle_quadrant(pos2(min.x + cr, max.y - cr), cr, 1.0); + add_circle_quadrant(&mut points, pos2(min.x + cr, max.y - cr), cr, 1.0); } if interaction.left { - path.add_line_segment([pos2(min.x, max.y - cr), pos2(min.x, min.y + cr)]); + points.push(pos2(min.x, max.y - cr)); + points.push(pos2(min.x, min.y + cr)); } if interaction.left && interaction.top { - path.add_circle_quadrant(pos2(min.x + cr, min.y + cr), cr, 2.0); + add_circle_quadrant(&mut points, pos2(min.x + cr, min.y + cr), cr, 2.0); } if interaction.top { - path.add_line_segment([pos2(min.x + cr, min.y), pos2(max.x - cr, min.y)]); + points.push(pos2(min.x + cr, min.y)); + points.push(pos2(max.x - cr, min.y)); } if interaction.right && interaction.top { - path.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0); - path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); + add_circle_quadrant(&mut points, pos2(max.x - cr, min.y + cr), cr, 3.0); + points.push(pos2(max.x, min.y + cr)); + points.push(pos2(max.x, max.y - cr)); } ui.painter().add(PaintCmd::Path { - path, + points, closed: false, fill: Default::default(), outline: visuals.bg_outline, diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index 7fc7e6122..8421195ee 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -653,7 +653,7 @@ impl Painting { if line.len() >= 2 { let points: Vec = line.iter().map(|p| rect.min + *p).collect(); painter.add(PaintCmd::Path { - path: Path::from_open_points(&points), + points, closed: false, outline: LineStyle::new(self.line_width, LIGHT_GRAY), fill: Default::default(), diff --git a/egui/src/paint.rs b/egui/src/paint.rs index c40e65a24..1ff709633 100644 --- a/egui/src/paint.rs +++ b/egui/src/paint.rs @@ -13,6 +13,6 @@ pub use { color::{Rgba, Srgba}, command::{LineStyle, PaintCmd}, fonts::{FontDefinitions, Fonts, TextStyle}, - tessellator::{PaintJobs, PaintOptions, Path, Triangles, Vertex}, + tessellator::{PaintJobs, PaintOptions, Triangles, Vertex}, texture_atlas::Texture, }; diff --git a/egui/src/paint/command.rs b/egui/src/paint/command.rs index 1af01e4f2..0f631bc4a 100644 --- a/egui/src/paint/command.rs +++ b/egui/src/paint/command.rs @@ -1,5 +1,5 @@ use { - super::{font::Galley, fonts::TextStyle, Path, Srgba, Triangles}, + super::{font::Galley, fonts::TextStyle, Srgba, Triangles}, crate::math::{Pos2, Rect}, }; @@ -19,7 +19,9 @@ pub enum PaintCmd { style: LineStyle, }, Path { - path: Path, + points: Vec, + /// If true, connect the first and last of the points together. + /// This is required if `fill != TRANSPARENT`. closed: bool, fill: Srgba, outline: LineStyle, diff --git a/egui/src/paint/tessellator.rs b/egui/src/paint/tessellator.rs index 1f8d05b67..4dc625585 100644 --- a/egui/src/paint/tessellator.rs +++ b/egui/src/paint/tessellator.rs @@ -184,34 +184,15 @@ pub struct PathPoint { /// A connected line (without thickness or gaps) which can be tessellated /// to either to an outline (with thickness) or a filled convex area. +/// Used as a scratch-pad during tesselation. #[derive(Clone, Debug, Default)] -pub struct Path(Vec); +struct Path(Vec); impl Path { - pub fn from_point_loop(points: &[Pos2]) -> Self { - let mut path = Self::default(); - path.add_line_loop(points); - path - } - - pub fn from_open_points(points: &[Pos2]) -> Self { - let mut path = Self::default(); - path.add_open_points(points); - path - } - pub fn clear(&mut self) { self.0.clear(); } - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn len(&self) -> usize { - self.0.len() - } - pub fn reserve(&mut self, additional: usize) { self.0.reserve(additional) } @@ -247,14 +228,15 @@ impl Path { // Common case optimization: self.add_line_segment([points[0], points[1]]); } else { + // TODO: optimize self.reserve(n); self.add_point(points[0], (points[1] - points[0]).normalized().rot90()); for i in 1..n - 1 { - let n0 = (points[i] - points[i - 1]).normalized().rot90(); // TODO: don't calculate each normal twice! - let n1 = (points[i + 1] - points[i]).normalized().rot90(); // TODO: don't calculate each normal twice! + let n0 = (points[i] - points[i - 1]).normalized().rot90(); + let n1 = (points[i + 1] - points[i]).normalized().rot90(); let v = (n0 + n1) / 2.0; - let normal = v / v.length_sq(); - self.add_point(points[i], normal); // TODO: handle VERY sharp turns better + let normal = v / v.length_sq(); // TODO: handle VERY sharp turns better + self.add_point(points[i], normal); } self.add_point( points[n - 1], @@ -273,22 +255,20 @@ impl Path { let n0 = (points[i] - points[(i + n - 1) % n]).normalized().rot90(); let n1 = (points[(i + 1) % n] - points[i]).normalized().rot90(); let v = (n0 + n1) / 2.0; - let normal = v / v.length_sq(); - self.add_point(points[i], normal); // TODO: handle VERY sharp turns better + let normal = v / v.length_sq(); // TODO: handle VERY sharp turns better + self.add_point(points[i], normal); } } +} - pub fn add_rectangle(&mut self, rect: Rect) { - let min = rect.min; - let max = rect.max; - self.reserve(4); - self.add_point(pos2(min.x, min.y), vec2(-1.0, -1.0)); - self.add_point(pos2(max.x, min.y), vec2(1.0, -1.0)); - self.add_point(pos2(max.x, max.y), vec2(1.0, 1.0)); - self.add_point(pos2(min.x, max.y), vec2(-1.0, 1.0)); - } +pub(crate) mod path { + //! Helpers for constructing paths + use super::*; + + /// overwrites existing points + pub fn rounded_rectangle(path: &mut Vec, rect: Rect, corner_radius: f32) { + path.clear(); - pub fn add_rounded_rectangle(&mut self, rect: Rect, corner_radius: f32) { let min = rect.min; let max = rect.max; @@ -297,12 +277,18 @@ impl Path { .min(rect.height() * 0.5); if cr <= 0.0 { - self.add_rectangle(rect); + let min = rect.min; + let max = rect.max; + path.reserve(4); + path.push(pos2(min.x, min.y)); + path.push(pos2(max.x, min.y)); + path.push(pos2(max.x, max.y)); + path.push(pos2(min.x, max.y)); } else { - self.add_circle_quadrant(pos2(max.x - cr, max.y - cr), cr, 0.0); - self.add_circle_quadrant(pos2(min.x + cr, max.y - cr), cr, 1.0); - self.add_circle_quadrant(pos2(min.x + cr, min.y + cr), cr, 2.0); - self.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0); + add_circle_quadrant(path, pos2(max.x - cr, max.y - cr), cr, 0.0); + add_circle_quadrant(path, pos2(min.x + cr, max.y - cr), cr, 1.0); + add_circle_quadrant(path, pos2(min.x + cr, min.y + cr), cr, 2.0); + add_circle_quadrant(path, pos2(max.x - cr, min.y + cr), cr, 3.0); } } @@ -324,21 +310,20 @@ impl Path { // * angle 3 * TAU / 4 = top // - quadrant 3: right top // * angle 4 * TAU / 4 = right - pub fn add_circle_quadrant(&mut self, center: Pos2, radius: f32, quadrant: f32) { + pub fn add_circle_quadrant(path: &mut Vec, center: Pos2, radius: f32, quadrant: f32) { // TODO: optimize with precalculated vertices for some radii ranges let n = (radius * 0.75).round() as i32; // TODO: tweak a bit more let n = clamp(n, 2..=32); - self.reserve(n as usize + 1); const RIGHT_ANGLE: f32 = TAU / 4.0; + path.reserve(n as usize + 1); for i in 0..=n { let angle = remap( i as f32, 0.0..=n as f32, quadrant * RIGHT_ANGLE..=(quadrant + 1.0) * RIGHT_ANGLE, ); - let normal = vec2(angle.cos(), angle.sin()); - self.add_point(center + radius * normal, normal); + path.push(center + radius * Vec2::angled(angle)); } } } @@ -374,12 +359,7 @@ impl Default for PaintOptions { } /// Tesselate the given convex area into a polygon. -pub fn fill_closed_path( - path: &[PathPoint], - color: Srgba, - options: PaintOptions, - out: &mut Triangles, -) { +fn fill_closed_path(path: &[PathPoint], color: Srgba, options: PaintOptions, out: &mut Triangles) { if color == color::TRANSPARENT { return; } @@ -420,7 +400,7 @@ pub fn fill_closed_path( } /// Tesselate the given path as an outline with thickness. -pub fn paint_path_outline( +fn paint_path_outline( path: &[PathPoint], path_type: PathType, style: LineStyle, @@ -584,11 +564,12 @@ fn mul_color(color: Srgba, factor: f32) -> Srgba { /// * `out`: where the triangles are put /// * `scratchpad_path`: if you plan to run `tessellate_paint_command` /// many times, pass it a reference to the same `Path` to avoid excessive allocations. -pub fn tessellate_paint_command( +fn tessellate_paint_command( command: PaintCmd, options: PaintOptions, fonts: &Fonts, out: &mut Triangles, + scratchpad_points: &mut Vec, scratchpad_path: &mut Path, ) { let path = scratchpad_path; @@ -616,12 +597,18 @@ pub fn tessellate_paint_command( paint_path_outline(&path.0, Open, style, options, out); } PaintCmd::Path { - path, + points, closed, fill, outline, } => { - if path.len() >= 2 { + if points.len() >= 2 { + if closed { + path.add_line_loop(&points); + } else { + path.add_open_points(&points); + } + if fill != TRANSPARENT { debug_assert!( closed, @@ -645,7 +632,8 @@ pub fn tessellate_paint_command( rect.min = rect.min.max(pos2(-1e7, -1e7)); rect.max = rect.max.min(pos2(1e7, 1e7)); - path.add_rounded_rectangle(rect, corner_radius); + path::rounded_rectangle(scratchpad_points, rect, corner_radius); + path.add_line_loop(scratchpad_points); fill_closed_path(&path.0, fill, options, out); paint_path_outline(&path.0, Closed, outline, options, out); } @@ -710,6 +698,7 @@ pub fn tessellate_paint_commands( options: PaintOptions, fonts: &Fonts, ) -> Vec<(Rect, Triangles)> { + let mut scratchpad_points = Vec::new(); let mut scratchpad_path = Path::default(); let mut jobs = PaintJobs::default(); @@ -721,7 +710,14 @@ pub fn tessellate_paint_commands( } let out = &mut jobs.last_mut().unwrap().1; - tessellate_paint_command(cmd, options, fonts, out, &mut scratchpad_path); + tessellate_paint_command( + cmd, + options, + fonts, + out, + &mut scratchpad_points, + &mut scratchpad_path, + ); } if options.debug_paint_clip_rects { @@ -736,6 +732,7 @@ pub fn tessellate_paint_commands( options, fonts, triangles, + &mut scratchpad_points, &mut scratchpad_path, ) } diff --git a/egui/src/widgets.rs b/egui/src/widgets.rs index 123f219fe..5f7628493 100644 --- a/egui/src/widgets.rs +++ b/egui/src/widgets.rs @@ -366,11 +366,11 @@ impl<'a> Widget for Checkbox<'a> { if *checked { ui.painter().add(PaintCmd::Path { - path: Path::from_open_points(&[ + points: vec![ pos2(small_icon_rect.left(), small_icon_rect.center().y), pos2(small_icon_rect.center().x, small_icon_rect.bottom()), pos2(small_icon_rect.right(), small_icon_rect.top()), - ]), + ], closed: false, outline: LineStyle::new(ui.style().visuals.line_width, stroke_color), fill: Default::default(),