mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
## Defining what `Rounding` is This PR defines what `Rounding` means: it is the corner radius of underlying `RectShape` rectangle. If you use `StrokeKind::Inside`, this means the rounding is of the outer part of the stroke. Conversely, if you use `StrokeKind::Outside`, the stroke is outside the rounded rectangle, so the stroke has an inner radius or `rounding`, and an outer radius that is larger by `stroke.width`. This definitions is the same as Figma uses. ## Improving general shape rendering The rendering of filled shapes (rectangles, circles, paths, bezier) has been rewritten. Instead of first painting the fill with the stroke on top, we now paint them as one single mesh with shared vertices at the border. This has several benefits: * Less work (faster and with fewer vertices produced) * No overdraw (nicer rendering of translucent shapes) * Correct blending of stroke and fill The logic for rendering thin strokes has also been improved, so that the width of a stroke of `StrokeKind::Outside` never affects the filled area (this used to be wrong for thin strokes). ## Improving of rectangle rendering Rectangles also has specific improvements in how thin rectangles are painted. The handling of "Blur width" is also a lot better, and now works for rectangles with strokes. There also used to be bugs with specific combinations of corner radius and stroke width, that are now fixed. ## But why? With the new `egui::Scene` we end up with a lot of zoomed out shapes, with sub-pixel strokes. These need to look good! One thing led to another, and then I became obsessive 😅 ## Tessellation Test In order to investigate the rendering, I created a Tessellation Test in the `egui_demo_lib`. [Try it here](https://egui-pr-preview.github.io/pr/5669-emilkimprove-tessellator)  
185 lines
4.9 KiB
Rust
185 lines
4.9 KiB
Rust
#![allow(clippy::derived_hash_with_manual_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
|
|
|
use std::{fmt::Debug, sync::Arc};
|
|
|
|
use super::{emath, Color32, ColorMode, Pos2, Rect};
|
|
|
|
/// Describes the width and color of a line.
|
|
///
|
|
/// The default stroke is the same as [`Stroke::NONE`].
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
pub struct Stroke {
|
|
pub width: f32,
|
|
pub color: Color32,
|
|
}
|
|
|
|
impl Stroke {
|
|
/// Same as [`Stroke::default`].
|
|
pub const NONE: Self = Self {
|
|
width: 0.0,
|
|
color: Color32::TRANSPARENT,
|
|
};
|
|
|
|
#[inline]
|
|
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
|
Self {
|
|
width: width.into(),
|
|
color: color.into(),
|
|
}
|
|
}
|
|
|
|
/// True if width is zero or color is transparent
|
|
#[inline]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.width <= 0.0 || self.color == Color32::TRANSPARENT
|
|
}
|
|
}
|
|
|
|
impl<Color> From<(f32, Color)> for Stroke
|
|
where
|
|
Color: Into<Color32>,
|
|
{
|
|
#[inline(always)]
|
|
fn from((width, color): (f32, Color)) -> Self {
|
|
Self::new(width, color)
|
|
}
|
|
}
|
|
|
|
impl std::hash::Hash for Stroke {
|
|
#[inline(always)]
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
let Self { width, color } = *self;
|
|
emath::OrderedFloat(width).hash(state);
|
|
color.hash(state);
|
|
}
|
|
}
|
|
|
|
/// Describes how the stroke of a shape should be painted.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
pub enum StrokeKind {
|
|
/// The stroke should be painted entirely inside of the shape
|
|
Inside,
|
|
|
|
/// The stroke should be painted right on the edge of the shape, half inside and half outside.
|
|
Middle,
|
|
|
|
/// The stroke should be painted entirely outside of the shape
|
|
Outside,
|
|
}
|
|
|
|
/// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`]
|
|
///
|
|
/// The default stroke is the same as [`Stroke::NONE`].
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
pub struct PathStroke {
|
|
pub width: f32,
|
|
pub color: ColorMode,
|
|
pub kind: StrokeKind,
|
|
}
|
|
|
|
impl Default for PathStroke {
|
|
#[inline]
|
|
fn default() -> Self {
|
|
Self::NONE
|
|
}
|
|
}
|
|
|
|
impl PathStroke {
|
|
/// Same as [`PathStroke::default`].
|
|
pub const NONE: Self = Self {
|
|
width: 0.0,
|
|
color: ColorMode::TRANSPARENT,
|
|
kind: StrokeKind::Middle,
|
|
};
|
|
|
|
#[inline]
|
|
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
|
Self {
|
|
width: width.into(),
|
|
color: ColorMode::Solid(color.into()),
|
|
kind: StrokeKind::Middle,
|
|
}
|
|
}
|
|
|
|
/// Create a new `PathStroke` with a UV function
|
|
///
|
|
/// The bounding box passed to the callback will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`)
|
|
#[inline]
|
|
pub fn new_uv(
|
|
width: impl Into<f32>,
|
|
callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static,
|
|
) -> Self {
|
|
Self {
|
|
width: width.into(),
|
|
color: ColorMode::UV(Arc::new(callback)),
|
|
kind: StrokeKind::Middle,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn with_kind(self, kind: StrokeKind) -> Self {
|
|
Self { kind, ..self }
|
|
}
|
|
|
|
/// Set the stroke to be painted right on the edge of the shape, half inside and half outside.
|
|
#[inline]
|
|
pub fn middle(self) -> Self {
|
|
Self {
|
|
kind: StrokeKind::Middle,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Set the stroke to be painted entirely outside of the shape
|
|
#[inline]
|
|
pub fn outside(self) -> Self {
|
|
Self {
|
|
kind: StrokeKind::Outside,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Set the stroke to be painted entirely inside of the shape
|
|
#[inline]
|
|
pub fn inside(self) -> Self {
|
|
Self {
|
|
kind: StrokeKind::Inside,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// True if width is zero or color is solid and transparent
|
|
#[inline]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.width <= 0.0 || self.color == ColorMode::TRANSPARENT
|
|
}
|
|
}
|
|
|
|
impl<Color> From<(f32, Color)> for PathStroke
|
|
where
|
|
Color: Into<Color32>,
|
|
{
|
|
#[inline(always)]
|
|
fn from((width, color): (f32, Color)) -> Self {
|
|
Self::new(width, color)
|
|
}
|
|
}
|
|
|
|
impl From<Stroke> for PathStroke {
|
|
fn from(value: Stroke) -> Self {
|
|
if value.is_empty() {
|
|
// Important, since we use the stroke color when doing feathering of the fill!
|
|
Self::NONE
|
|
} else {
|
|
Self {
|
|
width: value.width,
|
|
color: ColorMode::Solid(value.color),
|
|
kind: StrokeKind::Middle,
|
|
}
|
|
}
|
|
}
|
|
}
|