Fix blurry lines (#4943)
<!-- 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 test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * 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! --> * Closes <https://github.com/emilk/egui/issues/4776> * [x] I have followed the instructions in the PR template I've been meaning to look into this for a while but finally bit the bullet this week. Contrary to what I initially thought, the problem of blurry lines is unrelated to feathering because it also happens with feathering disabled. The root cause is that lines tend to land on pixel boundaries, and because of that, frequently used strokes (e.g. 1pt), end up partially covering pixels. This is especially noticeable on 1ppp displays. There were a couple of things to fix, namely: individual lines like separators and indents but also shape strokes (e.g. Frame). Lines were easy, I just made sure we round them to the nearest pixel _center_, instead of the nearest pixel boundary. Strokes were a little more complicated. To illustrate why, here’s an example: if we're rendering a 5x5 rect (black fill, red stroke), we would expect to see something like this:  The fill and the stroke to cover entire pixels. Instead, egui was painting the stroke partially inside and partially outside, centered around the shape’s path (blue line):  Both methods are valid for different use-cases but the first one is what we’d typically want for UIs to feel crisp and pixel perfect. It's also how CSS borders work (related to #4019 and #3284). Luckily, we can use the normal computed for each `PathPoint` to adjust the location of the stroke to be outside, inside, or in the middle. These also are the 3 types of strokes available in tools like Photoshop. This PR introduces an enum `StrokeKind` which determines if a `PathStroke` should be tessellated outside, inside, or _on_ the path itself. Where "outside" is defined by the directions normals point to. Tessellator will now use `StrokeKind::Outside` for closed shapes like rect, ellipse, etc. And `StrokeKind::Middle` for the rest since there's no meaningful "outside" concept for open paths. This PR doesn't expose `StrokeKind` to user-land, but we can implement that later so that users can render shapes and decide where to place the stroke. ### Strokes test (blue lines represent the size of the rect being rendered) `Stroke::Middle` (current behavior, 1px and 3px are blurry)  `Stroke::Outside` (proposed default behavior for closed paths)  `Stroke::Inside` (for completeness but unused at the moment)  ### Demo App The best way to review this PR is to run the demo on a 1ppp display, especially to test hover effects. Everything should look crisper. Also run it in a higher dpi screen to test that nothing broke 🙏. Before:  After (notice the sharper lines): 
This commit is contained in:
parent
3777b8d274
commit
f2815b423e
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<Stroke> for PathStroke {
|
|||
Self {
|
||||
width: value.width,
|
||||
color: ColorMode::Solid(value.color),
|
||||
kind: StrokeKind::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<Vec<Pos2>>())
|
||||
.expand((stroke.width / 2.0) + feathering);
|
||||
|
||||
let stroke_color = &stroke.color;
|
||||
let get_stroke_color: Box<dyn Fn(Pos2) -> 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::<Vec<Pos2>>())
|
||||
.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
|
||||
|
|
|
|||
Loading…
Reference in New Issue