1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 13:20:05 -04:00

Merge branch 'master' into cache_galley_lines

This commit is contained in:
Emil Ernerfeldt
2024-12-28 12:55:05 +01:00
68 changed files with 735 additions and 381 deletions

View File

@@ -2,6 +2,8 @@
//! It has no frame or own size. It is potentially movable.
//! It is the foundation for windows and popups.
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, NumExt, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
@@ -66,6 +68,7 @@ impl AreaState {
pivot_pos.x - self.pivot.x().to_factor() * size.x,
pivot_pos.y - self.pivot.y().to_factor() * size.y,
)
.round_ui()
}
/// Move the left top positions of the area.
@@ -80,7 +83,7 @@ impl AreaState {
/// Where the area is on screen.
pub fn rect(&self) -> Rect {
let size = self.size.unwrap_or_default();
Rect::from_min_size(self.left_top_pos(), size)
Rect::from_min_size(self.left_top_pos(), size).round_ui()
}
}
@@ -493,12 +496,11 @@ impl Area {
if constrain {
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), constrain_rect)
.min,
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
);
}
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
state.set_left_top_pos(state.left_top_pos());
// Update response with possibly moved/constrained rect:
move_response.rect = state.rect();

View File

@@ -15,6 +15,8 @@
//!
//! Add your [`crate::Window`]:s after any top-level panels.
use emath::GuiRounding as _;
use crate::{
lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt,
Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
@@ -76,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.
@@ -264,6 +273,8 @@ impl SidePanel {
}
}
panel_rect = panel_rect.round_ui();
let mut panel_ui = ui.new_child(
UiBuilder::new()
.id_salt(id)
@@ -345,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);
}
@@ -558,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.
@@ -756,6 +770,8 @@ impl TopBottomPanel {
}
}
panel_rect = panel_rect.round_ui();
let mut panel_ui = ui.new_child(
UiBuilder::new()
.id_salt(id)
@@ -837,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);
}
@@ -1130,7 +1142,6 @@ impl CentralPanel {
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let available_rect = ctx.available_rect();
let id = Id::new((ctx.viewport_id(), "central_panel"));
let mut panel_ui = Ui::new(
@@ -1138,7 +1149,7 @@ impl CentralPanel {
id,
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(available_rect),
.max_rect(ctx.available_rect().round_ui()),
);
panel_ui.set_clip_rect(ctx.screen_rect());

View File

@@ -221,7 +221,8 @@ impl Resize {
.at_most(self.max_size)
.at_most(
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
);
)
.round_ui();
State {
desired_size: default_size,
@@ -233,7 +234,8 @@ impl Resize {
state.desired_size = state
.desired_size
.at_least(self.min_size)
.at_most(self.max_size);
.at_most(self.max_size)
.round_ui();
let mut user_requested_size = state.requested_size.take();
@@ -383,6 +385,7 @@ impl Resize {
}
}
use emath::GuiRounding as _;
use epaint::Stroke;
pub fn paint_resize_corner(ui: &Ui, response: &Response) {
@@ -397,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

@@ -7,6 +7,7 @@ use crate::{
Align, Align2, Context, CursorIcon, Id, InnerResponse, LayerId, NumExt, Order, Response, Sense,
TextStyle, Ui, UiKind, Vec2b, WidgetInfo, WidgetRect, WidgetText, WidgetType,
};
use emath::GuiRounding as _;
use epaint::{emath, pos2, vec2, Galley, Pos2, Rect, RectShape, Rounding, Shape, Stroke, Vec2};
use super::scroll_area::ScrollBarVisibility;
@@ -582,6 +583,7 @@ impl<'open> Window<'open> {
outer_rect,
frame_stroke,
window_frame.rounding,
resize_interaction,
);
// END FRAME --------------------------------
@@ -595,7 +597,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;
@@ -650,29 +652,30 @@ fn paint_resize_corner(
outer_rect: Rect,
stroke: impl Into<Stroke>,
rounding: impl Into<Rounding>,
i: ResizeInteraction,
) {
let stroke = stroke.into();
let inactive_stroke = stroke.into();
let rounding = rounding.into();
let (corner, radius) = if possible.resize_right && possible.resize_bottom {
(Align2::RIGHT_BOTTOM, rounding.se)
let (corner, radius, corner_response) = if possible.resize_right && possible.resize_bottom {
(Align2::RIGHT_BOTTOM, rounding.se, i.right & i.bottom)
} else if possible.resize_left && possible.resize_bottom {
(Align2::LEFT_BOTTOM, rounding.sw)
(Align2::LEFT_BOTTOM, rounding.sw, i.left & i.bottom)
} else if possible.resize_left && possible.resize_top {
(Align2::LEFT_TOP, rounding.nw)
(Align2::LEFT_TOP, rounding.nw, i.left & i.top)
} else if possible.resize_right && possible.resize_top {
(Align2::RIGHT_TOP, rounding.ne)
(Align2::RIGHT_TOP, rounding.ne, i.right & i.top)
} else {
// We're not in two directions, but it is still nice to tell the user
// we're resizable by painting the resize corner in the expected place
// (i.e. for windows only resizable in one direction):
if possible.resize_right || possible.resize_bottom {
(Align2::RIGHT_BOTTOM, rounding.se)
(Align2::RIGHT_BOTTOM, rounding.se, i.right & i.bottom)
} else if possible.resize_left || possible.resize_bottom {
(Align2::LEFT_BOTTOM, rounding.sw)
(Align2::LEFT_BOTTOM, rounding.sw, i.left & i.bottom)
} else if possible.resize_left || possible.resize_top {
(Align2::LEFT_TOP, rounding.nw)
(Align2::LEFT_TOP, rounding.nw, i.left & i.top)
} else if possible.resize_right || possible.resize_top {
(Align2::RIGHT_TOP, rounding.ne)
(Align2::RIGHT_TOP, rounding.ne, i.right & i.top)
} else {
return;
}
@@ -682,6 +685,14 @@ fn paint_resize_corner(
let offset =
((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
let stroke = if corner_response.drag {
ui.visuals().widgets.active.fg_stroke
} else if corner_response.hover {
ui.visuals().widgets.hovered.fg_stroke
} else {
inactive_stroke
};
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
@@ -743,6 +754,17 @@ impl SideResponse {
}
}
impl std::ops::BitAnd for SideResponse {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self {
hover: self.hover && rhs.hover,
drag: self.drag && rhs.drag,
}
}
}
impl std::ops::BitOrAssign for SideResponse {
fn bitor_assign(&mut self, rhs: Self) {
*self = Self {
@@ -788,13 +810,12 @@ fn resize_response(
area: &mut area::Prepared,
resize_id: Id,
) {
let Some(new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
let Some(mut new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
return;
};
let mut new_rect = ctx.round_rect_to_pixels(new_rect);
if area.constrain() {
new_rect = ctx.constrain_window_rect_to_area(new_rect, area.constrain_rect());
new_rect = Context::constrain_window_rect_to_area(new_rect, area.constrain_rect());
}
// TODO(emilk): add this to a Window state instead as a command "move here next frame"
@@ -819,18 +840,18 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt
let mut rect = interaction.start_rect; // prevent drift
if interaction.left.drag {
rect.min.x = ctx.round_to_pixel(pointer_pos.x);
rect.min.x = pointer_pos.x;
} else if interaction.right.drag {
rect.max.x = ctx.round_to_pixel(pointer_pos.x);
rect.max.x = pointer_pos.x;
}
if interaction.top.drag {
rect.min.y = ctx.round_to_pixel(pointer_pos.y);
rect.min.y = pointer_pos.y;
} else if interaction.bottom.drag {
rect.max.y = ctx.round_to_pixel(pointer_pos.y);
rect.max.y = pointer_pos.y;
}
Some(rect)
Some(rect.round_ui())
}
fn resize_interaction(
@@ -849,7 +870,7 @@ fn resize_interaction(
};
}
let is_dragging = |rect, id| {
let side_response = |rect, id| {
let response = ctx.create_widget(
WidgetRect {
layer_id,
@@ -872,6 +893,12 @@ fn resize_interaction(
let side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner;
let vetrtical_rect = |a: Pos2, b: Pos2| {
Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
};
let horizontal_rect = |a: Pos2, b: Pos2| {
Rect::from_min_max(a, b).expand2(vec2(-corner_grab_radius, side_grab_radius))
};
let corner_rect =
|center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius));
@@ -882,59 +909,80 @@ fn resize_interaction(
// Check sides first, so that corners are on top, covering the sides (i.e. corners have priority)
if possible.resize_right {
let response = is_dragging(
Rect::from_min_max(rect.right_top(), rect.right_bottom()).expand(side_grab_radius),
let response = side_response(
vetrtical_rect(rect.right_top(), rect.right_bottom()),
id.with("right"),
);
right |= response;
}
if possible.resize_left {
let response = is_dragging(
Rect::from_min_max(rect.left_top(), rect.left_bottom()).expand(side_grab_radius),
let response = side_response(
vetrtical_rect(rect.left_top(), rect.left_bottom()),
id.with("left"),
);
left |= response;
}
if possible.resize_bottom {
let response = is_dragging(
Rect::from_min_max(rect.left_bottom(), rect.right_bottom()).expand(side_grab_radius),
let response = side_response(
horizontal_rect(rect.left_bottom(), rect.right_bottom()),
id.with("bottom"),
);
bottom |= response;
}
if possible.resize_top {
let response = is_dragging(
Rect::from_min_max(rect.left_top(), rect.right_top()).expand(side_grab_radius),
let response = side_response(
horizontal_rect(rect.left_top(), rect.right_top()),
id.with("top"),
);
top |= response;
}
// ----------------------------------------
// Now check corners:
// Now check corners.
// We check any corner that has either side resizable,
// because we shrink the side resize handled by the corner width.
// Also, even if we can only change the width (or height) of a window,
// we show one of the corners as a grab-handle, so it makes sense that
// the whole corner is grabbable:
if possible.resize_right && possible.resize_bottom {
let response = is_dragging(corner_rect(rect.right_bottom()), id.with("right_bottom"));
right |= response;
bottom |= response;
if possible.resize_right || possible.resize_bottom {
let response = side_response(corner_rect(rect.right_bottom()), id.with("right_bottom"));
if possible.resize_right {
right |= response;
}
if possible.resize_bottom {
bottom |= response;
}
}
if possible.resize_right && possible.resize_top {
let response = is_dragging(corner_rect(rect.right_top()), id.with("right_top"));
right |= response;
top |= response;
if possible.resize_right || possible.resize_top {
let response = side_response(corner_rect(rect.right_top()), id.with("right_top"));
if possible.resize_right {
right |= response;
}
if possible.resize_top {
top |= response;
}
}
if possible.resize_left && possible.resize_bottom {
let response = is_dragging(corner_rect(rect.left_bottom()), id.with("left_bottom"));
left |= response;
bottom |= response;
if possible.resize_left || possible.resize_bottom {
let response = side_response(corner_rect(rect.left_bottom()), id.with("left_bottom"));
if possible.resize_left {
left |= response;
}
if possible.resize_bottom {
bottom |= response;
}
}
if possible.resize_left && possible.resize_top {
let response = is_dragging(corner_rect(rect.left_top()), id.with("left_top"));
left |= response;
top |= response;
if possible.resize_left || possible.resize_top {
let response = side_response(corner_rect(rect.left_top()), id.with("left_top"));
if possible.resize_left {
left |= response;
}
if possible.resize_top {
top |= response;
}
}
let interaction = ResizeInteraction {
@@ -1070,7 +1118,7 @@ impl TitleBar {
let item_spacing = ui.spacing().item_spacing;
let button_size = Vec2::splat(ui.spacing().icon_width);
let pad = (height - button_size.y) / 2.0; // calculated so that the icon is on the diagonal (if window padding is symmetrical)
let pad = ((height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical)
if collapsible {
ui.add_space(pad);

View File

@@ -3,10 +3,10 @@
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
use containers::area::AreaState;
use emath::GuiRounding as _;
use epaint::{
emath::{self, TSTransform},
mutex::RwLock,
pos2,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
@@ -2003,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.
@@ -2121,7 +2077,7 @@ impl Context {
// ---------------------------------------------------------------------
/// Constrain the position of a window/area so it fits within the provided boundary.
pub(crate) fn constrain_window_rect_to_area(&self, window: Rect, area: Rect) -> Rect {
pub(crate) fn constrain_window_rect_to_area(window: Rect, area: Rect) -> Rect {
let mut pos = window.min;
// Constrain to screen, unless window is too large to fit:
@@ -2133,9 +2089,7 @@ impl Context {
pos.y = pos.y.at_most(area.bottom() + margin_y - window.height()); // move right if needed
pos.y = pos.y.at_least(area.top() - margin_y); // move down if needed
pos = self.round_pos_to_pixels(pos);
Rect::from_min_size(pos, window.size())
Rect::from_min_size(pos, window.size()).round_ui()
}
}
@@ -2568,7 +2522,7 @@ impl Context {
/// Position and size of the egui area.
pub fn screen_rect(&self) -> Rect {
self.input(|i| i.screen_rect())
self.input(|i| i.screen_rect()).round_ui()
}
/// How much space is still available after panels has been added.
@@ -2576,7 +2530,7 @@ impl Context {
/// This is the "background" area, what egui doesn't cover with panels (but may cover with windows).
/// This is also the area to which windows are constrained.
pub fn available_rect(&self) -> Rect {
self.pass_state(|s| s.available_rect())
self.pass_state(|s| s.available_rect()).round_ui()
}
/// How much space is used by panels and windows.
@@ -2586,7 +2540,7 @@ impl Context {
for (_id, window) in ctx.memory.areas().visible_windows() {
used = used.union(window.rect());
}
used
used.round_ui()
})
}
@@ -2594,7 +2548,7 @@ impl Context {
///
/// You can shrink your egui area to this size and still fit all egui components.
pub fn used_size(&self) -> Vec2 {
self.used_rect().max - Pos2::ZERO
(self.used_rect().max - Pos2::ZERO).round_ui()
}
// ---------------------------------------------------------------------

View File

@@ -1,3 +1,5 @@
use emath::GuiRounding as _;
use crate::{
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2,
@@ -179,13 +181,15 @@ impl GridLayout {
let width = self.prev_state.col_width(self.col).unwrap_or(0.0);
let height = self.prev_row_height(self.row);
let size = child_size.max(vec2(width, height));
Rect::from_min_size(cursor.min, size)
Rect::from_min_size(cursor.min, size).round_ui()
}
#[allow(clippy::unused_self)]
pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
// TODO(emilk): allow this alignment to be customized
Align2::LEFT_CENTER.align_size_within_rect(size, frame)
Align2::LEFT_CENTER
.align_size_within_rect(size, frame)
.round_ui()
}
pub(crate) fn justify_and_align(&self, frame: Rect, size: Vec2) -> Rect {

View File

@@ -256,10 +256,6 @@ pub(crate) fn interact(
// In that case we want to hover _both_ widgets,
// otherwise we won't see tooltips for the label.
//
// Because of how `Ui` work, we will often allocate the `Ui` rect
// _after_ adding the children in it (once we know the size it will occopy)
// so we will also have a lot of such `Ui` widgets rects covering almost any widget.
//
// So: we want to hover _all_ widgets above the interactive widget (if any),
// but none below it (an interactive widget stops the hover search).
//
@@ -275,8 +271,16 @@ pub(crate) fn interact(
let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect();
for w in &hits.contains_pointer {
if top_interactive_order <= order(w.id).unwrap_or(0) {
hovered.insert(w.id);
let is_interactive = w.sense.click || w.sense.drag;
if is_interactive {
// The only interactive widgets we mark as hovered are the ones
// in `hits.click` and `hits.drag`!
} else {
let is_on_top_of_the_interactive_widget =
top_interactive_order <= order(w.id).unwrap_or(0);
if is_on_top_of_the_interactive_widget {
hovered.insert(w.id);
}
}
}

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,3 +1,5 @@
use emath::GuiRounding as _;
use crate::{
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
Align,
@@ -394,7 +396,7 @@ impl Layout {
pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect {
debug_assert!(size.x >= 0.0 && size.y >= 0.0);
debug_assert!(!outer.is_negative());
self.align2().align_size_within_rect(size, outer)
self.align2().align_size_within_rect(size, outer).round_ui()
}
fn initial_cursor(&self, max_rect: Rect) -> Rect {
@@ -634,7 +636,7 @@ impl Layout {
debug_assert!(!frame_rect.any_nan());
debug_assert!(!frame_rect.is_negative());
frame_rect
frame_rect.round_ui()
}
/// Apply justify (fill width/height) and/or alignment after calling `next_space`.

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

@@ -1168,11 +1168,9 @@ pub struct DebugOptions {
/// Show interesting widgets under the mouse cursor.
pub show_widget_hits: bool,
/// If true, highlight widgets that are not aligned to integer point coordinates.
/// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`].
///
/// It's usually a good idea to keep to integer coordinates to avoid rounding issues.
///
/// See <https://github.com/emilk/egui/issues/5163> for more.
/// See [`emath::GuiRounding`] for more.
pub show_unaligned: bool,
}
@@ -1189,7 +1187,7 @@ impl Default for DebugOptions {
show_resize: false,
show_interactive_widgets: false,
show_widget_hits: false,
show_unaligned: false,
show_unaligned: cfg!(debug_assertions),
}
}
}
@@ -2487,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

@@ -97,19 +97,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

@@ -3,6 +3,7 @@
use std::{any::Any, hash::Hash, sync::Arc};
use emath::GuiRounding as _;
use epaint::mutex::RwLock;
use crate::{
@@ -482,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]
@@ -716,7 +723,9 @@ impl Ui {
self.painter().layer_id()
}
/// The height of text of this text style
/// The height of text of this text style.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
self.fonts(|f| f.row_height(&style.resolve(self.style())))
}
@@ -1295,6 +1304,7 @@ impl Ui {
/// Ignore the layout of the [`Ui`]: just put my widget here!
/// The layout cursor will advance to past this `rect`.
pub fn allocate_rect(&mut self, rect: Rect, sense: Sense) -> Response {
let rect = rect.round_ui();
let id = self.advance_cursor_after_rect(rect);
self.interact(rect, id, sense)
}
@@ -1302,6 +1312,8 @@ impl Ui {
/// Allocate a rect without interacting with it.
pub fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id {
debug_assert!(!rect.any_nan());
let rect = rect.round_ui();
let item_spacing = self.spacing().item_spacing;
self.placer.advance_after_rects(rect, rect, item_spacing);
register_rect(self, rect);
@@ -2379,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
@@ -3018,7 +3028,7 @@ impl Drop for Ui {
/// Show this rectangle to the user if certain debug options are set.
#[cfg(debug_assertions)]
fn register_rect(ui: &Ui, rect: Rect) {
use emath::Align2;
use emath::{Align2, GuiRounding};
let debug = ui.style().debug;
@@ -3031,16 +3041,16 @@ fn register_rect(ui: &Ui, rect: Rect) {
.text(p0, Align2::LEFT_TOP, "Unaligned", font_id, color);
};
if rect.left().fract() != 0.0 {
if rect.left() != rect.left().round_ui() {
unaligned_line(rect.left_top(), rect.left_bottom());
}
if rect.right().fract() != 0.0 {
if rect.right() != rect.right().round_ui() {
unaligned_line(rect.right_top(), rect.right_bottom());
}
if rect.top().fract() != 0.0 {
if rect.top() != rect.top().round_ui() {
unaligned_line(rect.left_top(), rect.right_top());
}
if rect.bottom().fract() != 0.0 {
if rect.bottom() != rect.bottom().round_ui() {
unaligned_line(rect.left_bottom(), rect.right_bottom());
}
}

View File

@@ -1,5 +1,7 @@
use std::{borrow::Cow, sync::Arc};
use emath::GuiRounding as _;
use crate::{
text::{LayoutJob, TextWrapping},
Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals,
@@ -278,6 +280,8 @@ impl RichText {
}
/// Read the font height of the selected text style.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
let mut font_id = self.text_style.as_ref().map_or_else(
|| FontSelection::Default.resolve(style),
@@ -635,15 +639,16 @@ impl WidgetText {
}
}
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
match self {
Self::RichText(text) => text.font_height(fonts, style),
Self::LayoutJob(job) => job.font_height(fonts),
Self::Galley(galley) => {
if let Some(placed_row) = galley.rows.first() {
placed_row.height()
placed_row.height().round_ui()
} else {
galley.size().y
galley.size().y.round_ui()
}
}
}

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,
);