From 58430ff0cd62dc911ed9dd4fb600514ad513a149 Mon Sep 17 00:00:00 2001 From: Jared Moulton Date: Tue, 18 Feb 2025 03:39:04 -0700 Subject: [PATCH] fix border when there is no border radius (#779) --- examples/widget-gallery/Cargo.toml | 3 ++ src/border_path_iter.rs | 55 ++++++++++++++++++++++-------- src/view.rs | 11 ++++-- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/examples/widget-gallery/Cargo.toml b/examples/widget-gallery/Cargo.toml index 383308cc..d1275dd0 100644 --- a/examples/widget-gallery/Cargo.toml +++ b/examples/widget-gallery/Cargo.toml @@ -8,3 +8,6 @@ im.workspace = true floem = { path = "../..", features = ["rfd-async-std"] } strum = { version = "0.25.0", features = ["derive"] } files = { path = "../files/" } + +[features] +vello = ["floem/vello"] diff --git a/src/border_path_iter.rs b/src/border_path_iter.rs index 0ae5c74c..f8082426 100644 --- a/src/border_path_iter.rs +++ b/src/border_path_iter.rs @@ -10,6 +10,9 @@ pub struct BorderPath { impl BorderPath { pub fn new(rect: Rect, radii: RoundedRectRadii) -> Self { + let rect = rect.abs(); + let shortest_side_length = (rect.width()).min(rect.height()); + let radii = radii.abs().clamp(shortest_side_length / 2.0); let rounded_path = RectPathIter { idx: 0, rect, @@ -90,7 +93,11 @@ impl<'a> Iterator for BorderPathIter<'a> { type Item = BorderPathEvent<'a>; fn next(&mut self) -> Option { - assert!(self.total_len > 0.0, "Total length must be positive"); + assert!( + self.total_len >= 0.0, + "Total length must be positive. Total_len: {}", + self.total_len + ); assert!( self.current_len <= self.total_len, "Current length cannot exceed total length" @@ -100,6 +107,9 @@ impl<'a> Iterator for BorderPathIter<'a> { assert!(end <= 1.0, "Range end must be <= 1.0"); assert!(end >= 0.0, "Range end must be >= 0.0"); + // this large-ish epsilon value is necessary. If we check numbers too small then we have weird behavior where we don't properly check our end conditions when we reasonably should. + const EPSILON: f64 = 1e-4; + // Handle current iterator if it exists if let Some(iter) = &mut self.current_iter { if let Some(element) = iter.next() { @@ -109,8 +119,6 @@ impl<'a> Iterator for BorderPathIter<'a> { } assert!(self.current_iter.is_none()); - const EPSILON: f64 = 1e-4; - // end condition: we have reached the target percentage if (self.current_len / self.total_len - end).abs() <= EPSILON || (self.current_len / self.total_len) >= end @@ -136,6 +144,9 @@ impl<'a> Iterator for BorderPathIter<'a> { Some(ArcOrPath::Arc(arc)) => { // Set corner flag based on arc transition let arc_len = arc.perimeter(self.tolerance); + if arc_len < EPSILON { + continue; + } let normalized_current = self.current_len / self.total_len; let normalized_seg_len = arc_len / self.total_len; @@ -151,7 +162,11 @@ impl<'a> Iterator for BorderPathIter<'a> { assert!(t > 0.0 && t <= 1.0, "Invalid subsegment parameter"); let subseg = arc_subsegment(&arc, 0.0..t); - self.current_len += subseg.perimeter(self.tolerance); + let seg_len = subseg.perimeter(self.tolerance); + if seg_len < EPSILON { + continue; + } + self.current_len += seg_len; self.current_iter = Some(Box::new(subseg.path_elements(self.tolerance))); } else { self.current_len += arc_len; @@ -161,6 +176,9 @@ impl<'a> Iterator for BorderPathIter<'a> { } Some(ArcOrPath::Path(path_seg)) => { let seg_len = path_seg.arclen(self.tolerance); + if seg_len < EPSILON { + continue; + } let normalized_current = self.current_len / self.total_len; let normalized_seg_len = seg_len / self.total_len; @@ -175,11 +193,16 @@ impl<'a> Iterator for BorderPathIter<'a> { assert!(t > 0.0 && t <= 1.0, "Invalid subsegment parameter"); let subseg = path_seg.subsegment(0.0..t); - self.current_len += subseg.arclen(self.tolerance); - self.current_iter = Some(Box::new(std::iter::once(subseg.as_path_el()))); + + let seg_len = subseg.arclen(self.tolerance); + if seg_len < f64::EPSILON { + continue; + } + self.current_len += seg_len; + self.current_iter = Some(Box::new(subseg.path_elements(self.tolerance))); } else { self.current_len += seg_len; - self.current_iter = Some(Box::new(std::iter::once(path_seg.as_path_el()))); + self.current_iter = Some(Box::new(path_seg.path_elements(self.tolerance))); } break; } @@ -324,23 +347,24 @@ impl RectPathIter { } fn total_len(&self, tolerance: f64) -> f64 { - // Calculate arc lengths - one for each corner + // Calculate arc lengths with clamped radii let arc_lengths: f64 = (0..4) .map(|i| self.build_corner_arc(i).perimeter(tolerance)) .sum(); - // Calculate straight segment lengths + // Calculate straight segment lengths with clamped radii let straight_lengths = { - let rect = self.rect; - let radii = self.radii; + let width = self.rect.width(); + let height = self.rect.height(); + let radii = &self.radii; // Top edge (minus the arc segments) - let top = rect.x1 - rect.x0 - radii.top_left - radii.top_right; + let top = width - radii.top_left - radii.top_right; // Right edge - let right = rect.y1 - rect.y0 - radii.top_right - radii.bottom_right; + let right = height - radii.top_right - radii.bottom_right; // Bottom edge - let bottom = rect.x1 - rect.x0 - radii.bottom_left - radii.bottom_right; + let bottom = width - radii.bottom_left - radii.bottom_right; // Left edge - let left = rect.y1 - rect.y0 - radii.top_left - radii.bottom_left; + let left = height - radii.top_left - radii.bottom_left; top + right + bottom + left }; @@ -383,6 +407,7 @@ pub struct RoundedRectPathIter { rect: RectPathIter, arcs: [Arc; 8], } + #[derive(Debug)] pub enum ArcOrPath { Arc(Arc), diff --git a/src/view.rs b/src/view.rs index 293c1bf8..885e770f 100644 --- a/src/view.rs +++ b/src/view.rs @@ -691,10 +691,17 @@ pub(crate) fn paint_border( border_path.subsegment(0.0..(border_progress.clamp(0.0, 100.) / 100.)); } - let mut current_path = Vec::new(); + // optimize for maximum which is 12 paths and a single move to + let mut current_path = smallvec::SmallVec::<[_; 13]>::new(); for event in border_path.path_elements(&borders, 0.1) { match event { - BorderPathEvent::PathElement(el) => current_path.push(el), + BorderPathEvent::PathElement(el) => { + if !current_path.is_empty() && matches!(el, PathEl::MoveTo(_)) { + // extra move to's will mess up dashed patterns + continue; + } + current_path.push(el) + } BorderPathEvent::NewStroke(stroke) => { // Render current path with previous stroke if any if !current_path.is_empty() {