mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 21:30:03 -04:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user