Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix blurry lines by aligning to pixel grid #4943

Merged
merged 19 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/egui/src/containers/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ impl Frame {
Self {
inner_margin: Margin::symmetric(8.0, 2.0),
fill: style.visuals.panel_fill,
// outer_margin: Margin::same(1.0),
juancampa marked this conversation as resolved.
Show resolved Hide resolved
..Default::default()
}
}
Expand Down
26 changes: 18 additions & 8 deletions crates/egui/src/containers/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,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);

{
Expand All @@ -339,10 +342,12 @@ 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);
let resize_x = ui.painter().round_to_pixel_center(resize_x);
emilk marked this conversation as resolved.
Show resolved Hide resolved

// 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);
}

Expand Down Expand Up @@ -814,6 +819,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);

{
Expand All @@ -828,10 +836,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);
}

Expand Down
26 changes: 8 additions & 18 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,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;
Comment on lines -439 to -441
Copy link
Contributor Author

@juancampa juancampa Aug 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an added benefit, spacing calculations for windows got simplified because the stroke is 100% outside, whereas before it was half in, half out.


let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
Expand Down Expand Up @@ -572,9 +569,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,
},
);
Expand All @@ -584,9 +581,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;
Expand All @@ -600,7 +594,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(
Expand Down Expand Up @@ -664,14 +658,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
Expand Down Expand Up @@ -1133,7 +1123,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(),
Expand All @@ -1147,6 +1136,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);
}
Expand Down
24 changes: 20 additions & 4 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1701,26 +1701,42 @@ impl Context {
});
}

/// Useful for pixel-perfect rendering
/// Useful for pixel-perfect rendering of lines
juancampa marked this conversation as resolved.
Show resolved Hide resolved
#[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
juancampa marked this conversation as resolved.
Show resolved Hide resolved
#[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 {
Expand Down
14 changes: 13 additions & 1 deletion crates/egui/src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,19 @@ impl Painter {
self.clip_rect = clip_rect;
}

/// Useful for pixel-perfect rendering.
/// Useful for pixel-perfect rendering of strokes.
juancampa marked this conversation as resolved.
Show resolved Hide resolved
#[inline]
pub fn round_to_pixel_center(&self, point: f32) -> f32 {
self.ctx().round_to_pixel_center(point)
}

/// Useful for pixel-perfect rendering of strokes.
juancampa marked this conversation as resolved.
Show resolved Hide resolved
#[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)
Expand Down
8 changes: 6 additions & 2 deletions crates/egui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2455,8 +2455,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
Expand Down
4 changes: 2 additions & 2 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2102,9 +2102,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
Expand Down
4 changes: 2 additions & 2 deletions crates/egui/src/widgets/separator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
13 changes: 8 additions & 5 deletions crates/egui_demo_app/src/wrap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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");
});
Expand Down
1 change: 1 addition & 0 deletions crates/egui_demo_lib/src/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
43 changes: 43 additions & 0 deletions crates/egui_demo_lib/src/rendering_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,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) {
Expand Down
Loading
Loading