1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-28 07:23:13 -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

@@ -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,

View File

@@ -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)
}
}
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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!(

View File

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