Make all lines and rectangles crisp (#5518)

* Merge this first: https://github.com/emilk/egui/pull/5517

This aligns all rectangles and (horizontal or vertical) line segments to
the physical pixel grid in the `epaint::Tessellator`, making these
shapes appear crisp everywhere.

* Closes https://github.com/emilk/egui/issues/5164
* Closes https://github.com/emilk/egui/issues/3667

This undoes a lot of the explicit, egui-side aligning added in:
* https://github.com/emilk/egui/pull/4943

The new approach has several benefits over the old one:

* It is done automatically by epaint, so it is applied to everything (no
longer opt-in)
* It is applied after any layer transforms (so it always works)
* It makes line segments crisper on high-DPI screens
* All filled rectangles now has sides that end on pixel boundaries
This commit is contained in:
Emil Ernerfeldt 2024-12-26 21:02:27 +01:00 committed by GitHub
parent dfcc679d5a
commit d20f93e9bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 314 additions and 251 deletions

View File

@ -78,6 +78,13 @@ impl Side {
Self::Right => rect.right(),
}
}
fn sign(self) -> f32 {
match self {
Self::Left => -1.0,
Self::Right => 1.0,
}
}
}
/// A panel that covers the entire left or right side of a [`Ui`] or screen.
@ -349,12 +356,8 @@ impl SidePanel {
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
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 };
// Make sure the line is on the inside of the panel:
let resize_x = resize_x + 0.5 * side.sign() * stroke.width;
ui.painter().vline(resize_x, panel_rect.y_range(), stroke);
}
@ -562,6 +565,13 @@ impl TopBottomSide {
Self::Bottom => rect.bottom(),
}
}
fn sign(self) -> f32 {
match self {
Self::Top => -1.0,
Self::Bottom => 1.0,
}
}
}
/// A panel that covers the entire top or bottom of a [`Ui`] or screen.
@ -843,12 +853,8 @@ impl TopBottomPanel {
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
let resize_y = side.opposite().side_y(rect);
// This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc)
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 };
// Make sure the line is on the inside of the panel:
let resize_y = resize_y + 0.5 * side.sign() * stroke.width;
ui.painter().hline(panel_rect.x_range(), resize_y, stroke);
}

View File

@ -400,7 +400,9 @@ pub fn paint_resize_corner_with_style(
corner: Align2,
) {
let painter = ui.painter();
let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect));
let cp = corner
.pos_in_rect(rect)
.round_to_pixels(ui.pixels_per_point());
let mut w = 2.0;
let stroke = Stroke {
width: 1.0, // Set width to 1.0 to prevent overlapping

View File

@ -596,7 +596,7 @@ impl<'open> Window<'open> {
},
);
title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect);
title_rect = title_rect.round_to_pixels(area_content_ui.pixels_per_point());
if on_top && area_content_ui.visuals().window_highlight_topmost {
let mut round = window_frame.rounding;

View File

@ -7,7 +7,6 @@ use emath::GuiRounding as _;
use epaint::{
emath::{self, TSTransform},
mutex::RwLock,
pos2,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
@ -2004,50 +2003,6 @@ impl Context {
});
}
/// 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 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 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 of filled shapes
#[inline]
pub(crate) fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
Rect {
min: self.round_pos_to_pixels(rect.min),
max: self.round_pos_to_pixels(rect.max),
}
}
/// Allocate a texture.
///
/// This is for advanced users.

View File

