diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index eaffa0be15e..d6c0321f110 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -329,6 +329,9 @@ impl SidePanel { ui.ctx().set_cursor_icon(cursor_icon); } + // Keep this rect snapped so that panel content can be pixel-perfect + let rect = ui.painter().round_rect_to_pixels(rect); + PanelState { rect }.store(ui.ctx(), id); { @@ -343,10 +346,14 @@ impl SidePanel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel - // (hence the shrink). - let resize_x = side.opposite().side_x(rect.shrink(1.0)); - let resize_x = ui.painter().round_to_pixel(resize_x); + let resize_x = side.opposite().side_x(rect); + + // This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc) + let resize_x = ui.painter().round_to_pixel_center(resize_x); + + // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for + // left-side panels + let resize_x = resize_x - if side == Side::Left { 1.0 } else { 0.0 }; ui.painter().vline(resize_x, panel_rect.y_range(), stroke); } @@ -817,6 +824,9 @@ impl TopBottomPanel { ui.ctx().set_cursor_icon(cursor_icon); } + // Keep this rect snapped so that panel content can be pixel-perfect + let rect = ui.painter().round_rect_to_pixels(rect); + PanelState { rect }.store(ui.ctx(), id); { @@ -831,10 +841,12 @@ impl TopBottomPanel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel - // (hence the shrink). - let resize_y = side.opposite().side_y(rect.shrink(1.0)); - let resize_y = ui.painter().round_to_pixel(resize_y); + let resize_y = side.opposite().side_y(rect); + let resize_y = ui.painter().round_to_pixel_center(resize_y); + + // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for + // top-side panels + let resize_y = resize_y - if side == TopBottomSide::Top { 1.0 } else { 0.0 }; ui.painter().hline(panel_rect.x_range(), resize_y, stroke); } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index eb7184c6e22..c183a4dc731 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -439,9 +439,6 @@ impl<'open> Window<'open> { let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); // Keep the original inner margin for later use let window_margin = window_frame.inner_margin; - let border_padding = window_frame.stroke.width / 2.0; - // Add border padding to the inner margin to prevent it from covering the contents - window_frame.inner_margin += border_padding; let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -575,9 +572,9 @@ impl<'open> Window<'open> { if let Some(title_bar) = title_bar { let mut title_rect = Rect::from_min_size( - outer_rect.min + vec2(border_padding, border_padding), + outer_rect.min, Vec2 { - x: outer_rect.size().x - border_padding * 2.0, + x: outer_rect.size().x, y: title_bar_height, }, ); @@ -587,9 +584,6 @@ impl<'open> Window<'open> { if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = window_frame.rounding; - // Eliminate the rounding gap between the title bar and the window frame - round -= border_padding; - if !is_collapsed { round.se = 0.0; round.sw = 0.0; @@ -603,7 +597,7 @@ impl<'open> Window<'open> { // Fix title bar separator line position if let Some(response) = &mut content_response { - response.rect.min.y = outer_rect.min.y + title_bar_height + border_padding; + response.rect.min.y = outer_rect.min.y + title_bar_height; } title_bar.ui( @@ -667,14 +661,10 @@ fn paint_resize_corner( } }; - // Adjust the corner offset to accommodate the stroke width and window rounding - let offset = if radius <= 2.0 && stroke.width < 2.0 { - 2.0 - } else { - // The corner offset is calculated to make the corner appear to be in the correct position - (2.0_f32.sqrt() * (1.0 + radius + stroke.width / 2.0) - radius) - * 45.0_f32.to_radians().cos() - }; + // Adjust the corner offset to accommodate for window rounding + let offset = + ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0); + let corner_size = Vec2::splat(ui.visuals().resize_corner_size); let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner @@ -1136,7 +1126,6 @@ impl TitleBar { let text_pos = emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top(); let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); - let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better) ui.painter().galley( text_pos, self.title_galley.clone(), @@ -1150,6 +1139,7 @@ impl TitleBar { let stroke = ui.visuals().widgets.noninteractive.bg_stroke; // Workaround: To prevent border infringement, // the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels + // or we could support selectively disabling feathering on line caps let x_range = outer_rect.x_range().shrink(0.1); ui.painter().hline(x_range, y, stroke); } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index ff13c14d764..7225af6d1ca 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1717,26 +1717,42 @@ impl Context { }); } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub(crate) fn round_to_pixel_center(&self, point: f32) -> f32 { + let pixels_per_point = self.pixels_per_point(); + ((point * pixels_per_point - 0.5).round() + 0.5) / pixels_per_point + } + + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub(crate) fn round_pos_to_pixel_center(&self, point: Pos2) -> Pos2 { + pos2( + self.round_to_pixel_center(point.x), + self.round_to_pixel_center(point.y), + ) + } + + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_to_pixel(&self, point: f32) -> f32 { let pixels_per_point = self.pixels_per_point(); (point * pixels_per_point).round() / pixels_per_point } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y)) } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_rect_to_pixels(&self, rect: Rect) -> Rect { Rect { diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index b173310f756..a79deac5c9a 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -158,7 +158,19 @@ impl Painter { self.clip_rect = clip_rect; } - /// Useful for pixel-perfect rendering. + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub fn round_to_pixel_center(&self, point: f32) -> f32 { + self.ctx().round_to_pixel_center(point) + } + + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { + self.ctx().round_pos_to_pixel_center(pos) + } + + /// Useful for pixel-perfect rendering of filled shapes. #[inline] pub fn round_to_pixel(&self, point: f32) -> f32 { self.ctx().round_to_pixel(point) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 46f391c0216..0097f53521f 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2477,8 +2477,12 @@ impl Widget for &mut Stroke { // stroke preview: let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size); - let left = stroke_rect.left_center(); - let right = stroke_rect.right_center(); + let left = ui + .painter() + .round_pos_to_pixel_center(stroke_rect.left_center()); + let right = ui + .painter() + .round_pos_to_pixel_center(stroke_rect.right_center()); ui.painter().line_segment([left, right], (*width, *color)); }) .response diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 8f9454ce45c..e502f11e148 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2215,9 +2215,9 @@ impl Ui { let stroke = self.visuals().widgets.noninteractive.bg_stroke; let left_top = child_rect.min - 0.5 * indent * Vec2::X; - let left_top = self.painter().round_pos_to_pixels(left_top); + let left_top = self.painter().round_pos_to_pixel_center(left_top); let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0); - let left_bottom = self.painter().round_pos_to_pixels(left_bottom); + let left_bottom = self.painter().round_pos_to_pixel_center(left_bottom); if left_vline { // draw a faint line on the left to mark the indented section diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index 7792bd23950..e421de9cf89 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -116,12 +116,12 @@ impl Widget for Separator { if is_horizontal_line { painter.hline( (rect.left() - grow)..=(rect.right() + grow), - painter.round_to_pixel(rect.center().y), + painter.round_to_pixel_center(rect.center().y), stroke, ); } else { painter.vline( - painter.round_to_pixel(rect.center().x), + painter.round_to_pixel_center(rect.center().x), (rect.top() - grow)..=(rect.bottom() + grow), stroke, ); diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 13e26f83b45..3805ce9a4cc 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -277,12 +277,14 @@ impl eframe::App for WrapApp { } let mut cmd = Command::Nothing; - egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { - ui.horizontal_wrapped(|ui| { - ui.visuals_mut().button_frame = false; - self.bar_contents(ui, frame, &mut cmd); + egui::TopBottomPanel::top("wrap_app_top_bar") + .frame(egui::Frame::none().inner_margin(4.0)) + .show(ctx, |ui| { + ui.horizontal_wrapped(|ui| { + ui.visuals_mut().button_frame = false; + self.bar_contents(ui, frame, &mut cmd); + }); }); - }); self.state.backend_panel.update(ctx, frame); @@ -324,6 +326,7 @@ impl WrapApp { egui::SidePanel::left("backend_panel") .resizable(false) .show_animated(ctx, is_open, |ui| { + ui.add_space(4.0); ui.vertical_centered(|ui| { ui.heading("💻 Backend"); }); diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 160ab2e6a60..576a69e66df 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -260,6 +260,7 @@ impl DemoWindows { .resizable(false) .default_width(150.0) .show(ctx, |ui| { + ui.add_space(4.0); ui.vertical_centered(|ui| { ui.heading("✒ egui demos"); }); diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index f3c788a69e3..70fd9983768 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -415,6 +415,49 @@ pub fn pixel_test(ui: &mut Ui) { ui.add_space(4.0); pixel_test_squares(ui); + + ui.add_space(4.0); + + pixel_test_strokes(ui); +} + +fn pixel_test_strokes(ui: &mut Ui) { + ui.label("The strokes should align to the physical pixel grid."); + let color = if ui.style().visuals.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + let pixels_per_point = ui.ctx().pixels_per_point(); + + for thickness_pixels in 1..=3 { + let thickness_pixels = thickness_pixels as f32; + let thickness_points = thickness_pixels / pixels_per_point; + let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; + let size_pixels = vec2( + ui.available_width(), + num_squares as f32 + thickness_pixels * 2.0, + ); + let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); + let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); + + let mut cursor_pixel = Pos2::new( + response.rect.min.x * pixels_per_point + thickness_pixels, + response.rect.min.y * pixels_per_point + thickness_pixels, + ) + .ceil(); + + let stroke = Stroke::new(thickness_points, color); + for size in 1..=num_squares { + let rect_points = Rect::from_min_size( + Pos2::new(cursor_pixel.x, cursor_pixel.y), + Vec2::splat(size as f32), + ); + painter.rect_stroke(rect_points / pixels_per_point, 0.0, stroke); + cursor_pixel.x += (1 + size) as f32 + thickness_pixels * 2.0; + } + } } fn pixel_test_squares(ui: &mut Ui) { diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index f1155bd07d6..399a602c259 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -55,6 +55,26 @@ impl std::hash::Hash for Stroke { } } +/// Describes how the stroke of a shape should be painted. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum StrokeKind { + /// The stroke should be painted entirely outside of the shape + Outside, + + /// The stroke should be painted entirely inside of the shape + Inside, + + /// The stroke should be painted right on the edge of the shape, half inside and half outside. + Middle, +} + +impl Default for StrokeKind { + fn default() -> Self { + Self::Middle + } +} + /// 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`]. @@ -63,6 +83,7 @@ impl std::hash::Hash for Stroke { pub struct PathStroke { pub width: f32, pub color: ColorMode, + pub kind: StrokeKind, } impl PathStroke { @@ -70,6 +91,7 @@ impl PathStroke { pub const NONE: Self = Self { width: 0.0, color: ColorMode::TRANSPARENT, + kind: StrokeKind::Middle, }; #[inline] @@ -77,6 +99,7 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::Solid(color.into()), + kind: StrokeKind::default(), } } @@ -91,6 +114,31 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::UV(Arc::new(callback)), + kind: StrokeKind::default(), + } + } + + /// Set the stroke to be painted right on the edge of the shape, half inside and half outside. + pub fn middle(self) -> Self { + Self { + kind: StrokeKind::Middle, + ..self + } + } + + /// Set the stroke to be painted entirely outside of the shape + pub fn outside(self) -> Self { + Self { + kind: StrokeKind::Outside, + ..self + } + } + + /// Set the stroke to be painted entirely inside of the shape + pub fn inside(self) -> Self { + Self { + kind: StrokeKind::Inside, + ..self } } @@ -116,6 +164,7 @@ impl From for PathStroke { Self { width: value.width, color: ColorMode::Solid(value.color), + kind: StrokeKind::default(), } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 6ee1a11a53d..1ef7c471880 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -303,7 +303,7 @@ mod precomputed_vertices { // ---------------------------------------------------------------------------- -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] struct PathPoint { pos: Pos2, @@ -478,23 +478,23 @@ impl Path { } /// Open-ended. - pub fn stroke_open(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { - stroke_path(feathering, &self.0, PathType::Open, stroke, out); + pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { + stroke_path(feathering, &mut self.0, PathType::Open, stroke, out); } /// A closed path (returning to the first point). - pub fn stroke_closed(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { - stroke_path(feathering, &self.0, PathType::Closed, stroke, out); + pub fn stroke_closed(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { + stroke_path(feathering, &mut self.0, PathType::Closed, stroke, out); } pub fn stroke( - &self, + &mut self, feathering: f32, path_type: PathType, stroke: &PathStroke, out: &mut Mesh, ) { - stroke_path(feathering, &self.0, path_type, stroke, out); + stroke_path(feathering, &mut self.0, path_type, stroke, out); } /// The path is taken to be closed (i.e. returning to the start again). @@ -502,8 +502,8 @@ impl Path { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. - pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) { - fill_closed_path(feathering, &mut self.0, color, out); + pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { + fill_closed_path(feathering, &mut self.0, color, stroke, out); } /// Like [`Self::fill`] but with texturing. @@ -536,8 +536,6 @@ pub mod path { let r = clamp_rounding(rounding, rect); if r == Rounding::ZERO { - let min = rect.min; - let max = rect.max; path.reserve(4); path.push(pos2(min.x, min.y)); // left top path.push(pos2(max.x, min.y)); // right top @@ -738,11 +736,31 @@ fn cw_signed_area(path: &[PathPoint]) -> f64 { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. -fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out: &mut Mesh) { +/// +/// A stroke is required so that the fill's feathering can fade to the right color. You can pass `&PathStroke::NONE` if +/// this path won't be stroked. +fn fill_closed_path( + feathering: f32, + path: &mut [PathPoint], + color: Color32, + stroke: &PathStroke, + out: &mut Mesh, +) { if color == Color32::TRANSPARENT { return; } + // TODO(juancampa): This bounding box is computed twice per shape: once here and another when tessellating the + // stroke, consider hoisting that logic to the tessellator/scratchpad. + let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) + .expand((stroke.width / 2.0) + feathering); + + let stroke_color = &stroke.color; + let get_stroke_color: Box Color32> = match stroke_color { + ColorMode::Solid(col) => Box::new(|_pos: Pos2| *col), + ColorMode::UV(fun) => Box::new(|pos: Pos2| fun(bbox, pos)), + }; + let n = path.len() as u32; if feathering > 0.0 { if cw_signed_area(path) < 0.0 { @@ -755,7 +773,6 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out out.reserve_triangles(3 * n as usize); out.reserve_vertices(2 * n as usize); - let color_outer = Color32::TRANSPARENT; let idx_inner = out.vertices.len() as u32; let idx_outer = idx_inner + 1; @@ -769,8 +786,13 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out for i1 in 0..n { let p1 = &path[i1 as usize]; let dm = 0.5 * feathering * p1.normal; - out.colored_vertex(p1.pos - dm, color); - out.colored_vertex(p1.pos + dm, color_outer); + + let pos_inner = p1.pos - dm; + let pos_outer = p1.pos + dm; + let color_outer = get_stroke_color(pos_outer); + + out.colored_vertex(pos_inner, color); + out.colored_vertex(pos_outer, color_outer); out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0); out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1); i0 = i1; @@ -872,10 +894,24 @@ fn fill_closed_path_with_uv( } } +/// Translate a point along their normals according to the stroke kind. +#[inline(always)] +fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) { + match stroke.kind { + stroke::StrokeKind::Middle => { /* Nothingn to do */ } + stroke::StrokeKind::Outside => { + p.pos += p.normal * stroke.width * 0.5; + } + stroke::StrokeKind::Inside => { + p.pos -= p.normal * stroke.width * 0.5; + } + } +} + /// Tessellate the given path as a stroke with thickness. fn stroke_path( feathering: f32, - path: &[PathPoint], + path: &mut [PathPoint], path_type: PathType, stroke: &PathStroke, out: &mut Mesh, @@ -888,6 +924,12 @@ fn stroke_path( let idx = out.vertices.len() as u32; + // Translate the points along their normals if the stroke is outside or inside + if stroke.kind != stroke::StrokeKind::Middle { + path.iter_mut() + .for_each(|p| translate_stroke_point(p, stroke)); + } + // expand the bounding box to include the thickness of the path let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) .expand((stroke.width / 2.0) + feathering); @@ -924,7 +966,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { let connect_with_previous = path_type == PathType::Closed || i1 > 0; - let p1 = &path[i1 as usize]; + let p1 = path[i1 as usize]; let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); @@ -966,7 +1008,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { - let p1 = &path[i1 as usize]; + let p1 = path[i1 as usize]; let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1011,7 +1053,7 @@ fn stroke_path( out.reserve_vertices(4 * n as usize); { - let end = &path[0]; + let end = path[0]; let p = end.pos; let n = end.normal; let back_extrude = n.rot90() * feathering; @@ -1032,7 +1074,7 @@ fn stroke_path( let mut i0 = 0; for i1 in 1..n - 1 { - let point = &path[i1 as usize]; + let point = path[i1 as usize]; let p = point.pos; let n = point.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1060,7 +1102,7 @@ fn stroke_path( { let i1 = n - 1; - let end = &path[i1 as usize]; + let end = path[i1 as usize]; let p = end.pos; let n = end.normal; let back_extrude = -n.rot90() * feathering; @@ -1227,11 +1269,20 @@ impl Tessellator { #[inline(always)] pub fn round_to_pixel(&self, point: f32) -> f32 { - if self.options.round_text_to_pixels { - (point * self.pixels_per_point).round() / self.pixels_per_point - } else { - point - } + (point * self.pixels_per_point).round() / self.pixels_per_point + } + + #[inline(always)] + pub fn round_to_pixel_center(&self, point: f32) -> f32 { + ((point * self.pixels_per_point - 0.5).round() + 0.5) / self.pixels_per_point + } + + #[inline(always)] + pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { + pos2( + self.round_to_pixel_center(pos.x), + self.round_to_pixel_center(pos.y), + ) } /// Tessellate a clipped shape into a list of primitives. @@ -1404,11 +1455,13 @@ impl Tessellator { } } + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); - self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(self.feathering, &stroke.into(), out); + .fill(self.feathering, fill, &path_stroke, out); + self.scratchpad_path + .stroke_closed(self.feathering, &path_stroke, out); } /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. @@ -1471,11 +1524,13 @@ impl Tessellator { points.push(center + Vec2::new(0.0, -radius.y)); points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y))); + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); - self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(self.feathering, &stroke.into(), out); + .fill(self.feathering, fill, &path_stroke, out); + self.scratchpad_path + .stroke_closed(self.feathering, &path_stroke, out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1562,7 +1617,8 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(self.feathering, *fill, out); + self.scratchpad_path + .fill(self.feathering, *fill, stroke, out); } let typ = if *closed { PathType::Closed @@ -1650,7 +1706,7 @@ impl Tessellator { path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); path.add_line_loop(&self.scratchpad_points); - + let path_stroke = PathStroke::from(stroke).outside(); if uv.is_positive() { // Textured let uv_from_pos = |p: Pos2| { @@ -1662,10 +1718,9 @@ impl Tessellator { path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); } else { // Untextured - path.fill(self.feathering, fill, out); + path.fill(self.feathering, fill, &path_stroke, out); } - - path.stroke_closed(self.feathering, &stroke.into(), out); + path.stroke_closed(self.feathering, &path_stroke, out); } self.feathering = old_feathering; // restore @@ -1701,12 +1756,16 @@ impl Tessellator { out.vertices.reserve(galley.num_vertices); out.indices.reserve(galley.num_indices); - // The contents of the galley is already snapped to pixel coordinates, + // The contents of the galley are already snapped to pixel coordinates, // but we need to make sure the galley ends up on the start of a physical pixel: - let galley_pos = pos2( - self.round_to_pixel(galley_pos.x), - self.round_to_pixel(galley_pos.y), - ); + let galley_pos = if self.options.round_text_to_pixels { + pos2( + self.round_to_pixel(galley_pos.x), + self.round_to_pixel(galley_pos.y), + ) + } else { + *galley_pos + }; let uv_normalizer = vec2( 1.0 / self.font_tex_size[0] as f32, @@ -1782,13 +1841,12 @@ impl Tessellator { if *underline != Stroke::NONE { self.scratchpad_path.clear(); + self.scratchpad_path.add_line_segment([ + self.round_pos_to_pixel_center(row_rect.left_bottom()), + self.round_pos_to_pixel_center(row_rect.right_bottom()), + ]); self.scratchpad_path - .add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]); - self.scratchpad_path.stroke_open( - self.feathering, - &PathStroke::from(*underline), - out, - ); + .stroke_open(0.0, &PathStroke::from(*underline), out); } } } @@ -1872,7 +1930,8 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(self.feathering, fill, out); + self.scratchpad_path + .fill(self.feathering, fill, stroke, out); } let typ = if closed { PathType::Closed