Skip to content

Commit

Permalink
Make all lines and rectangles crisp (emilk#5518)
Browse files Browse the repository at this point in the history
* Merge this first: emilk#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 emilk#5164
* Closes emilk#3667

This undoes a lot of the explicit, egui-side aligning added in:
* emilk#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
  • Loading branch information
emilk authored Dec 26, 2024
1 parent dfcc679 commit d20f93e
Show file tree
Hide file tree
Showing 55 changed files with 314 additions and 251 deletions.
30 changes: 18 additions & 12 deletions crates/egui/src/containers/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down
4 changes: 3 additions & 1 deletion crates/egui/src/containers/resize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
45 changes: 0 additions & 45 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use emath::GuiRounding as _;
use epaint::{
emath::{self, TSTransform},
mutex::RwLock,
pos2,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions crates/egui/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down
83 changes: 47 additions & 36 deletions crates/egui/src/painter.rs
Original file line number Diff line number Diff line change
@@ -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,

Expand All @@ -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,
Expand All @@ -49,28 +58,20 @@ 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`].
///
/// 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.
Expand All @@ -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;
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -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(),
Expand All @@ -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(
Expand Down Expand Up @@ -398,6 +407,7 @@ impl Painter {
})
}

/// The stroke extends _outside_ the [`Rect`].
pub fn rect(
&self,
rect: Rect,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit d20f93e

Please sign in to comment.