mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 07:03:14 -04:00
Merge branch 'master' into cache_galley_lines
This commit is contained in:
@@ -4537,7 +4537,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4835,9 +4835,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winit"
|
||||
version = "0.30.5"
|
||||
version = "0.30.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0be9e76a1f1077e04a411f0b989cbd3c93339e1771cb41e71ac4aee95bfd2c67"
|
||||
checksum = "dba50bc8ef4b6f1a75c9274fb95aa9a8f63fbc66c56f391bd85cf68d51e7b1a3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"android-activity",
|
||||
|
||||
@@ -101,7 +101,7 @@ web-sys = "0.3.70"
|
||||
web-time = "1.1.0" # Timekeeping for native and web
|
||||
wgpu = { version = "23.0.0", default-features = false }
|
||||
windows-sys = "0.59"
|
||||
winit = { version = "0.30.5", default-features = false }
|
||||
winit = { version = "0.30.7", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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`.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -681,7 +682,7 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ColorTest;
|
||||
use egui::vec2;
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
|
||||
#[test]
|
||||
pub fn rendering_test() {
|
||||
@@ -689,13 +690,16 @@ mod tests {
|
||||
for dpi in [1.0, 1.25, 1.5, 1.75, 1.6666667, 2.0] {
|
||||
let mut color_test = ColorTest::default();
|
||||
let mut harness = egui_kittest::Harness::builder()
|
||||
.with_size(vec2(2000.0, 2000.0))
|
||||
.with_pixels_per_point(dpi)
|
||||
.build_ui(|ui| {
|
||||
color_test.ui(ui);
|
||||
});
|
||||
|
||||
//harness.set_size(harness.ctx.used_size());
|
||||
{
|
||||
// Expand color-test collapsing header
|
||||
harness.get_by_label("Color test").click();
|
||||
harness.run();
|
||||
}
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a725aa81433f301fda4ff8a28be869366332964995d1ae4ed996591596eb7e2
|
||||
size 31461
|
||||
oid sha256:cf83bead834ec8f88d74b32ae6331715e8c6df183e007e2a16004c019534a30f
|
||||
size 31810
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36028d85f49ee77562250214237def2b676ecc9ed413d2fd8afc473d61289ca1
|
||||
size 32761
|
||||
oid sha256:b8ca5a27491c0589a97e43a70bc10dc52778d25ca3f7e7c895dbbbb784adfcfa
|
||||
size 33245
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:01aaa4ef1a167a94fa1e5163550aabe4fa5e9f3a012b26170fe3088a6ca32d94
|
||||
size 81064
|
||||
oid sha256:e640606207265b4f040f793b0ffb989504b6a98b89e95e77a9a9d3e3abc9327a
|
||||
size 80933
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:684648bea4ef5ce138fc25dbe7576e3937a797e87f2244cb3656ff8b4c2777f5
|
||||
size 11574
|
||||
oid sha256:332c2af36873d8ccccb36c08fd2e475dc1f18454a3090a851c0889395d4f364f
|
||||
size 11518
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ad38bff7cc5661be43e730e1b34c444b571b24b9f50791209496a1687610dd3d
|
||||
size 20543
|
||||
oid sha256:d0d0b1b4d2c4b624904250bc8d6870559f0179e3f7f2d6dc4a4ff256df356237
|
||||
size 20626
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ff78748f2571c49638d8fe8fdc859aaa5181758aad65498b7217551350fb9138
|
||||
size 20672
|
||||
oid sha256:d5fe6166bb8cd5fae0899957e968312b9229a79e423a5d154eda483a149b264d
|
||||
size 20831
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9dee66004cc47f5e27aaac34d137ff005eedf70cbfa3fbe43153dfd5c09d5e18
|
||||
size 10610
|
||||
oid sha256:b71e1d109f90e017644dd20b9d84d81e3a6d5195afbd01ba86c85fa248c8b5c5
|
||||
size 10703
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0d1086b789f1fe0a8085c86f5b6a5ae7ecb53020f385b84775d6812ebc9d74a3
|
||||
size 132349
|
||||
oid sha256:e3c9ba9064f44a4a14405f53316c1c602184caf16cb584d7c1f1912fe59f85ab
|
||||
size 135712
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08be378c01e376aab6e99ba3158519bbd7b301e815dc3447b57c9abab558977f
|
||||
size 24237
|
||||
oid sha256:097bd512dd71c17370f6e374312c19e7ab6845f151b3c3565f2a0790b61ee7ba
|
||||
size 24413
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:53097b2c26ebcba8b8ad657ed8e52ca40261155e96dbbfca1e8eb01fce25d290
|
||||
size 17586
|
||||
oid sha256:2bdf54573a6b0d2fedd90314f91dd7de22dd13709e8dd699b37ef8935b6adda5
|
||||
size 17785
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d9c8395e6b4287b92d85a52ca2d47750f67abeb0ad88c6b42264bfe2e62fd09d
|
||||
size 22283
|
||||
oid sha256:d6328c86b70f3e24aaf87db200d13dfd0baa787dd8431e47621457937f8f5021
|
||||
size 22552
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:38d21b6f8c364f86ad759e88ea1068649c23c58ded5d2953ba8ff1c83b46112f
|
||||
size 63884
|
||||
oid sha256:afb57dc0bb8ff839138e36b8780136e0c8da55ff380832538fae0394143807c0
|
||||
size 65321
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:026723cb5d89b32386a849328c34420ee9e3ae1f97cbf6fa3c4543141123549e
|
||||
size 32890
|
||||
oid sha256:403fc1d08d79168bc9f7a08ac7f363f2df916547f08311838edfda8a499a9b2d
|
||||
size 32879
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83162f8c496a55230375dbc4cc636cfacf63049c913904bea9d06bdb56e63da6
|
||||
size 36282
|
||||
oid sha256:cbd490a15377bdd4fd3272f3cd126cbc4fb9ed2f6f84edcbba03dd30dc3f3d99
|
||||
size 36780
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2537c681d1ffceb5cf4bf19d11295891525c96aea0b1422ab28f133021185be0
|
||||
size 17451
|
||||
oid sha256:c31b3998817e345b12b037d7f8bec7641f77d0c7eab7da9a746b7b51c9efc8fb
|
||||
size 17531
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ccfda16ef7cdf94f7fbbd2c0f8df6f6de7904969e2a66337920c32608a6f9f05
|
||||
size 25357
|
||||
oid sha256:4e8963c3ecd0e74fe9641d87101742a0d45c82a582d70e30eb36bc835f5aac06
|
||||
size 25330
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5068df8549ffc91028addfec6f851f12a4de80e208b50b39e4d44b6aa2c7240e
|
||||
size 261946
|
||||
oid sha256:88b3a50b481942630b5804c60227f23b719dc7e3eb6dbe432c2448cb69332def
|
||||
size 262141
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:579a7a66f86ade628e9f469b0014e9010aa56312ad5bd1e8de2faaae7e0d1af6
|
||||
size 23770
|
||||
oid sha256:4d5d628b54b28eccac0d9ef21bbdabace0cdf507898707956943e2259df846ca
|
||||
size 23741
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:be2ac005fd5aafa293e21b162c22a09078e46d2d45b6208ce0f7841eeb05314a
|
||||
size 183934
|
||||
oid sha256:8763a8f8b4242cbe589cd3282dc5f4b32a84b4e0416fb8072dfefb7879a5d4f6
|
||||
size 187982
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2e3436906f7ac459b7f4330a286937722e78ad885ae1e90f75be566e970a8ca7
|
||||
size 116899
|
||||
oid sha256:3d61088bf1f992467e8396ac89407fa158d6f44e2d2d196778247f3ff18cb893
|
||||
size 119759
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df7dabf726620ab5205ce153f692d1ba02365848ead7b79c95b873d5121d52a6
|
||||
size 25850
|
||||
oid sha256:79ebaf9cccd19da2923507b5a553a23edc67382ef59e4b71f01d3bd6cc913539
|
||||
size 25829
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae6c2e3aad43cfad3322340ff7045ec50ba01d58feb7b8acc5ba062a8a5c9ab8
|
||||
size 70230
|
||||
oid sha256:06cbf13841e6ac5fbc57bdae6f5ad9d19718193c344420dedcc0e9d7ed2b8ba9
|
||||
size 71590
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ec0c2efff75cb8d621f5a4ea59f9fa8d3076521ca34f4499e07fb9dc8681d7ba
|
||||
size 65916
|
||||
oid sha256:35e66f211c0b30a684371b520c46dbe4f9d5b6835e053a4eb65f492dd66a9e6c
|
||||
size 67288
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c04aee0a3a77a3691bb601a93871117500be917e0896138fda43251454ec04c2
|
||||
size 20988
|
||||
oid sha256:fa9ee8631bfe433ee6fad1fb1651fd6b63e2fb3fbc5f321a5410f7266dc16d09
|
||||
size 21296
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:814d863deaa4fa029044da1783db87744f0d82e874edd6cbab16e712ed8715aa
|
||||
size 59881
|
||||
oid sha256:6475702b1bf2c65fb5196928a8934ade647a6053d6902a836e3d69cb7920091e
|
||||
size 59874
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e682f5cb9ecb1bdf89281c2ba1612078e70e97f28c76facc64d717e4015ced6a
|
||||
size 12977
|
||||
oid sha256:9d8daaec0c58709298a4594b7b2aa935aa2de327e6b71bd7417c2ba3a6eb060c
|
||||
size 13020
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15acfb041cc53ef9bd966d6edd53a6b692cdb645ae5cf34bc20e70d403371c30
|
||||
size 34809
|
||||
oid sha256:29d8d859a8cb11e4b390d45c658ed8ff191c2e542909e12f2385c0cba62baa2d
|
||||
size 35109
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8348ff582e11fdc9baf008b5434f81f8d77b834479cb3765c87d1f4fd695e30f
|
||||
size 48212
|
||||
oid sha256:17217e600d8a85ec00ecb11f0e5fe69496b30bbf715cc86785cec6e18b8c2fa1
|
||||
size 48158
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:23482b77cbd817c66421a630e409ac3d8c5d24de00aa91e476e8d42b607c24b1
|
||||
size 48104
|
||||
oid sha256:fac50d2327a9e5e99989dd81d5644db86031b91b9c5c68fc17b5ef53ae655048
|
||||
size 47970
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d94aa33d72c32f6f1aafab92c9753dc07bc5224c701003ac7fe8a01ae8c701a
|
||||
size 44011
|
||||
oid sha256:e5e829257b742194742429be0efd326be17ff7f5b9b16f9df58df21e899320bd
|
||||
size 43963
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e1a5d265470c36e64340ccceea4ade464b3c4a1177d60630b02ae8287934748f
|
||||
size 44026
|
||||
oid sha256:5da064332c669a860a92a34b101f23e28026d4f07948f7c3e9a40e611f5e284f
|
||||
size 43986
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:023eaa363b42ec24ae845dc2ca9ff271a0bd47217e625785d3716044ecfa7a64
|
||||
size 278444
|
||||
oid sha256:ed2a356452d792e32bea57f044da9d86da27fd8504826dd6b87618a53519ea6a
|
||||
size 522556
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d81f618e54176b1c43b710121f249e13ce29827fbea3451827ab62229006677e
|
||||
size 378603
|
||||
oid sha256:0d04a5854528c6141f7def6f9229c51c6d2d4c87e2f656be4d149e7b2b852976
|
||||
size 729056
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d8eca6d5555ef779233175615b877fb91318b4a09a37e5cfbe71973d56f4caf
|
||||
size 465907
|
||||
oid sha256:e6e5c1a745e357faa7b98f7a2cd1ca139c4a14be154b9d21feb8030933acfdb7
|
||||
size 867552
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4768804f57dfc54c5f6b84a2686038b8d630a28c7e928ae044d5b2ce8377e2cd
|
||||
size 538775
|
||||
oid sha256:7d9f4a37541fd1a0754c1cb1f3a2d4a76f03d67ca4e5596c8e6982d691d29dea
|
||||
size 980286
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fcee0e0302f33d681348d62bee3b548beb494c6dd1fa3454586986e0b699e162
|
||||
size 572403
|
||||
oid sha256:4d1c99867202e16500146b7146a32fd83d70d60f5ac94aae4ca405a6377e4625
|
||||
size 1066559
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:254a8dff0b1d4b74971fd3bd4044c4ec0ce49412a95e98419a14dc55b32a4fc9
|
||||
size 663272
|
||||
oid sha256:08fc1c89fd2d04aa12c75a1829dacfff090e322c65ad969648799833e1b072eb
|
||||
size 1235574
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c069ef4f86beeeafd8686f30fc914bedd7e7e7ec38fd96e9a46ac6b31308c43f
|
||||
size 160883
|
||||
oid sha256:d122b1a995e691b5049c57d65c9f222a5f1639b1e4f6f96f91823444339693cc
|
||||
size 160540
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{Id, Pos2, Rect, Response, Sense, Ui, UiBuilder};
|
||||
use egui::{emath::GuiRounding, Id, Pos2, Rect, Response, Sense, Ui, UiBuilder};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum CellSize {
|
||||
@@ -123,7 +123,7 @@ impl<'l> StripLayout<'l> {
|
||||
|
||||
// Make sure we don't have a gap in the stripe/frame/selection background:
|
||||
let item_spacing = self.ui.spacing().item_spacing;
|
||||
let gapless_rect = max_rect.expand2(0.5 * item_spacing);
|
||||
let gapless_rect = max_rect.expand2(0.5 * item_spacing).round_ui();
|
||||
|
||||
if flags.striped {
|
||||
self.ui.painter().rect_filled(
|
||||
|
||||
202
crates/emath/src/gui_rounding.rs
Normal file
202
crates/emath/src/gui_rounding.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
/// We (sometimes) round sizes and coordinates to an even multiple of this value.
|
||||
///
|
||||
/// This is only used for rounding _logical UI points_, used for widget coordinates and sizes.
|
||||
/// When rendering, you may want to round to an integer multiple of the physical _pixels_ instead,
|
||||
/// using [`GuiRounding::round_to_pixels`].
|
||||
///
|
||||
/// See [`GuiRounding::round_ui`] for more information.
|
||||
///
|
||||
/// This constant has to be a (negative) power of two so that it can be represented exactly
|
||||
/// by a floating point number.
|
||||
///
|
||||
/// If we pick too large a value (e.g. 1 or 1/2), then we get judder during scrolling and animations.
|
||||
/// If we pick too small a value (e.g. 1/4096), we run the risk of rounding errors again.
|
||||
///
|
||||
/// `f32` has 23 bits of mantissa, so if we use e.g. 1/8 as the rounding factor,
|
||||
/// we can represent all numbers up to 2^20 exactly, which is plenty
|
||||
/// (to my knowledge there are no displays that are a million pixels wide).
|
||||
pub const GUI_ROUNDING: f32 = 1.0 / 32.0;
|
||||
|
||||
/// Trait for rounding coordinates and sizes to align with either .
|
||||
///
|
||||
/// See [`GuiRounding::round_ui`] for more information.
|
||||
pub trait GuiRounding {
|
||||
/// Rounds floating point numbers to an even multiple of the GUI rounding factor, [`crate::GUI_ROUNDING`].
|
||||
///
|
||||
/// Use this for widget coordinates and sizes.
|
||||
///
|
||||
/// Rounding sizes and positions prevent rounding errors when doing sizing calculations.
|
||||
/// We don't round to integers, because that would be too coarse (causing visible juddering when scrolling, for instance).
|
||||
/// Instead we round to an even multiple of [`GUI_ROUNDING`].
|
||||
fn round_ui(self) -> Self;
|
||||
|
||||
/// Like [`Self::round_ui`], but always rounds towards negative infinity.
|
||||
fn floor_ui(self) -> Self;
|
||||
|
||||
/// Round a size or position to an even multiple of the physical pixel size.
|
||||
///
|
||||
/// This can be useful for crisp rendering.
|
||||
///
|
||||
/// The `self` should be in coordinates of _logical UI points_.
|
||||
/// The argument `pixels_per_point` is the number of _physical pixels_ per logical UI point.
|
||||
/// For instance, on a high-DPI screen, `pixels_per_point` could be `2.0`.
|
||||
fn round_to_pixels(self, pixels_per_point: f32) -> Self;
|
||||
|
||||
/// Will round the position to be in the center of a pixel.
|
||||
///
|
||||
/// The pixel size is `1.0 / pixels_per_point`.
|
||||
///
|
||||
/// So if `pixels_per_point = 2` (i.e. `pixel size = 0.5`),
|
||||
/// then the position will be rounded to the closest of `…, 0.25, 0.75, 1.25, …`.
|
||||
///
|
||||
/// This is useful, for instance, when picking the center of a line that is one pixel wide.
|
||||
fn round_to_pixel_center(self, pixels_per_point: f32) -> Self;
|
||||
}
|
||||
|
||||
impl GuiRounding for f32 {
|
||||
#[inline]
|
||||
fn round_ui(self) -> Self {
|
||||
(self / GUI_ROUNDING).round() * GUI_ROUNDING
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn floor_ui(self) -> Self {
|
||||
(self / GUI_ROUNDING).floor() * GUI_ROUNDING
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_to_pixels(self, pixels_per_point: f32) -> Self {
|
||||
(self * pixels_per_point).round() / pixels_per_point
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
|
||||
((self * pixels_per_point - 0.5).round() + 0.5) / pixels_per_point
|
||||
}
|
||||
}
|
||||
|
||||
impl GuiRounding for f64 {
|
||||
#[inline]
|
||||
fn round_ui(self) -> Self {
|
||||
(self / GUI_ROUNDING as Self).round() * GUI_ROUNDING as Self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn floor_ui(self) -> Self {
|
||||
(self / GUI_ROUNDING as Self).floor() * GUI_ROUNDING as Self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_to_pixels(self, pixels_per_point: f32) -> Self {
|
||||
(self * pixels_per_point as Self).round() / pixels_per_point as Self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
|
||||
((self * pixels_per_point as Self - 0.5).round() + 0.5) / pixels_per_point as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl GuiRounding for crate::Vec2 {
|
||||
#[inline]
|
||||
fn round_ui(self) -> Self {
|
||||
Self::new(self.x.round_ui(), self.y.round_ui())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn floor_ui(self) -> Self {
|
||||
Self::new(self.x.floor_ui(), self.y.floor_ui())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_to_pixels(self, pixels_per_point: f32) -> Self {
|
||||
Self::new(
|
||||
self.x.round_to_pixels(pixels_per_point),
|
||||
self.y.round_to_pixels(pixels_per_point),
|
||||
)
|
||||
}
|
||||
|
||||
// This doesn't really make sense for a Vec2, but 🤷♂️
|
||||
#[inline]
|
||||
fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
|
||||
Self::new(
|
||||
self.x.round_to_pixel_center(pixels_per_point),
|
||||
self.y.round_to_pixel_center(pixels_per_point),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl GuiRounding for crate::Pos2 {
|
||||
#[inline]
|
||||
fn round_ui(self) -> Self {
|
||||
Self::new(self.x.round_ui(), self.y.round_ui())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn floor_ui(self) -> Self {
|
||||
Self::new(self.x.floor_ui(), self.y.floor_ui())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_to_pixels(self, pixels_per_point: f32) -> Self {
|
||||
Self::new(
|
||||
self.x.round_to_pixels(pixels_per_point),
|
||||
self.y.round_to_pixels(pixels_per_point),
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
|
||||
Self::new(
|
||||
self.x.round_to_pixel_center(pixels_per_point),
|
||||
self.y.round_to_pixel_center(pixels_per_point),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl GuiRounding for crate::Rect {
|
||||
/// Rounded so that two adjacent rects that tile perfectly
|
||||
/// will continue to tile perfectly.
|
||||
#[inline]
|
||||
fn round_ui(self) -> Self {
|
||||
Self::from_min_max(self.min.round_ui(), self.max.round_ui())
|
||||
}
|
||||
|
||||
/// Rounded so that two adjacent rects that tile perfectly
|
||||
/// will continue to tile perfectly.
|
||||
#[inline]
|
||||
fn floor_ui(self) -> Self {
|
||||
Self::from_min_max(self.min.floor_ui(), self.max.floor_ui())
|
||||
}
|
||||
|
||||
/// Rounded so that two adjacent rects that tile perfectly
|
||||
/// will continue to tile perfectly.
|
||||
#[inline]
|
||||
fn round_to_pixels(self, pixels_per_point: f32) -> Self {
|
||||
Self::from_min_max(
|
||||
self.min.round_to_pixels(pixels_per_point),
|
||||
self.max.round_to_pixels(pixels_per_point),
|
||||
)
|
||||
}
|
||||
|
||||
/// Rounded so that two adjacent rects that tile perfectly
|
||||
/// will continue to tile perfectly.
|
||||
#[inline]
|
||||
fn round_to_pixel_center(self, pixels_per_point: f32) -> Self {
|
||||
Self::from_min_max(
|
||||
self.min.round_to_pixel_center(pixels_per_point),
|
||||
self.max.round_to_pixel_center(pixels_per_point),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gui_rounding() {
|
||||
assert_eq!(0.0_f32.round_ui(), 0.0);
|
||||
assert_eq!((GUI_ROUNDING * 1.11).round_ui(), GUI_ROUNDING);
|
||||
assert_eq!((-GUI_ROUNDING * 1.11).round_ui(), -GUI_ROUNDING);
|
||||
assert_eq!(f32::NEG_INFINITY.round_ui(), f32::NEG_INFINITY);
|
||||
assert_eq!(f32::INFINITY.round_ui(), f32::INFINITY);
|
||||
|
||||
assert_eq!(0.17_f32.round_to_pixel_center(2.0), 0.25);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ use std::ops::{Add, Div, Mul, RangeInclusive, Sub};
|
||||
|
||||
pub mod align;
|
||||
pub mod easing;
|
||||
mod gui_rounding;
|
||||
mod history;
|
||||
mod numeric;
|
||||
mod ordered_float;
|
||||
@@ -42,6 +43,7 @@ mod vec2b;
|
||||
|
||||
pub use self::{
|
||||
align::{Align, Align2},
|
||||
gui_rounding::{GuiRounding, GUI_ROUNDING},
|
||||
history::History,
|
||||
numeric::*,
|
||||
ordered_float::*,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -671,6 +669,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
|
||||
@@ -696,6 +699,7 @@ pub struct RectShape {
|
||||
}
|
||||
|
||||
impl RectShape {
|
||||
/// The stroke extends _outside_ the [`Rect`].
|
||||
#[inline]
|
||||
pub fn new(
|
||||
rect: Rect,
|
||||
@@ -731,6 +735,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 {
|
||||
@@ -762,8 +767,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1970,6 +2016,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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use emath::{vec2, Vec2};
|
||||
use emath::{vec2, GuiRounding, Vec2};
|
||||
|
||||
use crate::{
|
||||
mutex::{Mutex, RwLock},
|
||||
@@ -96,22 +96,18 @@ impl FontImpl {
|
||||
|
||||
use ab_glyph::{Font, ScaleFont};
|
||||
let scaled = ab_glyph_font.as_scaled(scale_in_pixels);
|
||||
let ascent = scaled.ascent() / pixels_per_point;
|
||||
let descent = scaled.descent() / pixels_per_point;
|
||||
let line_gap = scaled.line_gap() / pixels_per_point;
|
||||
let ascent = (scaled.ascent() / pixels_per_point).round_ui();
|
||||
let descent = (scaled.descent() / pixels_per_point).round_ui();
|
||||
let line_gap = (scaled.line_gap() / pixels_per_point).round_ui();
|
||||
|
||||
// Tweak the scale as the user desired
|
||||
let scale_in_pixels = scale_in_pixels * tweak.scale;
|
||||
let scale_in_points = scale_in_pixels / pixels_per_point;
|
||||
|
||||
let baseline_offset = {
|
||||
let scale_in_points = scale_in_pixels / pixels_per_point;
|
||||
scale_in_points * tweak.baseline_offset_factor
|
||||
};
|
||||
let baseline_offset = (scale_in_points * tweak.baseline_offset_factor).round_ui();
|
||||
|
||||
let y_offset_points = {
|
||||
let scale_in_points = scale_in_pixels / pixels_per_point;
|
||||
scale_in_points * tweak.y_offset_factor
|
||||
} + tweak.y_offset;
|
||||
let y_offset_points =
|
||||
((scale_in_points * tweak.y_offset_factor) + tweak.y_offset).round_ui();
|
||||
|
||||
// Center scaled glyphs properly:
|
||||
let height = ascent + descent;
|
||||
@@ -247,6 +243,8 @@ impl FontImpl {
|
||||
}
|
||||
|
||||
/// Height of one row of text in points.
|
||||
///
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
#[inline(always)]
|
||||
pub fn row_height(&self) -> f32 {
|
||||
self.height_in_points
|
||||
@@ -418,7 +416,9 @@ impl Font {
|
||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||
}
|
||||
|
||||
/// Height of one row of text. In points
|
||||
/// Height of one row of text. In points.
|
||||
///
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
#[inline(always)]
|
||||
pub fn row_height(&self) -> f32 {
|
||||
self.row_height
|
||||
|
||||
@@ -519,7 +519,9 @@ impl Fonts {
|
||||
self.lock().fonts.has_glyphs(font_id, s)
|
||||
}
|
||||
|
||||
/// Height of one row of text in points
|
||||
/// Height of one row of text in points.
|
||||
///
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
#[inline]
|
||||
pub fn row_height(&self, font_id: &FontId) -> f32 {
|
||||
self.lock().fonts.row_height(font_id)
|
||||
@@ -706,6 +708,8 @@ impl FontsImpl {
|
||||
}
|
||||
|
||||
/// Height of one row of text in points.
|
||||
///
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
fn row_height(&mut self, font_id: &FontId) -> f32 {
|
||||
self.font(font_id).row_height()
|
||||
}
|
||||
@@ -817,7 +821,7 @@ impl GalleyCache {
|
||||
halign: job.halign,
|
||||
justify: job.justify,
|
||||
first_row_min_height,
|
||||
round_output_size_to_nearest_ui_point: job.round_output_size_to_nearest_ui_point,
|
||||
round_output_to_gui: job.round_output_to_gui,
|
||||
};
|
||||
first_row_min_height = 0.0;
|
||||
|
||||
@@ -910,11 +914,8 @@ impl GalleyCache {
|
||||
merged_galley.elided |= galley.elided;
|
||||
}
|
||||
|
||||
if merged_galley.job.round_output_size_to_nearest_ui_point {
|
||||
super::round_output_size_to_nearest_ui_point(
|
||||
&mut merged_galley.rect,
|
||||
&merged_galley.job,
|
||||
);
|
||||
if merged_galley.job.round_output_to_gui {
|
||||
super::round_output_to_gui(&mut merged_galley.rect, &merged_galley.job);
|
||||
}
|
||||
|
||||
merged_galley
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use emath::{pos2, vec2, Align, NumExt, Pos2, Rect, Vec2};
|
||||
use emath::{pos2, vec2, Align, GuiRounding as _, NumExt, Pos2, Rect, Vec2};
|
||||
|
||||
use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex};
|
||||
|
||||
@@ -643,7 +643,7 @@ fn galley_from_rows(
|
||||
min_x = min_x.min(placed_row.rect().min.x);
|
||||
max_x = max_x.max(placed_row.rect().max.x);
|
||||
cursor_y += max_row_height;
|
||||
cursor_y = point_scale.round_to_pixel(cursor_y);
|
||||
cursor_y = point_scale.round_to_pixel(cursor_y); // TODO(emilk): it would be better to do the calculations in pixels instead.
|
||||
}
|
||||
|
||||
let format_summary = format_summary(&job);
|
||||
@@ -662,8 +662,13 @@ fn galley_from_rows(
|
||||
|
||||
let mut rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y));
|
||||
|
||||
if job.round_output_size_to_nearest_ui_point {
|
||||
round_output_size_to_nearest_ui_point(&mut rect, &job);
|
||||
if job.round_output_to_gui {
|
||||
for placed_row in &mut rows {
|
||||
placed_row.pos = placed_row.pos.round_ui();
|
||||
let row = Arc::get_mut(&mut placed_row.row).unwrap();
|
||||
row.size = row.size.round_ui();
|
||||
}
|
||||
round_output_to_gui(&mut rect, &job);
|
||||
}
|
||||
|
||||
Galley {
|
||||
@@ -678,20 +683,21 @@ fn galley_from_rows(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn round_output_size_to_nearest_ui_point(rect: &mut Rect, job: &LayoutJob) {
|
||||
pub(crate) fn round_output_to_gui(rect: &mut Rect, job: &LayoutJob) {
|
||||
let did_exceed_wrap_width_by_a_lot = rect.width() > job.wrap.max_width + 1.0;
|
||||
|
||||
// We round the size to whole ui points here (not pixels!) so that the egui layout code
|
||||
// can have the advantage of working in integer units, avoiding rounding errors.
|
||||
rect.min = rect.min.round();
|
||||
rect.max = rect.max.round();
|
||||
*rect = rect.round_ui();
|
||||
|
||||
if did_exceed_wrap_width_by_a_lot {
|
||||
// If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph),
|
||||
// we should let the user know by reporting that our width is wider than the wrap width.
|
||||
} else {
|
||||
// Make sure we don't report being wider than the wrap width the user picked:
|
||||
rect.max.x = rect.max.x.at_most(rect.min.x + job.wrap.max_width).floor();
|
||||
rect.max.x = rect
|
||||
.max
|
||||
.x
|
||||
.at_most(rect.min.x + job.wrap.max_width)
|
||||
.floor_ui();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,6 +1157,7 @@ mod tests {
|
||||
LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default());
|
||||
layout_job.wrap.max_width = f32::INFINITY;
|
||||
layout_job.wrap.max_rows = 1;
|
||||
layout_job.round_output_to_gui = false;
|
||||
let galley = layout(&mut fonts, layout_job.into());
|
||||
assert!(galley.elided);
|
||||
assert_eq!(
|
||||
|
||||
@@ -78,9 +78,8 @@ pub struct LayoutJob {
|
||||
/// Justify text so that word-wrapped rows fill the whole [`TextWrapping::max_width`].
|
||||
pub justify: bool,
|
||||
|
||||
/// Rounding to the closest ui point (not pixel!) allows the rest of the
|
||||
/// layout code to run on perfect integers, avoiding rounding errors.
|
||||
pub round_output_size_to_nearest_ui_point: bool,
|
||||
/// Round output sizes using [`emath::GuiRounding`], to avoid rounding errors in layout code.
|
||||
pub round_output_to_gui: bool,
|
||||
}
|
||||
|
||||
impl Default for LayoutJob {
|
||||
@@ -94,7 +93,7 @@ impl Default for LayoutJob {
|
||||
break_on_newline: true,
|
||||
halign: Align::LEFT,
|
||||
justify: false,
|
||||
round_output_size_to_nearest_ui_point: true,
|
||||
round_output_to_gui: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +167,8 @@ impl LayoutJob {
|
||||
}
|
||||
|
||||
/// The height of the tallest font used in the job.
|
||||
///
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
pub fn font_height(&self, fonts: &crate::Fonts) -> f32 {
|
||||
let mut max_height = 0.0_f32;
|
||||
for section in &self.sections {
|
||||
@@ -178,7 +179,7 @@ impl LayoutJob {
|
||||
|
||||
/// The wrap with, with a small margin in some cases.
|
||||
pub fn effective_wrap_width(&self) -> f32 {
|
||||
if self.round_output_size_to_nearest_ui_point {
|
||||
if self.round_output_to_gui {
|
||||
// On a previous pass we may have rounded down by at most 0.5 and reported that as a width.
|
||||
// egui may then set that width as the max width for subsequent frames, and it is important
|
||||
// that we then don't wrap earlier.
|
||||
@@ -200,7 +201,7 @@ impl std::hash::Hash for LayoutJob {
|
||||
break_on_newline,
|
||||
halign,
|
||||
justify,
|
||||
round_output_size_to_nearest_ui_point,
|
||||
round_output_to_gui,
|
||||
} = self;
|
||||
|
||||
text.hash(state);
|
||||
@@ -210,7 +211,7 @@ impl std::hash::Hash for LayoutJob {
|
||||
break_on_newline.hash(state);
|
||||
halign.hash(state);
|
||||
justify.hash(state);
|
||||
round_output_size_to_nearest_ui_point.hash(state);
|
||||
round_output_to_gui.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user