@ -144,6 +144,8 @@ impl Widget for &mut epaint::TessellationOptions {
coarse_tessellation_culling,
prerasterized_discs,
round_text_to_pixels,
round_line_segments_to_pixels,
round_rects_to_pixels,
debug_paint_clip_rects,
debug_paint_text_rects,
debug_ignore_clip_rects,
@ -179,13 +181,22 @@ impl Widget for &mut epaint::TessellationOptions {
ui.checkbox(validate_meshes, "Validate meshes").on_hover_text("Check that incoming meshes are valid, i.e. that all indices are in range, etc.");
ui.collapsing("Align to pixel grid", |ui| {
ui.checkbox(round_text_to_pixels, "Text")
.on_hover_text("Most text already is, so don't expect to see a large change.");
ui.checkbox(round_line_segments_to_pixels, "Line segments")
.on_hover_text("Makes line segments appear crisp on any display.");
ui.checkbox(round_rects_to_pixels, "Rectangles")
.on_hover_text("Makes line segments appear crisp on any display.");
});
ui.collapsing("Debug", |ui| {
ui.checkbox(
coarse_tessellation_culling,
"Do coarse culling in the tessellator",
);
ui.checkbox(round_text_to_pixels, "Align text positions to pixel grid")
.on_hover_text("Most text already is, so don't expect to see a large change.");
ui.checkbox(debug_ignore_clip_rects, "Ignore clip rectangles");
ui.checkbox(debug_paint_clip_rects, "Paint clip rectangles");

View File

@ -1,23 +1,30 @@
use std::sync::Arc;
use emath::GuiRounding as _;
use epaint::{
text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke,
};
use crate::{
emath::{Align2, Pos2, Rangef, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, Context, FontId,
};
use epaint::{
text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke,
};
/// Helper to paint shapes and text to a specific region on a specific layer.
///
/// All coordinates are screen coordinates in the unit points (one point can consist of many physical pixels).
///
/// A [`Painter`] never outlive a single frame/pass.
#[derive(Clone)]
pub struct Painter {
/// Source of fonts and destination of shapes
ctx: Context,
/// For quick access, without having to go via [`Context`].
pixels_per_point: f32,
/// Where we paint
layer_id: LayerId,
@ -38,8 +45,10 @@ pub struct Painter {
impl Painter {
/// Create a painter to a specific layer within a certain clip rectangle.
pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self {
let pixels_per_point = ctx.pixels_per_point();
Self {
ctx,
pixels_per_point,
layer_id,
clip_rect,
fade_to_color: None,
@ -49,14 +58,10 @@ impl Painter {
/// Redirect where you are painting.
#[must_use]
pub fn with_layer_id(self, layer_id: LayerId) -> Self {
Self {
ctx: self.ctx,
layer_id,
clip_rect: self.clip_rect,
fade_to_color: None,
opacity_factor: 1.0,
}
#[inline]
pub fn with_layer_id(mut self, layer_id: LayerId) -> Self {
self.layer_id = layer_id;
self
}
/// Create a painter for a sub-region of this [`Painter`].
@ -64,13 +69,9 @@ impl Painter {
/// The clip-rect of the returned [`Painter`] will be the intersection
/// of the given rectangle and the `clip_rect()` of the parent [`Painter`].
pub fn with_clip_rect(&self, rect: Rect) -> Self {
Self {
ctx: self.ctx.clone(),
layer_id: self.layer_id,
clip_rect: rect.intersect(self.clip_rect),
fade_to_color: self.fade_to_color,
opacity_factor: self.opacity_factor,
}
let mut new_self = self.clone();
new_self.clip_rect = rect.intersect(self.clip_rect);
new_self
}
/// Redirect where you are painting.
@ -82,7 +83,7 @@ impl Painter {
}
/// If set, colors will be modified to look like this
pub(crate) fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
self.fade_to_color = fade_to_color;
}
@ -118,24 +119,27 @@ impl Painter {
/// If `false`, nothing you paint will show up.
///
/// Also checks [`Context::will_discard`].
pub(crate) fn is_visible(&self) -> bool {
pub fn is_visible(&self) -> bool {
self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
}
/// If `false`, nothing added to the painter will be visible
pub(crate) fn set_invisible(&mut self) {
pub fn set_invisible(&mut self) {
self.fade_to_color = Some(Color32::TRANSPARENT);
}
}
/// ## Accessors etc
impl Painter {
/// Get a reference to the parent [`Context`].
#[inline]
pub fn ctx(&self) -> &Context {
&self.ctx
}
/// Number of physical pixels for each logical UI point.
#[inline]
pub fn pixels_per_point(&self) -> f32 {
self.pixels_per_point
}
/// Read-only access to the shared [`Fonts`].
///
/// See [`Context`] documentation for how locks work.
@ -180,37 +184,42 @@ impl Painter {
/// 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)
point.round_to_pixel_center(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels).
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
self.ctx().round_pos_to_pixel_center(pos)
pos.round_to_pixel_center(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering of filled shapes.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_to_pixel(&self, point: f32) -> f32 {
self.ctx().round_to_pixel(point)
point.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
self.ctx().round_vec_to_pixels(vec)
vec.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
self.ctx().round_pos_to_pixels(pos)
pos.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
self.ctx().round_rect_to_pixels(rect)
rect.round_to_pixels(self.pixels_per_point())
}
}
@ -337,7 +346,7 @@ impl Painter {
/// # Paint different primitives
impl Painter {
/// Paints a line from the first point to the second.
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<PathStroke>) -> ShapeIdx {
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::LineSegment {
points,
stroke: stroke.into(),
@ -351,13 +360,13 @@ impl Painter {
}
/// Paints a horizontal line.
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> ShapeIdx {
self.add(Shape::hline(x, y, stroke.into()))
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::hline(x, y, stroke))
}
/// Paints a vertical line.
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> ShapeIdx {
self.add(Shape::vline(x, y, stroke.into()))
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::vline(x, y, stroke))
}
pub fn circle(
@ -398,6 +407,7 @@ impl Painter {
})
}
/// The stroke extends _outside_ the [`Rect`].
pub fn rect(
&self,
rect: Rect,
@ -417,6 +427,7 @@ impl Painter {
self.add(RectShape::filled(rect, rounding, fill_color))
}
/// The stroke extends _outside_ the [`Rect`].
pub fn rect_stroke(
&self,
rect: Rect,

View File

@ -2485,12 +2485,8 @@ impl Widget for &mut Stroke {
// stroke preview:
let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size);
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());
let left = stroke_rect.left_center();
let right = stroke_rect.right_center();
ui.painter().line_segment([left, right], (*width, *color));
})
.response

View File

@ -96,19 +96,8 @@ pub fn paint_text_selection(
pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
let stroke = visuals.text_cursor.stroke;
// Ensure the cursor is aligned to the pixel grid for whole number widths.
// See https://github.com/emilk/egui/issues/5164
let (top, bottom) = if (stroke.width as usize) % 2 == 0 {
(
painter.round_pos_to_pixels(cursor_rect.center_top()),
painter.round_pos_to_pixels(cursor_rect.center_bottom()),
)
} else {
(
painter.round_pos_to_pixel_center(cursor_rect.center_top()),
painter.round_pos_to_pixel_center(cursor_rect.center_bottom()),
)
};
let top = cursor_rect.center_top();
let bottom = cursor_rect.center_bottom();
painter.line_segment([top, bottom], (stroke.width, stroke.color));

View File

@ -483,6 +483,12 @@ impl Ui {
&self.painter
}
/// Number of physical pixels for each logical UI point.
#[inline]
pub fn pixels_per_point(&self) -> f32 {
self.painter.pixels_per_point()
}
/// If `false`, the [`Ui`] does not allow any interaction and
/// the widgets in it will draw with a gray look.
#[inline]
@ -2385,9 +2391,7 @@ 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_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_pixel_center(left_bottom);
if left_vline {
// draw a faint line on the left to mark the indented section

View File

@ -116,12 +116,12 @@ impl Widget for Separator {
if is_horizontal_line {
painter.hline(
(rect.left() - grow)..=(rect.right() + grow),
painter.round_to_pixel_center(rect.center().y),
rect.center().y,
stroke,
);
} else {
painter.vline(
painter.round_to_pixel_center(rect.center().x),
rect.center().x,
(rect.top() - grow)..=(rect.bottom() + grow),
stroke,
);

View File

@ -1,9 +1,9 @@
use std::collections::HashMap;
use egui::{
epaint, lerp, pos2, vec2, widgets::color_picker::show_color, Align2, Color32, FontId, Image,
Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape, Stroke, TextureHandle,
TextureOptions, Ui, Vec2,
emath::GuiRounding, epaint, lerp, pos2, vec2, widgets::color_picker::show_color, Align2,
Color32, FontId, Image, Mesh, Pos2, Rect, Response, Rgba, RichText, Sense, Shape, Stroke,
TextureHandle, TextureOptions, Ui, Vec2,
};
const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0);
@ -270,6 +270,7 @@ impl ColorTest {
fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Response {
let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover());
let rect = rect.round_to_pixels(ui.pixels_per_point());
if bg_fill != Default::default() {
let mut mesh = Mesh::default();
mesh.add_colored_rect(rect, bg_fill);

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4a725aa81433f301fda4ff8a28be869366332964995d1ae4ed996591596eb7e2
size 31461
oid sha256:cf83bead834ec8f88d74b32ae6331715e8c6df183e007e2a16004c019534a30f
size 31810

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36028d85f49ee77562250214237def2b676ecc9ed413d2fd8afc473d61289ca1
size 32761
oid sha256:b8ca5a27491c0589a97e43a70bc10dc52778d25ca3f7e7c895dbbbb784adfcfa
size 33245

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f93cbf4968e69e2b51256631ab8703d1af2274147924f8d802d9da0f9767aa1b
size 81162
oid sha256:e640606207265b4f040f793b0ffb989504b6a98b89e95e77a9a9d3e3abc9327a
size 80933

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7157cc3a1c87b63a1af41e9c3493beb426f9a2c7e3ce1adf228a8d1aee23818
size 11549
oid sha256:332c2af36873d8ccccb36c08fd2e475dc1f18454a3090a851c0889395d4f364f
size 11518

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7fcc5ac195fdf455836cce7503c530d56a5c9e053cf743f8165c2b33212bfaae
size 20645
oid sha256:d0d0b1b4d2c4b624904250bc8d6870559f0179e3f7f2d6dc4a4ff256df356237
size 20626

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eee96c06b54b0c9779a84bce1caf94f907593d675625a51791b18925881bd619
size 20883
oid sha256:d5fe6166bb8cd5fae0899957e968312b9229a79e423a5d154eda483a149b264d
size 20831

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cced772c3830cf1d8d5f946cb457f1a0a059ac3b08b791899db5f5fe8132fc85
size 10711
oid sha256:b71e1d109f90e017644dd20b9d84d81e3a6d5195afbd01ba86c85fa248c8b5c5
size 10703

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cce41bc2887462a0a3aebc4148faf769d3666ff4a0e4c7db6ffbddb174be0ed3
size 135734
oid sha256:e3c9ba9064f44a4a14405f53316c1c602184caf16cb584d7c1f1912fe59f85ab
size 135712

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:194c07bce8370b886b1bcb58936044e71f4725b2034dff3bbb7e3f710cf82f17
size 24701
oid sha256:097bd512dd71c17370f6e374312c19e7ab6845f151b3c3565f2a0790b61ee7ba
size 24413

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1b5a71ea8cd6b3e2b267d668c1f149689e938eef3b4038a0935823d37aba348f
size 17806
oid sha256:2bdf54573a6b0d2fedd90314f91dd7de22dd13709e8dd699b37ef8935b6adda5
size 17785

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9c8395e6b4287b92d85a52ca2d47750f67abeb0ad88c6b42264bfe2e62fd09d
size 22283
oid sha256:d6328c86b70f3e24aaf87db200d13dfd0baa787dd8431e47621457937f8f5021
size 22552

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17901f3e235cfbac03fb2334f36f614ed152a25b3a212e78beba9fb342039844
size 65339
oid sha256:afb57dc0bb8ff839138e36b8780136e0c8da55ff380832538fae0394143807c0
size 65321

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:026723cb5d89b32386a849328c34420ee9e3ae1f97cbf6fa3c4543141123549e
size 32890
oid sha256:403fc1d08d79168bc9f7a08ac7f363f2df916547f08311838edfda8a499a9b2d
size 32879

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:950c825c8f6eadc682c57fe8843ded2ceb6981b6d4086d427dcf0d27740df8ef
size 36795
oid sha256:cbd490a15377bdd4fd3272f3cd126cbc4fb9ed2f6f84edcbba03dd30dc3f3d99
size 36780

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e68f145f57812a1f6a81e1a8ab41bcb134320dcaaee3f462d1aa00d6c331752a
size 17579
oid sha256:c31b3998817e345b12b037d7f8bec7641f77d0c7eab7da9a746b7b51c9efc8fb
size 17531

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6aec5a6291ad2796457d1c4fc44228c75bf075262aa672749a3cceb4baef1e08
size 25271
oid sha256:4e8963c3ecd0e74fe9641d87101742a0d45c82a582d70e30eb36bc835f5aac06
size 25330

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5068df8549ffc91028addfec6f851f12a4de80e208b50b39e4d44b6aa2c7240e
size 261946
oid sha256:88b3a50b481942630b5804c60227f23b719dc7e3eb6dbe432c2448cb69332def
size 262141

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:579a7a66f86ade628e9f469b0014e9010aa56312ad5bd1e8de2faaae7e0d1af6
size 23770
oid sha256:4d5d628b54b28eccac0d9ef21bbdabace0cdf507898707956943e2259df846ca
size 23741

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c50f7cbac65ff98b4bdb11034dd18bf19836ea22f27e0ed04415ac763d86d09
size 188067
oid sha256:8763a8f8b4242cbe589cd3282dc5f4b32a84b4e0416fb8072dfefb7879a5d4f6
size 187982

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e3436906f7ac459b7f4330a286937722e78ad885ae1e90f75be566e970a8ca7
size 116899
oid sha256:3d61088bf1f992467e8396ac89407fa158d6f44e2d2d196778247f3ff18cb893
size 119759

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:764fc4f5fc60cc72d9731c7073e05abda70c1b5caf2992aabad63772e0cd5e33
size 25905
oid sha256:79ebaf9cccd19da2923507b5a553a23edc67382ef59e4b71f01d3bd6cc913539
size 25829

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cce44cee74da69ce065e79f92919928bbdca6b1e57b44f0165666508b3cddce3
size 72240
oid sha256:06cbf13841e6ac5fbc57bdae6f5ad9d19718193c344420dedcc0e9d7ed2b8ba9
size 71590

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:10d278417ae66a788a88676874381b2d7c026b3130ca1c9178e2027595bb8e74
size 67383
oid sha256:35e66f211c0b30a684371b520c46dbe4f9d5b6835e053a4eb65f492dd66a9e6c
size 67288

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f13c687e27870df62488563de07b02711eb508ec7eee91470a44f2669610889
size 21342
oid sha256:fa9ee8631bfe433ee6fad1fb1651fd6b63e2fb3fbc5f321a5410f7266dc16d09
size 21296

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:814d863deaa4fa029044da1783db87744f0d82e874edd6cbab16e712ed8715aa
size 59881
oid sha256:6475702b1bf2c65fb5196928a8934ade647a6053d6902a836e3d69cb7920091e
size 59874

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65674a0da6ee80eff735a6404ad328332ad0ab48766de9b1d358e38ccb3ecc29
size 13059
oid sha256:9d8daaec0c58709298a4594b7b2aa935aa2de327e6b71bd7417c2ba3a6eb060c
size 13020

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:450ede959fac122e6b045d18a6ab62952fa03371508704a1f2303d6379e41593
size 35523
oid sha256:29d8d859a8cb11e4b390d45c658ed8ff191c2e542909e12f2385c0cba62baa2d
size 35109

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd53d25bcecc4f22cb9226237d69557696e8aa08b969157c0cdb015cfacf206d
size 48244
oid sha256:17217e600d8a85ec00ecb11f0e5fe69496b30bbf715cc86785cec6e18b8c2fa1
size 48158

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9d9226996b0eecf516b9b89f09c57e5f7ad43cc154a2c8937779421a6580113
size 48157
oid sha256:fac50d2327a9e5e99989dd81d5644db86031b91b9c5c68fc17b5ef53ae655048
size 47970

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:49401fcd6e348c75d81ad7419e1fa1a77500ca3d2360b418e8304ad5843e5b3a
size 44080
oid sha256:e5e829257b742194742429be0efd326be17ff7f5b9b16f9df58df21e899320bd
size 43963

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6cc1363890eb878ec7f37bf5350dd5a81ed39e16655b2e8b7fee0937e605bcae
size 44044
oid sha256:5da064332c669a860a92a34b101f23e28026d4f07948f7c3e9a40e611f5e284f
size 43986

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc83a67ac7ff574f981dae02c05ecfba934df4fb181bba12fd7607ef9cba5aa6
size 523650
oid sha256:ed2a356452d792e32bea57f044da9d86da27fd8504826dd6b87618a53519ea6a
size 522556

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a0fe7626d08a4d28a66d39824cdcd8fef140c7c818e5fc07a7e86756c249875d
size 737802
oid sha256:0d04a5854528c6141f7def6f9229c51c6d2d4c87e2f656be4d149e7b2b852976
size 729056

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:477437c5c8766beac88a96f531d8dfe5fac1fe2cdd4bad02fbc53610eb8925fb
size 887154
oid sha256:e6e5c1a745e357faa7b98f7a2cd1ca139c4a14be154b9d21feb8030933acfdb7
size 867552

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8aa0503d6e5dc82953000c7e68be826b0bf4f288aef82c2d5d832189538eb3b2
size 1002104
oid sha256:7d9f4a37541fd1a0754c1cb1f3a2d4a76f03d67ca4e5596c8e6982d691d29dea
size 980286

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bef9f6dde686733e04a447be280fd50704db163d330fd8c663357b057cd50e27
size 1080661
oid sha256:4d1c99867202e16500146b7146a32fd83d70d60f5ac94aae4ca405a6377e4625
size 1066559

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:136c60283286df17e6751220e0c849028bc1379910995cf9e4930c5d63d0a90f
size 1235057
oid sha256:08fc1c89fd2d04aa12c75a1829dacfff090e322c65ad969648799833e1b072eb
size 1235574

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9da93a7c27e17d370a3df73930d71694d436c300656b054722646ea57db45810
size 160803
oid sha256:d122b1a995e691b5049c57d65c9f222a5f1639b1e4f6f96f91823444339693cc
size 160540

View File

@ -116,6 +116,7 @@ impl GuiRounding for crate::Vec2 {
)
}
// This doesn't really make sense for a Vec2, but 🤷‍♂️
#[inline]
fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
Self::new(

View File

@ -53,7 +53,7 @@ pub use self::{
Rounding, Shape, TextShape,
},
stats::PaintStats,
stroke::{PathStroke, Stroke},
stroke::{PathStroke, Stroke, StrokeKind},
tessellator::{TessellationOptions, Tessellator},
text::{FontFamily, FontId, Fonts, Galley},
texture_atlas::TextureAtlas,

View File

@ -35,10 +35,7 @@ pub enum Shape {
Ellipse(EllipseShape),
/// A line between two points.
LineSegment {
points: [Pos2; 2],
stroke: PathStroke,
},
LineSegment { points: [Pos2; 2], stroke: Stroke },
/// A series of lines between points.
/// The path can have a stroke and/or fill (if closed).
@ -92,7 +89,7 @@ impl Shape {
/// A line between two points.
/// More efficient than calling [`Self::line`].
#[inline]
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<PathStroke>) -> Self {
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<Stroke>) -> Self {
Self::LineSegment {
points,
stroke: stroke.into(),
@ -100,7 +97,7 @@ impl Shape {
}
/// A horizontal line.
pub fn hline(x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> Self {
pub fn hline(x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> Self {
let x = x.into();
Self::LineSegment {
points: [pos2(x.min, y), pos2(x.max, y)],
@ -109,7 +106,7 @@ impl Shape {
}
/// A vertical line.
pub fn vline(x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> Self {
pub fn vline(x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> Self {
let y = y.into();
Self::LineSegment {
points: [pos2(x, y.min), pos2(x, y.max)],
@ -262,6 +259,7 @@ impl Shape {
Self::Rect(RectShape::filled(rect, rounding, fill_color))
}
/// The stroke extends _outside_ the [`Rect`].
#[inline]
pub fn rect_stroke(
rect: Rect,
@ -670,6 +668,11 @@ pub struct RectShape {
pub fill: Color32,
/// The thickness and color of the outline.
///
/// The stroke extends _outside_ the edge of [`Self::rect`],
/// i.e. using [`crate::StrokeKind::Outside`].
///
/// This means the [`Self::visual_bounding_rect`] is `rect.size() + 2.0 * stroke.width`.
pub stroke: Stroke,
/// If larger than zero, the edges of the rectangle
@ -695,6 +698,7 @@ pub struct RectShape {
}
impl RectShape {
/// The stroke extends _outside_ the [`Rect`].
#[inline]
pub fn new(
rect: Rect,
@ -730,6 +734,7 @@ impl RectShape {
}
}
/// The stroke extends _outside_ the [`Rect`].
#[inline]
pub fn stroke(rect: Rect, rounding: impl Into<Rounding>, stroke: impl Into<Stroke>) -> Self {
Self {
@ -761,8 +766,8 @@ impl RectShape {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
self.rect
.expand((self.stroke.width + self.blur_width) / 2.0)
let Stroke { width, .. } = self.stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke`
self.rect.expand(width + self.blur_width / 2.0)
}
}
}

View File

@ -21,7 +21,7 @@ pub fn adjust_colors(
}
Shape::LineSegment { stroke, points: _ } => {
adjust_color_mode(&mut stroke.color, adjust_color);
adjust_color(&mut stroke.color);
}
Shape::Path(PathShape {

View File

@ -11,7 +11,7 @@ use crate::{
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Rounding, Shape,
Stroke, TextShape, TextureId, Vertex, WHITE_UV,
};
use emath::{pos2, remap, vec2, NumExt, Pos2, Rect, Rot2, Vec2};
use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2};
use self::color::ColorMode;
use self::stroke::PathStroke;
@ -671,10 +671,21 @@ pub struct TessellationOptions {
/// from the font atlas.
pub prerasterized_discs: bool,
/// If `true` (default) align text to mesh grid.
/// If `true` (default) align text to the physical pixel grid.
/// This makes the text sharper on most platforms.
pub round_text_to_pixels: bool,
/// If `true` (default), align right-angled line segments to the physical pixel grid.
///
/// This makes the line segments appear crisp on any display.
pub round_line_segments_to_pixels: bool,
/// If `true` (default), align rectangles to the physical pixel grid.
///
/// This makes the rectangle strokes more crisp,
/// and makes filled rectangles tile perfectly (without feathering).
pub round_rects_to_pixels: bool,
/// Output the clip rectangles to be painted.
pub debug_paint_clip_rects: bool,
@ -708,6 +719,8 @@ impl Default for TessellationOptions {
coarse_tessellation_culling: true,
prerasterized_discs: true,
round_text_to_pixels: true,
round_line_segments_to_pixels: true,
round_rects_to_pixels: true,
debug_paint_text_rects: false,
debug_paint_clip_rects: false,
debug_ignore_clip_rects: false,
@ -754,8 +767,11 @@ fn fill_closed_path(
// 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 bbox = if matches!(stroke.color, ColorMode::UV(_)) {
Rect::from_points(&path.iter().map(|p| p.pos).collect::<Vec<Pos2>>()).expand(feathering)
} else {
Rect::NAN
};
let stroke_color = &stroke.color;
let get_stroke_color: Box<dyn Fn(Pos2) -> Color32> = match stroke_color {
@ -900,7 +916,7 @@ fn fill_closed_path_with_uv(
#[inline(always)]
fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) {
match stroke.kind {
stroke::StrokeKind::Middle => { /* Nothingn to do */ }
stroke::StrokeKind::Middle => { /* Nothing to do */ }
stroke::StrokeKind::Outside => {
p.pos += p.normal * stroke.width * 0.5;
}
@ -932,9 +948,13 @@ fn stroke_path(
.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);
// Expand the bounding box to include the thickness of the path
let bbox = if matches!(stroke.color, ColorMode::UV(_)) {
Rect::from_points(&path.iter().map(|p| p.pos).collect::<Vec<Pos2>>())
.expand((stroke.width / 2.0) + feathering)
} else {
Rect::NAN
};
let get_color = |col: &ColorMode, pos: Pos2| match col {
ColorMode::Solid(col) => *col,
@ -1386,7 +1406,9 @@ impl Tessellator {
out.append(mesh);
}
Shape::LineSegment { points, stroke } => self.tessellate_line(points, stroke, out),
Shape::LineSegment { points, stroke } => {
self.tessellate_line_segment(points, stroke, out);
}
Shape::Path(path_shape) => {
self.tessellate_path(&path_shape, out);
}
@ -1563,10 +1585,10 @@ impl Tessellator {
///
/// * `shape`: the mesh to tessellate.
/// * `out`: triangles are appended to this.
pub fn tessellate_line(
pub fn tessellate_line_segment(
&mut self,
points: [Pos2; 2],
stroke: impl Into<PathStroke>,
mut points: [Pos2; 2],
stroke: impl Into<Stroke>,
out: &mut Mesh,
) {
let stroke = stroke.into();
@ -1582,10 +1604,38 @@ impl Tessellator {
return;
}
if self.options.round_line_segments_to_pixels {
let [a, b] = &mut points;
if a.x == b.x {
// Vertical line
let mut x = a.x;
round_line_segment(&mut x, &stroke, self.pixels_per_point);
a.x = x;
b.x = x;
}
if a.y == b.y {
// Horizontal line
let mut y = a.y;
round_line_segment(&mut y, &stroke, self.pixels_per_point);
a.y = y;
b.y = y;
}
}
self.scratchpad_path.clear();
self.scratchpad_path.add_line_segment(points);
self.scratchpad_path
.stroke_open(self.feathering, &stroke, out);
.stroke_open(self.feathering, &stroke.into(), out);
}
#[deprecated = "Use `tessellate_line_segment` instead"]
pub fn tessellate_line(
&mut self,
points: [Pos2; 2],
stroke: impl Into<Stroke>,
out: &mut Mesh,
) {
self.tessellate_line_segment(points, stroke, out);
}
/// Tessellate a single [`PathShape`] into a [`Mesh`].
@ -1660,6 +1710,14 @@ impl Tessellator {
return;
}
if self.options.round_rects_to_pixels {
// Since the stroke extends outside of the rectangle,
// we can round the rectangle sides to the physical pixel edges,
// and the filled rect will appear crisp, as will the inside of the stroke.
let Stroke { .. } = stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke`
rect = rect.round_to_pixels(self.pixels_per_point);
}
// It is common to (sometimes accidentally) create an infinitely sized rectangle.
// Make sure we can handle that:
rect.min = rect.min.at_least(pos2(-1e7, -1e7));
@ -1688,46 +1746,33 @@ impl Tessellator {
self.feathering = self.feathering.max(blur_width);
}
if rect.width() < self.feathering {
if rect.width() < 0.5 * self.feathering {
// Very thin - approximate by a vertical line-segment:
let line = [rect.center_top(), rect.center_bottom()];
if fill != Color32::TRANSPARENT {
self.tessellate_line(line, Stroke::new(rect.width(), fill), out);
self.tessellate_line_segment(line, Stroke::new(rect.width(), fill), out);
}
if !stroke.is_empty() {
self.tessellate_line(line, stroke, out); // back…
self.tessellate_line(line, stroke, out); // …and forth
self.tessellate_line_segment(line, stroke, out); // back…
self.tessellate_line_segment(line, stroke, out); // …and forth
}
} else if rect.height() < self.feathering {
} else if rect.height() < 0.5 * self.feathering {
// Very thin - approximate by a horizontal line-segment:
let line = [rect.left_center(), rect.right_center()];
if fill != Color32::TRANSPARENT {
self.tessellate_line(line, Stroke::new(rect.height(), fill), out);
self.tessellate_line_segment(line, Stroke::new(rect.height(), fill), out);
}
if !stroke.is_empty() {
self.tessellate_line(line, stroke, out); // back…
self.tessellate_line(line, stroke, out); // …and forth
self.tessellate_line_segment(line, stroke, out); // back…
self.tessellate_line_segment(line, stroke, out); // …and forth
}
} else {
let rect = if !stroke.is_empty() && stroke.width < self.feathering {
// Very thin rectangle strokes create extreme aliasing when they move around.
// We can fix that by rounding the rectangle corners to pixel centers.
// TODO(#5164): maybe do this for all shapes and stroke sizes
// TODO(emilk): since we use StrokeKind::Outside, we should probably round the
// corners after offsetting them with half the stroke width (see `translate_stroke_point`).
Rect {
min: self.round_pos_to_pixel_center(rect.min),
max: self.round_pos_to_pixel_center(rect.max),
}
} else {
rect
};
let path = &mut self.scratchpad_path;
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| {
@ -1741,6 +1786,7 @@ impl Tessellator {
// Untextured
path.fill(self.feathering, fill, &path_stroke, out);
}
path.stroke_closed(self.feathering, &path_stroke, out);
}
@ -1968,6 +2014,45 @@ impl Tessellator {
}
}
fn round_line_segment(coord: &mut f32, stroke: &Stroke, pixels_per_point: f32) {
// If the stroke is an odd number of pixels wide,
// we want to round the center of it to the center of a pixel.
//
// If however it is an even number of pixels wide,
// we want to round the center to be between two pixels.
//
// We also want to treat strokes that are _almost_ odd as it it was odd,
// to make it symmetric. Same for strokes that are _almost_ even.
//
// For strokes less than a pixel wide we also round to the center,
// because it will rendered as a single row of pixels by the tessellator.
let pixel_size = 1.0 / pixels_per_point;
if stroke.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * stroke.width) {
*coord = coord.round_to_pixel_center(pixels_per_point);
} else {
*coord = coord.round_to_pixels(pixels_per_point);
}
}
fn is_nearest_integer_odd(width: f32) -> bool {
(width * 0.5 + 0.25).fract() > 0.5
}
#[test]
fn test_is_nearest_integer_odd() {
assert!(is_nearest_integer_odd(0.6));
assert!(is_nearest_integer_odd(1.0));
assert!(is_nearest_integer_odd(1.4));
assert!(!is_nearest_integer_odd(1.6));
assert!(!is_nearest_integer_odd(2.0));
assert!(!is_nearest_integer_odd(2.4));
assert!(is_nearest_integer_odd(2.6));
assert!(is_nearest_integer_odd(3.0));
assert!(is_nearest_integer_odd(3.4));
}
#[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"]
pub fn tessellate_shapes(
pixels_per_point: f32,

View File

@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.76"
rust-version = "1.80"
publish = false
# `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error.
@ -16,10 +16,7 @@ crate-type = ["cdylib"]
[dependencies]
eframe = { workspace = true, features = [
"default",
"android-native-activity",
] }
eframe = { workspace = true, features = ["default", "android-native-activity"] }
# For image support:
egui_extras = { workspace = true, features = ["default", "image"] }
@ -29,4 +26,4 @@ winit = { workspace = true }
android_logger = "0.14"
[package.metadata.android]
build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android" ]
build_targets = ["armv7-linux-androideabi", "aarch64-linux-android"]