1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Merge branch 'master' into cache_galley_lines

This commit is contained in:
Hubert Głuchowski
2025-01-03 01:42:32 +01:00
64 changed files with 2838 additions and 1741 deletions

View File

@@ -32,7 +32,7 @@ For small things, just go ahead an open a PR. For bigger things, please file an
Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pieces connects.
You can test your code locally by running `./scripts/check.sh`.
There are snapshots test that might need to be updated.
There are snapshots test that might need to be updated.
Run the tests with `UPDATE_SNAPSHOTS=true cargo test --workspace --all-features` to update all of them.
For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md).
@@ -102,6 +102,7 @@ While using an immediate mode gui is simple, implementing one is a lot more tric
* Avoid double negatives
* Flip `if !condition {} else {}`
* Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`)
* Put each type in their own file, unless they are trivial (e.g. a `struct` with no `impl`)
* Break the above rules when it makes sense

View File

@@ -1319,6 +1319,7 @@ dependencies = [
"egui",
"egui_demo_lib",
"egui_extras",
"egui_kittest",
"ehttp",
"env_logger",
"image",
@@ -1396,6 +1397,7 @@ version = "0.30.0"
dependencies = [
"dify",
"document-features",
"eframe",
"egui",
"egui-wgpu",
"image",

View File

@@ -266,11 +266,10 @@ use_self = "warn"
useless_transmute = "warn"
verbose_file_reads = "warn"
wildcard_dependencies = "warn"
wildcard_imports = "warn"
zero_sized_map_values = "warn"
# TODO(emilk): enable more of these lints:
# TODO(emilk): maybe enable more of these lints?
iter_over_hash_type = "allow"
missing_assert_message = "allow"
should_panic_without_expect = "allow"
@@ -284,3 +283,4 @@ let_underscore_untyped = "allow"
manual_range_contains = "allow" # this one is just worse imho
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
significant_drop_tightening = "allow" # Too many false positives
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports

View File

@@ -109,6 +109,28 @@ impl HasDisplayHandle for CreationContext<'_> {
}
}
impl CreationContext<'_> {
/// Create a new empty [CreationContext] for testing [App]s in kittest.
#[doc(hidden)]
pub fn _new_kittest(egui_ctx: egui::Context) -> Self {
Self {
egui_ctx,
integration_info: IntegrationInfo::mock(),
storage: None,
#[cfg(feature = "glow")]
gl: None,
#[cfg(feature = "glow")]
get_proc_address: None,
#[cfg(feature = "wgpu")]
wgpu_render_state: None,
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
#[cfg(not(target_arch = "wasm32"))]
raw_display_handle: Err(HandleError::NotSupported),
}
}
}
// ----------------------------------------------------------------------------
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
@@ -617,7 +639,8 @@ pub struct Frame {
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
pub(crate) wgpu_render_state: Option<egui_wgpu::RenderState>,
#[doc(hidden)]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
/// Raw platform window handle
#[cfg(not(target_arch = "wasm32"))]
@@ -651,6 +674,25 @@ impl HasDisplayHandle for Frame {
}
impl Frame {
/// Create a new empty [Frame] for testing [App]s in kittest.
#[doc(hidden)]
pub fn _new_kittest() -> Self {
Self {
#[cfg(feature = "glow")]
gl: None,
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
glow_register_native_texture: None,
info: IntegrationInfo::mock(),
#[cfg(not(target_arch = "wasm32"))]
raw_display_handle: Err(HandleError::NotSupported),
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
storage: None,
#[cfg(feature = "wgpu")]
wgpu_render_state: None,
}
}
/// True if you are in a web environment.
///
/// Equivalent to `cfg!(target_arch = "wasm32")`
@@ -794,6 +836,29 @@ pub struct IntegrationInfo {
pub cpu_usage: Option<f32>,
}
impl IntegrationInfo {
fn mock() -> Self {
Self {
#[cfg(target_arch = "wasm32")]
web_info: WebInfo {
user_agent: "kittest".to_owned(),
location: Location {
url: "http://localhost".to_owned(),
protocol: "http:".to_owned(),
host: "localhost".to_owned(),
hostname: "localhost".to_owned(),
port: "80".to_owned(),
hash: String::new(),
query: String::new(),
query_map: Default::default(),
origin: "http://localhost".to_owned(),
},
},
cpu_usage: None,
}
}
}
// ----------------------------------------------------------------------------
/// A place where you can store custom data in a way that persists when you restart the app.

View File

@@ -115,7 +115,7 @@ impl WebPainterWgpu {
let render_state = RenderState::create(
&options.wgpu_options,
&instance,
&surface,
Some(&surface),
depth_format,
1,
options.dithering,

View File

@@ -24,7 +24,7 @@ pub use wgpu;
mod renderer;
pub use renderer::*;
use wgpu::{Adapter, Device, Instance, Queue};
use wgpu::{Adapter, Device, Instance, Queue, TextureFormat};
/// Helpers for capturing screenshots of the UI.
pub mod capture;
@@ -91,7 +91,7 @@ impl RenderState {
pub async fn create(
config: &WgpuConfiguration,
instance: &wgpu::Instance,
surface: &wgpu::Surface<'static>,
compatible_surface: Option<&wgpu::Surface<'static>>,
depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
dithering: bool,
@@ -113,7 +113,7 @@ impl RenderState {
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface: Some(surface),
compatible_surface,
force_fallback_adapter: false,
})
.await
@@ -186,11 +186,14 @@ impl RenderState {
} => (adapter, device, queue),
};
let capabilities = {
let surface_formats = {
profiling::scope!("get_capabilities");
surface.get_capabilities(&adapter).formats
compatible_surface.map_or_else(
|| vec![TextureFormat::Rgba8Unorm],
|s| s.get_capabilities(&adapter).formats,
)
};
let target_format = crate::preferred_framebuffer_format(&capabilities)?;
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
let renderer = Renderer::new(
&device,

View File

@@ -212,7 +212,7 @@ impl Painter {
let render_state = RenderState::create(
&self.configuration,
&self.instance,
&surface,
Some(&surface),
self.depth_format,
self.msaa_samples,
self.dithering,

View File

@@ -4,7 +4,7 @@ use crate::{
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
UiStackInfo,
};
use epaint::{Color32, Margin, Rect, Rounding, Shadow, Shape, Stroke};
use epaint::{Color32, Margin, Marginf, Rect, Rounding, Shadow, Shape, Stroke};
/// Add a background, frame and/or margin to a rectangular background of a [`Ui`].
///
@@ -73,6 +73,18 @@ pub struct Frame {
pub stroke: Stroke,
}
#[test]
fn frame_size() {
assert_eq!(
std::mem::size_of::<Frame>(), 32,
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(
std::mem::size_of::<Frame>() <= 64,
"Frame is getting way too big!"
);
}
impl Frame {
pub fn none() -> Self {
Self::default()
@@ -81,7 +93,7 @@ impl Frame {
/// For when you want to group a few widgets together within a frame.
pub fn group(style: &Style) -> Self {
Self {
inner_margin: Margin::same(6.0), // same and symmetric looks best in corners when nesting groups
inner_margin: Margin::same(6), // same and symmetric looks best in corners when nesting groups
rounding: style.visuals.widgets.noninteractive.rounding,
stroke: style.visuals.widgets.noninteractive.bg_stroke,
..Default::default()
@@ -90,7 +102,7 @@ impl Frame {
pub fn side_top_panel(style: &Style) -> Self {
Self {
inner_margin: Margin::symmetric(8.0, 2.0),
inner_margin: Margin::symmetric(8, 2),
fill: style.visuals.panel_fill,
..Default::default()
}
@@ -98,7 +110,7 @@ impl Frame {
pub fn central_panel(style: &Style) -> Self {
Self {
inner_margin: Margin::same(8.0),
inner_margin: Margin::same(8),
fill: style.visuals.panel_fill,
..Default::default()
}
@@ -143,7 +155,7 @@ impl Frame {
/// and in dark mode this will be very dark.
pub fn canvas(style: &Style) -> Self {
Self {
inner_margin: Margin::same(2.0),
inner_margin: Margin::same(2),
rounding: style.visuals.widgets.noninteractive.rounding,
fill: style.visuals.extreme_bg_color,
stroke: style.visuals.window_stroke(),
@@ -213,10 +225,10 @@ impl Frame {
}
impl Frame {
/// inner margin plus outer margin.
/// Inner margin plus outer margin.
#[inline]
pub fn total_margin(&self) -> Margin {
self.inner_margin + self.outer_margin
pub fn total_margin(&self) -> Marginf {
Marginf::from(self.inner_margin) + Marginf::from(self.outer_margin)
}
}

View File

@@ -8,7 +8,9 @@ use crate::{
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 epaint::{
emath, pos2, vec2, Galley, Pos2, Rect, RectShape, Rounding, Roundingf, Shape, Stroke, Vec2,
};
use super::scroll_area::ScrollBarVisibility;
use super::{area, resize, Area, Frame, Resize, ScrollArea};
@@ -484,10 +486,11 @@ impl<'open> Window<'open> {
// Calculate roughly how much larger the window size is compared to the inner rect
let (title_bar_height, title_content_spacing) = if with_title_bar {
let style = ctx.style();
let spacing = window_margin.top + window_margin.bottom;
let spacing = window_margin.sum().y;
let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing;
window_frame.rounding.ne = window_frame.rounding.ne.clamp(0.0, height / 2.0);
window_frame.rounding.nw = window_frame.rounding.nw.clamp(0.0, height / 2.0);
let half_height = (height / 2.0).round() as _;
window_frame.rounding.ne = window_frame.rounding.ne.clamp(0, half_height);
window_frame.rounding.nw = window_frame.rounding.nw.clamp(0, half_height);
(height, spacing)
} else {
(0.0, 0.0)
@@ -603,8 +606,8 @@ impl<'open> Window<'open> {
let mut round = window_frame.rounding;
if !is_collapsed {
round.se = 0.0;
round.sw = 0.0;
round.se = 0;
round.sw = 0;
}
area_content_ui.painter().set(
@@ -682,6 +685,7 @@ fn paint_resize_corner(
};
// Adjust the corner offset to accommodate for window rounding
let radius = radius as f32;
let offset =
((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
@@ -1022,7 +1026,7 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction)
bottom = interaction.bottom.hover;
}
let rounding = ui.visuals().window_rounding;
let rounding = Roundingf::from(ui.visuals().window_rounding);
let Rect { min, max } = rect;
let mut points = Vec::new();

View File

@@ -1238,8 +1238,8 @@ impl Default for Spacing {
fn default() -> Self {
Self {
item_spacing: vec2(8.0, 3.0),
window_margin: Margin::same(6.0),
menu_margin: Margin::same(6.0),
window_margin: Margin::same(6),
menu_margin: Margin::same(6),
button_padding: vec2(4.0, 1.0),
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
interact_size: vec2(40.0, 18.0),
@@ -1291,25 +1291,25 @@ impl Visuals {
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
error_fg_color: Color32::from_rgb(255, 0, 0), // red
window_rounding: Rounding::same(6.0),
window_rounding: Rounding::same(6),
window_shadow: Shadow {
offset: vec2(10.0, 20.0),
blur: 15.0,
spread: 0.0,
offset: [10, 20],
blur: 15,
spread: 0,
color: Color32::from_black_alpha(96),
},
window_fill: Color32::from_gray(27),
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
window_highlight_topmost: true,
menu_rounding: Rounding::same(6.0),
menu_rounding: Rounding::same(6),
panel_fill: Color32::from_gray(27),
popup_shadow: Shadow {
offset: vec2(6.0, 10.0),
blur: 8.0,
spread: 0.0,
offset: [6, 10],
blur: 8,
spread: 0,
color: Color32::from_black_alpha(96),
},
@@ -1349,9 +1349,9 @@ impl Visuals {
error_fg_color: Color32::from_rgb(255, 0, 0), // red
window_shadow: Shadow {
offset: vec2(10.0, 20.0),
blur: 15.0,
spread: 0.0,
offset: [10, 20],
blur: 15,
spread: 0,
color: Color32::from_black_alpha(25),
},
window_fill: Color32::from_gray(248),
@@ -1360,9 +1360,9 @@ impl Visuals {
panel_fill: Color32::from_gray(248),
popup_shadow: Shadow {
offset: vec2(6.0, 10.0),
blur: 8.0,
spread: 0.0,
offset: [6, 10],
blur: 8,
spread: 0,
color: Color32::from_black_alpha(25),
},
@@ -1412,7 +1412,7 @@ impl Widgets {
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 0.0,
},
inactive: WidgetVisuals {
@@ -1420,7 +1420,7 @@ impl Widgets {
bg_fill: Color32::from_gray(60), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 0.0,
},
hovered: WidgetVisuals {
@@ -1428,7 +1428,7 @@ impl Widgets {
bg_fill: Color32::from_gray(70),
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
rounding: Rounding::same(3.0),
rounding: Rounding::same(3),
expansion: 1.0,
},
active: WidgetVisuals {
@@ -1436,7 +1436,7 @@ impl Widgets {
bg_fill: Color32::from_gray(55),
bg_stroke: Stroke::new(1.0, Color32::WHITE),
fg_stroke: Stroke::new(2.0, Color32::WHITE),
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 1.0,
},
open: WidgetVisuals {
@@ -1444,7 +1444,7 @@ impl Widgets {
bg_fill: Color32::from_gray(27),
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 0.0,
},
}
@@ -1457,7 +1457,7 @@ impl Widgets {
bg_fill: Color32::from_gray(248),
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 0.0,
},
inactive: WidgetVisuals {
@@ -1465,7 +1465,7 @@ impl Widgets {
bg_fill: Color32::from_gray(230), // checkbox background
bg_stroke: Default::default(),
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 0.0,
},
hovered: WidgetVisuals {
@@ -1473,7 +1473,7 @@ impl Widgets {
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
fg_stroke: Stroke::new(1.5, Color32::BLACK),
rounding: Rounding::same(3.0),
rounding: Rounding::same(3),
expansion: 1.0,
},
active: WidgetVisuals {
@@ -1481,7 +1481,7 @@ impl Widgets {
bg_fill: Color32::from_gray(165),
bg_stroke: Stroke::new(1.0, Color32::BLACK),
fg_stroke: Stroke::new(2.0, Color32::BLACK),
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 1.0,
},
open: WidgetVisuals {
@@ -1489,7 +1489,7 @@ impl Widgets {
bg_fill: Color32::from_gray(220),
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
fg_stroke: Stroke::new(1.0, Color32::BLACK),
rounding: Rounding::same(2.0),
rounding: Rounding::same(2),
expansion: 0.0,
},
}
@@ -2371,9 +2371,17 @@ impl Widget for &mut Margin {
// Apply the checkbox:
if same {
*self = Margin::same((self.left + self.right + self.top + self.bottom) / 4.0);
} else if self.is_same() {
self.right *= 1.00001; // prevent collapsing into sameness
*self =
Margin::from((self.leftf() + self.rightf() + self.topf() + self.bottomf()) / 4.0);
} else {
// Make sure it is not same:
if self.is_same() {
if self.right == i8::MAX {
self.right = i8::MAX - 1;
} else {
self.right += 1;
}
}
}
response
@@ -2420,9 +2428,16 @@ impl Widget for &mut Rounding {
// Apply the checkbox:
if same {
*self = Rounding::same((self.nw + self.ne + self.sw + self.se) / 4.0);
} else if self.is_same() {
self.se *= 1.00001; // prevent collapsing into sameness
*self = Rounding::from(self.average());
} else {
// Make sure we aren't same:
if self.is_same() {
if self.average() == 0.0 {
self.se = 1;
} else {
self.se -= 1;
}
}
}
response
@@ -2441,13 +2456,13 @@ impl Widget for &mut Shadow {
ui.vertical(|ui| {
crate::Grid::new("shadow_ui").show(ui, |ui| {
ui.add(
DragValue::new(&mut offset.x)
DragValue::new(&mut offset[0])
.speed(1.0)
.range(-100.0..=100.0)
.prefix("x: "),
);
ui.add(
DragValue::new(&mut offset.y)
DragValue::new(&mut offset[1])
.speed(1.0)
.range(-100.0..=100.0)
.prefix("y: "),

View File

@@ -99,7 +99,7 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
show_color_at(ui.painter(), color, rect);
let rounding = visuals.rounding.at_most(2.0); // Can't do more rounding because the background grid doesn't do any rounding
let rounding = visuals.rounding.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding
ui.painter()
.rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border
}

View File

@@ -9,7 +9,7 @@ use epaint::{
use crate::{
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
pos2, Color32, Context, Id, Mesh, Painter, Rect, Response, Rounding, Sense, Shape, Spinner,
Stroke, TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
};
/// A widget which displays an image.
@@ -822,15 +822,10 @@ pub fn paint_texture_at(
painter.add(Shape::mesh(mesh));
}
None => {
painter.add(RectShape {
rect,
rounding: options.rounding,
fill: options.tint,
stroke: Stroke::NONE,
blur_width: 0.0,
fill_texture_id: texture.id,
uv: options.uv,
});
painter.add(
RectShape::filled(rect, options.rounding, options.tint)
.with_texture(texture.id, options.uv),
);
}
}
}

View File

@@ -138,7 +138,8 @@ impl Widget for ProgressBar {
let rounding = rounding.unwrap_or_else(|| corner_radius.into());
ui.painter()
.rect(outer_rect, rounding, visuals.extreme_bg_color, Stroke::NONE);
let min_width = 2.0 * rounding.sw.at_least(rounding.nw).at_most(corner_radius);
let min_width =
2.0 * f32::max(rounding.sw as _, rounding.nw as _).at_most(corner_radius);
let filled_width = (outer_rect.width() * progress).at_least(min_width);
let inner_rect =
Rect::from_min_size(outer_rect.min, vec2(filled_width, outer_rect.height()));

View File

@@ -780,10 +780,10 @@ impl<'a> Slider<'a> {
// The trailing rect has to be drawn differently depending on the orientation.
match self.orientation {
SliderOrientation::Horizontal => {
trailing_rail_rect.max.x = center.x + rounding.nw;
trailing_rail_rect.max.x = center.x + rounding.nw as f32;
}
SliderOrientation::Vertical => {
trailing_rail_rect.min.y = center.y - rounding.se;
trailing_rail_rect.min.y = center.y - rounding.se as f32;
}
};

View File

@@ -126,7 +126,7 @@ impl<'t> TextEdit<'t> {
layouter: None,
password: false,
frame: true,
margin: Margin::symmetric(4.0, 2.0),
margin: Margin::symmetric(4, 2),
multiline: true,
interactive: true,
desired_width: None,

View File

@@ -93,3 +93,6 @@ rfd = { version = "0.15", optional = true }
wasm-bindgen = "=0.2.95"
wasm-bindgen-futures.workspace = true
web-sys.workspace = true
[dev-dependencies]
egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] }

View File

@@ -54,8 +54,9 @@ impl eframe::App for ImageViewer {
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.label("URI:");
ui.text_edit_singleline(&mut self.uri_edit_text);
let label = ui.label("URI:");
ui.text_edit_singleline(&mut self.uri_edit_text)
.labelled_by(label.id);
if ui.small_button("").clicked() {
ctx.forget_image(&self.current_uri);
self.uri_edit_text = self.uri_edit_text.trim().to_owned();

View File

@@ -6,7 +6,7 @@ mod backend_panel;
mod frame_history;
mod wrap_app;
pub use wrap_app::WrapApp;
pub use wrap_app::{Anchor, WrapApp};
/// Time of day as seconds since midnight. Used for clock in demo app.
pub(crate) fn seconds_since_midnight() -> f64 {

View File

@@ -38,6 +38,7 @@ impl eframe::App for DemoApp {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FractalClockApp {
fractal_clock: crate::apps::FractalClock,
pub mock_time: Option<f64>,
}
impl eframe::App for FractalClockApp {
@@ -46,7 +47,7 @@ impl eframe::App for FractalClockApp {
.frame(egui::Frame::dark_canvas(&ctx.style()))
.show(ctx, |ui| {
self.fractal_clock
.ui(ui, Some(crate::seconds_since_midnight()));
.ui(ui, self.mock_time.or(Some(crate::seconds_since_midnight())));
});
}
}
@@ -77,7 +78,7 @@ impl eframe::App for ColorTestApp {
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum Anchor {
pub enum Anchor {
Demo,
EasyMarkEditor,
@@ -161,7 +162,7 @@ pub struct State {
http: crate::apps::HttpApp,
#[cfg(feature = "image_viewer")]
image_viewer: crate::apps::ImageViewer,
clock: FractalClockApp,
pub clock: FractalClockApp,
rendering_test: ColorTestApp,
selected_anchor: Anchor,
@@ -170,7 +171,7 @@ pub struct State {
/// Wraps many demo/test apps into one.
pub struct WrapApp {
state: State,
pub state: State,
#[cfg(any(feature = "glow", feature = "wgpu"))]
custom3d: Option<crate::apps::Custom3d>,
@@ -203,7 +204,9 @@ impl WrapApp {
slf
}
fn apps_iter_mut(&mut self) -> impl Iterator<Item = (&str, Anchor, &mut dyn eframe::App)> {
pub fn apps_iter_mut(
&mut self,
) -> impl Iterator<Item = (&'static str, Anchor, &mut dyn eframe::App)> {
let mut vec = vec![
(
"✨ Demos",

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c05cc3d48242e46a391af34cb56f72de7933bf2cead009b6cd477c21867a84e
size 327802

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61212e30fe1fecf5891ddad6ac795df510bfad76b21a7a8a13aa024fdad6d05e
size 93118

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bcf6e2977bed682d7bdaa0b6a6786e528662dd0791d2e6f83cf1b4852035838
size 182833

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e6cc6ff64eb73ddac89ecdacd07c2176f3ab952c0db4593fccf6d11f155ec392
size 103100

View File

@@ -0,0 +1,78 @@
use egui::accesskit::Role;
use egui::Vec2;
use egui_demo_app::{Anchor, WrapApp};
use egui_kittest::kittest::Queryable;
#[test]
fn test_demo_app() {
let mut harness = egui_kittest::Harness::builder()
.with_size(Vec2::new(900.0, 600.0))
.wgpu()
.build_eframe(|cc| WrapApp::new(cc));
let app = harness.state_mut();
// Mock the fractal clock time so snapshots are consistent.
app.state.clock.mock_time = Some(36383.0);
let apps = app
.apps_iter_mut()
.map(|(name, anchor, _)| (name, anchor))
.collect::<Vec<_>>();
#[cfg(feature = "wgpu")]
assert!(
apps.iter()
.any(|(_, anchor)| matches!(anchor, Anchor::Custom3d)),
"Expected to find the Custom3d app.",
);
let mut results = vec![];
for (name, anchor) in apps {
harness.get_by_role_and_label(Role::Button, name).click();
match anchor {
// The widget gallery demo shows the current date, so we can't use it for snapshot testing
Anchor::Demo => {
continue;
}
// This is already tested extensively elsewhere
Anchor::Rendering => {
continue;
}
// We don't want to rely on a network connection for tests
#[cfg(feature = "http")]
Anchor::Http => {
continue;
}
// Load a local image where we know it exists and loads quickly
#[cfg(feature = "image_viewer")]
Anchor::ImageViewer => {
harness.run();
harness
.get_by_role_and_label(Role::TextInput, "URI:")
.focus();
harness.press_key_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
harness
.get_by_role_and_label(Role::TextInput, "URI:")
.type_text("file://../eframe/data/icon.png");
harness.get_by_role_and_label(Role::Button, "").click();
}
_ => {}
}
harness.run();
if let Err(e) = harness.try_snapshot(&anchor.to_string()) {
results.push(e);
}
}
if let Some(error) = results.first() {
panic!("{error}");
}
}

View File

@@ -405,7 +405,7 @@ mod tests {
options.threshold = 2.1;
}
let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options);
let result = harness.try_snapshot_options(&format!("demos/{name}"), &options);
if let Err(err) = result {
errors.push(err.to_string());
}

View File

@@ -12,9 +12,9 @@ impl Default for FrameDemo {
outer_margin: 24.0.into(),
rounding: 14.0.into(),
shadow: egui::Shadow {
offset: [8.0, 12.0].into(),
blur: 16.0,
spread: 0.0,
offset: [8, 12],
blur: 16,
spread: 0,
color: egui::Color32::from_black_alpha(180),
},
fill: egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128),

View File

@@ -235,21 +235,21 @@ mod tests {
let mut results = Vec::new();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_1"));
results.push(harness.try_snapshot("modals_1"));
harness.get_by_label("Save").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_2"));
results.push(harness.try_snapshot("modals_2"));
harness.get_by_label("Yes Please").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_3"));
results.push(harness.try_snapshot("modals_3"));
for result in results {
result.unwrap();
@@ -282,6 +282,6 @@ mod tests {
harness.run();
// This snapshots should show the progress bar modal on top of the save modal.
harness.wgpu_snapshot("modals_backdrop_should_prevent_focusing_lower_area");
harness.snapshot("modals_backdrop_should_prevent_focusing_lower_area");
}
}

View File

@@ -127,8 +127,8 @@ impl crate::View for PanZoom {
.show(ui.ctx(), |ui| {
ui.set_clip_rect(transform.inverse() * rect);
egui::Frame::default()
.rounding(egui::Rounding::same(4.0))
.inner_margin(egui::Margin::same(8.0))
.rounding(egui::Rounding::same(4))
.inner_margin(egui::Margin::same(8))
.stroke(ui.ctx().style().visuals.window_stroke)
.fill(ui.style().visuals.panel_fill)
.show(ui, |ui| {

View File

@@ -307,6 +307,6 @@ mod tests {
harness.fit_contents();
harness.wgpu_snapshot("widget_gallery");
harness.snapshot("widget_gallery");
}
}

View File

@@ -703,7 +703,7 @@ mod tests {
harness.fit_contents();
let result = harness.try_wgpu_snapshot(&format!("rendering_test/dpi_{dpi:.2}"));
let result = harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}"));
if let Err(err) = result {
errors.push(err);
}

View File

@@ -134,14 +134,14 @@ impl<'a> Widget for DatePickerButton<'a> {
let mut pos = button_response.rect.left_bottom();
let width_with_padding = width
+ ui.style().spacing.item_spacing.x
+ ui.style().spacing.window_margin.left
+ ui.style().spacing.window_margin.right;
+ ui.style().spacing.window_margin.leftf()
+ ui.style().spacing.window_margin.rightf();
if pos.x + width_with_padding > ui.clip_rect().right() {
pos.x = button_response.rect.right() - width_with_padding;
}
// Check to make sure the calendar never is displayed out of window
pos.x = pos.x.max(ui.style().spacing.window_margin.left);
pos.x = pos.x.max(ui.style().spacing.window_margin.leftf());
//TODO(elwerene): Better positioning

View File

@@ -20,15 +20,19 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[features]
# Adds a wgpu-based test renderer.
wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image"]
wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image", "eframe?/wgpu"]
# Adds a dify-based image snapshot utility.
snapshot = ["dep:dify", "dep:image", "image/png"]
# Allows testing eframe::App
eframe = ["dep:eframe", "eframe/accesskit"]
[dependencies]
kittest.workspace = true
egui = { workspace = true, features = ["accesskit"] }
eframe = { workspace = true, optional = true }
# wgpu dependencies
egui-wgpu = { workspace = true, optional = true }

View File

@@ -29,13 +29,13 @@ fn main() {
// You can even render the ui and do image snapshot tests
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
harness.wgpu_snapshot("readme_example");
harness.snapshot("readme_example");
}
```
## Snapshot testing
There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features.
Once enabled, you can call `Harness::wgpu_snapshot` to render the ui and save the image to the `tests/snapshots` directory.
Once enabled, you can call `Harness::snapshot` to render the ui and save the image to the `tests/snapshots` directory.
To update the snapshots, run your tests with `UPDATE_SNAPSHOTS=true`, so e.g. `UPDATE_SNAPSHOTS=true cargo test`.
Running with `UPDATE_SNAPSHOTS=true` will still cause the tests to fail, but on the next run, the tests should pass.

View File

@@ -5,37 +5,22 @@ type AppKindUiState<'a, State> = Box<dyn FnMut(&mut egui::Ui, &mut State) + 'a>;
type AppKindContext<'a> = Box<dyn FnMut(&egui::Context) + 'a>;
type AppKindUi<'a> = Box<dyn FnMut(&mut egui::Ui) + 'a>;
/// In order to access the [`eframe::App`] trait from the generic `State`, we store a function pointer
/// here that will return the dyn trait from the struct. In the builder we have the correct where
/// clause to be able to create this.
/// Later we can use it anywhere to get the [`eframe::App`] from the `State`.
#[cfg(feature = "eframe")]
type AppKindEframe<'a, State> = (fn(&mut State) -> &mut dyn eframe::App, eframe::Frame);
pub(crate) enum AppKind<'a, State> {
Context(AppKindContext<'a>),
Ui(AppKindUi<'a>),
ContextState(AppKindContextState<'a, State>),
UiState(AppKindUiState<'a, State>),
#[cfg(feature = "eframe")]
Eframe(AppKindEframe<'a, State>),
}
// TODO(lucasmerlin): These aren't working unfortunately :(
// I think they should work though: https://geo-ant.github.io/blog/2021/rust-traits-and-variadic-functions/
// pub trait IntoAppKind<'a, UiKind> {
// fn into_harness_kind(self) -> AppKind<'a>;
// }
//
// impl<'a, F> IntoAppKind<'a, &egui::Context> for F
// where
// F: FnMut(&egui::Context) + 'a,
// {
// fn into_harness_kind(self) -> AppKind<'a> {
// AppKind::Context(Box::new(self))
// }
// }
//
// impl<'a, F> IntoAppKind<'a, &mut egui::Ui> for F
// where
// F: FnMut(&mut egui::Ui) + 'a,
// {
// fn into_harness_kind(self) -> AppKind<'a> {
// AppKind::Ui(Box::new(self))
// }
// }
impl<'a, State> AppKind<'a, State> {
pub fn run(
&mut self,
@@ -54,6 +39,12 @@ impl<'a, State> AppKind<'a, State> {
f(ctx, state);
None
}
#[cfg(feature = "eframe")]
AppKind::Eframe((get_app, frame)) => {
let app = get_app(state);
app.update(ctx, frame);
None
}
kind_ui => Some(kind_ui.run_ui(ctx, state, sizing_pass)),
}
}
@@ -78,7 +69,9 @@ impl<'a, State> AppKind<'a, State> {
.show(ui, |ui| match self {
AppKind::Ui(f) => f(ui),
AppKind::UiState(f) => f(ui, state),
_ => unreachable!(),
_ => unreachable!(
"run_ui should only be called with AppKind::Ui or AppKind UiState"
),
});
})
.response

View File

@@ -1,5 +1,6 @@
use crate::app_kind::AppKind;
use crate::Harness;
use crate::wgpu::WgpuTestRenderer;
use crate::{Harness, LazyRenderer, TestRenderer};
use egui::{Pos2, Rect, Vec2};
use std::marker::PhantomData;
@@ -8,6 +9,7 @@ pub struct HarnessBuilder<State = ()> {
pub(crate) screen_rect: Rect,
pub(crate) pixels_per_point: f32,
pub(crate) state: PhantomData<State>,
pub(crate) renderer: Box<dyn TestRenderer>,
}
impl<State> Default for HarnessBuilder<State> {
@@ -16,6 +18,7 @@ impl<State> Default for HarnessBuilder<State> {
screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)),
pixels_per_point: 1.0,
state: PhantomData,
renderer: Box::new(LazyRenderer::default()),
}
}
}
@@ -37,6 +40,29 @@ impl<State> HarnessBuilder<State> {
self
}
/// Set the [`TestRenderer`] to use for rendering.
///
/// By default, a [`LazyRenderer`] is used.
#[inline]
pub fn renderer(mut self, renderer: impl TestRenderer + 'static) -> Self {
self.renderer = Box::new(renderer);
self
}
/// Enable wgpu rendering with a default setup suitable for testing.
///
/// This sets up a [`WgpuTestRenderer`] with the default setup.
#[cfg(feature = "wgpu")]
pub fn wgpu(self) -> Self {
self.renderer(WgpuTestRenderer::default())
}
/// Enable wgpu rendering with the given setup.
#[cfg(feature = "wgpu")]
pub fn wgpu_setup(self, setup: egui_wgpu::WgpuSetup) -> Self {
self.renderer(WgpuTestRenderer::from_setup(setup))
}
/// Create a new Harness with the given app closure and a state.
///
/// The app closure will immediately be called once to create the initial ui.
@@ -66,7 +92,7 @@ impl<State> HarnessBuilder<State> {
app: impl FnMut(&egui::Context, &mut State) + 'a,
state: State,
) -> Harness<'a, State> {
Harness::from_builder(&self, AppKind::ContextState(Box::new(app)), state)
Harness::from_builder(self, AppKind::ContextState(Box::new(app)), state, None)
}
/// Create a new Harness with the given ui closure and a state.
@@ -95,7 +121,30 @@ impl<State> HarnessBuilder<State> {
app: impl FnMut(&mut egui::Ui, &mut State) + 'a,
state: State,
) -> Harness<'a, State> {
Harness::from_builder(&self, AppKind::UiState(Box::new(app)), state)
Harness::from_builder(self, AppKind::UiState(Box::new(app)), state, None)
}
/// Create a new [Harness] from the given eframe creation closure.
/// The app can be accessed via the [`Harness::state`] / [`Harness::state_mut`] methods.
#[cfg(feature = "eframe")]
pub fn build_eframe<'a>(
self,
build: impl FnOnce(&mut eframe::CreationContext<'a>) -> State,
) -> Harness<'a, State>
where
State: eframe::App,
{
let ctx = egui::Context::default();
let mut cc = eframe::CreationContext::_new_kittest(ctx.clone());
let mut frame = eframe::Frame::_new_kittest();
self.renderer.setup_eframe(&mut cc, &mut frame);
let app = build(&mut cc);
let kind = AppKind::Eframe((|state| state, frame));
Harness::from_builder(self, kind, app, Some(ctx))
}
}
@@ -119,7 +168,7 @@ impl HarnessBuilder {
/// });
/// ```
pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> {
Harness::from_builder(&self, AppKind::Context(Box::new(app)), ())
Harness::from_builder(self, AppKind::Context(Box::new(app)), (), None)
}
/// Create a new Harness with the given ui closure.
@@ -138,6 +187,6 @@ impl HarnessBuilder {
/// });
/// ```
pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> {
Harness::from_builder(&self, AppKind::Ui(Box::new(app)), ())
Harness::from_builder(self, AppKind::Ui(Box::new(app)), (), None)
}
}

View File

@@ -12,18 +12,21 @@ mod snapshot;
pub use snapshot::*;
use std::fmt::{Debug, Formatter};
mod app_kind;
mod renderer;
#[cfg(feature = "wgpu")]
mod texture_to_image;
#[cfg(feature = "wgpu")]
pub mod wgpu;
pub use kittest;
use std::mem;
use crate::app_kind::AppKind;
use crate::event::EventState;
pub use builder::*;
use egui::{Pos2, Rect, TexturesDelta, Vec2, ViewportId};
pub use renderer::*;
use egui::{Modifiers, Pos2, Rect, Vec2, ViewportId};
use kittest::{Node, Queryable};
/// The test Harness. This contains everything needed to run the test.
@@ -37,11 +40,11 @@ pub struct Harness<'a, State = ()> {
input: egui::RawInput,
kittest: kittest::State,
output: egui::FullOutput,
texture_deltas: Vec<TexturesDelta>,
app: AppKind<'a, State>,
event_state: EventState,
response: Option<egui::Response>,
state: State,
renderer: Box<dyn TestRenderer>,
}
impl<'a, State> Debug for Harness<'a, State> {
@@ -52,11 +55,12 @@ impl<'a, State> Debug for Harness<'a, State> {
impl<'a, State> Harness<'a, State> {
pub(crate) fn from_builder(
builder: &HarnessBuilder<State>,
builder: HarnessBuilder<State>,
mut app: AppKind<'a, State>,
mut state: State,
ctx: Option<egui::Context>,
) -> Self {
let ctx = egui::Context::default();
let ctx = ctx.unwrap_or_default();
ctx.enable_accesskit();
let mut input = egui::RawInput {
screen_rect: Some(builder.screen_rect),
@@ -73,6 +77,9 @@ impl<'a, State> Harness<'a, State> {
response = app.run(ctx, &mut state, false);
});
let mut renderer = builder.renderer;
renderer.handle_delta(&output.textures_delta);
let mut harness = Self {
app,
ctx,
@@ -84,11 +91,11 @@ impl<'a, State> Harness<'a, State> {
.take()
.expect("AccessKit was disabled"),
),
texture_deltas: vec![mem::take(&mut output.textures_delta)],
output,
response,
event_state: EventState::default(),
state,
renderer,
};
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
harness.run();
@@ -153,6 +160,15 @@ impl<'a, State> Harness<'a, State> {
Self::builder().build_ui_state(app, state)
}
/// Create a new [Harness] from the given eframe creation closure.
#[cfg(feature = "eframe")]
pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self
where
State: eframe::App,
{
Self::builder().build_eframe(builder)
}
/// Set the size of the window.
/// Note: If you only want to set the size once at the beginning,
/// prefer using [`HarnessBuilder::with_size`].
@@ -194,8 +210,7 @@ impl<'a, State> Harness<'a, State> {
.take()
.expect("AccessKit was disabled"),
);
self.texture_deltas
.push(mem::take(&mut output.textures_delta));
self.renderer.handle_delta(&output.textures_delta);
self.output = output;
}
@@ -253,21 +268,35 @@ impl<'a, State> Harness<'a, State> {
/// Press a key.
/// This will create a key down event and a key up event.
pub fn press_key(&mut self, key: egui::Key) {
self.press_key_modifiers(Modifiers::default(), key);
}
/// Press a key with modifiers.
/// This will create a key down event and a key up event.
pub fn press_key_modifiers(&mut self, modifiers: Modifiers, key: egui::Key) {
self.input.events.push(egui::Event::Key {
key,
pressed: true,
modifiers: Default::default(),
modifiers,
repeat: false,
physical_key: None,
});
self.input.events.push(egui::Event::Key {
key,
pressed: false,
modifiers: Default::default(),
modifiers,
repeat: false,
physical_key: None,
});
}
/// Render the last output to an image.
///
/// # Errors
/// Returns an error if the rendering fails.
pub fn render(&mut self) -> Result<image::RgbaImage, String> {
self.renderer.render(&self.ctx, &self.output)
}
}
/// Utilities for stateless harnesses.

View File

@@ -0,0 +1,81 @@
use egui::{Context, FullOutput, TexturesDelta};
use image::RgbaImage;
pub trait TestRenderer {
/// We use this to pass the glow / wgpu render state to [`eframe::Frame`].
#[cfg(feature = "eframe")]
fn setup_eframe(&self, _cc: &mut eframe::CreationContext<'_>, _frame: &mut eframe::Frame) {}
/// Handle a [`TexturesDelta`] by updating the renderer's textures.
fn handle_delta(&mut self, delta: &TexturesDelta);
/// Render the [`crate::Harness`] and return the resulting image.
///
/// # Errors
/// Returns an error if the rendering fails.
fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result<RgbaImage, String>;
}
/// A lazy renderer that initializes the renderer on the first render call.
///
/// By default, this will create a wgpu renderer if the wgpu feature is enabled.
pub enum LazyRenderer {
Uninitialized {
texture_ops: Vec<egui::TexturesDelta>,
builder: Option<Box<dyn FnOnce() -> Box<dyn TestRenderer>>>,
},
Initialized {
renderer: Box<dyn TestRenderer>,
},
}
impl Default for LazyRenderer {
fn default() -> Self {
#[cfg(feature = "wgpu")]
return Self::new(crate::wgpu::WgpuTestRenderer::new);
#[cfg(not(feature = "wgpu"))]
return Self::Uninitialized {
texture_ops: Vec::new(),
builder: None,
};
}
}
impl LazyRenderer {
pub fn new<T: TestRenderer + 'static>(create_renderer: impl FnOnce() -> T + 'static) -> Self {
Self::Uninitialized {
texture_ops: Vec::new(),
builder: Some(Box::new(move || Box::new(create_renderer()))),
}
}
}
impl TestRenderer for LazyRenderer {
fn handle_delta(&mut self, delta: &TexturesDelta) {
match self {
Self::Uninitialized { texture_ops, .. } => texture_ops.push(delta.clone()),
Self::Initialized { renderer } => renderer.handle_delta(delta),
}
}
fn render(&mut self, ctx: &Context, output: &FullOutput) -> Result<RgbaImage, String> {
match self {
Self::Uninitialized {
texture_ops,
builder: build,
} => {
let mut renderer = build.take().ok_or({
"No default renderer available. \
Enable the wgpu feature or set one via HarnessBuilder::renderer"
})?();
for delta in texture_ops.drain(..) {
renderer.handle_delta(&delta);
}
let image = renderer.render(ctx, output)?;
*self = Self::Initialized { renderer };
Ok(image)
}
Self::Initialized { renderer } => renderer.render(ctx, output),
}
}
}

View File

@@ -93,10 +93,16 @@ pub enum SnapshotError {
/// The error that occurred
err: ImageError,
},
/// Error rendering the image
RenderError {
/// The error that occurred
err: String,
},
}
const HOW_TO_UPDATE_SCREENSHOTS: &str =
"Run `UPDATE_SNAPSHOTS=1 cargo test` to update the snapshots.";
"Run `UPDATE_SNAPSHOTS=1 cargo test --all-features` to update the snapshots.";
impl Display for SnapshotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -106,24 +112,28 @@ impl Display for SnapshotError {
diff,
diff_path,
} => {
let diff_path = std::path::absolute(diff_path).unwrap_or(diff_path.clone());
write!(
f,
"'{name}' Image did not match snapshot. Diff: {diff}, {diff_path:?}. {HOW_TO_UPDATE_SCREENSHOTS}"
)
}
Self::OpenSnapshot { path, err } => match err {
ImageError::IoError(io) => match io.kind() {
ErrorKind::NotFound => {
write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
}
Self::OpenSnapshot { path, err } => {
let path = std::path::absolute(path).unwrap_or(path.clone());
match err {
ImageError::IoError(io) => match io.kind() {
ErrorKind::NotFound => {
write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
}
err => {
write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
}
},
err => {
write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
write!(f, "Error decoding snapshot: {err:?}\nAt: {path:?}. Make sure git-lfs is setup correctly. Read the instructions here: https://github.com/emilk/egui/blob/master/CONTRIBUTING.md#making-a-pr")
}
},
err => {
write!(f, "Error decoding snapshot: {err:?}\nAt: {path:?}. Make sure git-lfs is setup correctly. Read the instructions here: https://github.com/emilk/egui/blob/master/CONTRIBUTING.md#making-a-pr")
}
},
}
Self::SizeMismatch {
name,
expected,
@@ -135,8 +145,12 @@ impl Display for SnapshotError {
)
}
Self::WriteSnapshot { path, err } => {
let path = std::path::absolute(path).unwrap_or(path.clone());
write!(f, "Error writing snapshot: {err:?}\nAt: {path:?}")
}
Self::RenderError { err } => {
write!(f, "Error rendering image: {err:?}")
}
}
}
}
@@ -310,7 +324,7 @@ pub fn image_snapshot(current: &image::RgbaImage, name: &str) {
#[cfg(feature = "wgpu")]
impl<State> Harness<'_, State> {
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot
/// with custom options.
///
/// If you want to change the default options for your whole project, you could create an
@@ -318,7 +332,7 @@ impl<State> Harness<'_, State> {
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
/// You could additionally use the
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
/// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults.
/// lint to disable use of the [`Harness::snapshot`] to prevent accidentally using the wrong defaults.
///
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
/// The snapshot will be saved under `{output_path}/{name}.png`.
@@ -326,31 +340,35 @@ impl<State> Harness<'_, State> {
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
///
/// # Errors
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
/// reading or writing the snapshot.
pub fn try_wgpu_snapshot_options(
&self,
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
pub fn try_snapshot_options(
&mut self,
name: &str,
options: &SnapshotOptions,
) -> Result<(), SnapshotError> {
let image = crate::wgpu::TestRenderer::new().render(self);
let image = self
.render()
.map_err(|err| SnapshotError::RenderError { err })?;
try_image_snapshot_options(&image, name, options)
}
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot.
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot.
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
///
/// # Errors
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
/// reading or writing the snapshot.
pub fn try_wgpu_snapshot(&self, name: &str) -> Result<(), SnapshotError> {
let image = crate::wgpu::TestRenderer::new().render(self);
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
pub fn try_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> {
let image = self
.render()
.map_err(|err| SnapshotError::RenderError { err })?;
try_image_snapshot(&image, name)
}
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot
/// with custom options.
///
/// If you want to change the default options for your whole project, you could create an
@@ -358,7 +376,7 @@ impl<State> Harness<'_, State> {
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
/// You could additionally use the
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
/// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults.
/// lint to disable use of the [`Harness::snapshot`] to prevent accidentally using the wrong defaults.
///
/// The snapshot files will be saved under [`SnapshotOptions::output_path`].
/// The snapshot will be saved under `{output_path}/{name}.png`.
@@ -366,11 +384,11 @@ impl<State> Harness<'_, State> {
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
///
/// # Panics
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
/// snapshot.
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
/// snapshot, if the rendering fails or if no default renderer is available.
#[track_caller]
pub fn wgpu_snapshot_options(&self, name: &str, options: &SnapshotOptions) {
match self.try_wgpu_snapshot_options(name, options) {
pub fn snapshot_options(&mut self, name: &str, options: &SnapshotOptions) {
match self.try_snapshot_options(name, options) {
Ok(_) => {}
Err(err) => {
panic!("{}", err);
@@ -378,17 +396,17 @@ impl<State> Harness<'_, State> {
}
}
/// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot.
/// Render a image using the setup [`crate::TestRenderer`] and compare it to the snapshot.
/// The snapshot will be saved under `tests/snapshots/{name}.png`.
/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`.
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
///
/// # Panics
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
/// snapshot.
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
/// snapshot, if the rendering fails or if no default renderer is available.
#[track_caller]
pub fn wgpu_snapshot(&self, name: &str) {
match self.try_wgpu_snapshot(name) {
pub fn snapshot(&mut self, name: &str) {
match self.try_snapshot(name) {
Ok(_) => {}
Err(err) => {
panic!("{}", err);
@@ -396,3 +414,45 @@ impl<State> Harness<'_, State> {
}
}
}
// Deprecated wgpu_snapshot functions
// TODO(lucasmerlin): Remove in 0.32
#[allow(clippy::missing_errors_doc)]
#[cfg(feature = "wgpu")]
impl<State> Harness<'_, State> {
#[deprecated(
since = "0.31.0",
note = "Use `try_snapshot_options` instead. This function will be removed in 0.32"
)]
pub fn try_wgpu_snapshot_options(
&mut self,
name: &str,
options: &SnapshotOptions,
) -> Result<(), SnapshotError> {
self.try_snapshot_options(name, options)
}
#[deprecated(
since = "0.31.0",
note = "Use `try_snapshot` instead. This function will be removed in 0.32"
)]
pub fn try_wgpu_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> {
self.try_snapshot(name)
}
#[deprecated(
since = "0.31.0",
note = "Use `snapshot_options` instead. This function will be removed in 0.32"
)]
pub fn wgpu_snapshot_options(&mut self, name: &str, options: &SnapshotOptions) {
self.snapshot_options(name, options);
}
#[deprecated(
since = "0.31.0",
note = "Use `snapshot` instead. This function will be removed in 0.32"
)]
pub fn wgpu_snapshot(&mut self, name: &str) {
self.snapshot(name);
}
}

View File

@@ -1,122 +1,152 @@
use crate::texture_to_image::texture_to_image;
use crate::Harness;
use egui_wgpu::wgpu::{Backends, InstanceDescriptor, StoreOp, TextureFormat};
use egui_wgpu::{wgpu, ScreenDescriptor};
use eframe::epaint::TextureId;
use egui::TexturesDelta;
use egui_wgpu::wgpu::{Backends, StoreOp, TextureFormat};
use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup};
use image::RgbaImage;
use std::iter::once;
use std::sync::Arc;
use wgpu::Maintain;
/// Utility to render snapshots from a [`Harness`] using [`egui_wgpu`].
pub struct TestRenderer {
device: wgpu::Device,
queue: wgpu::Queue,
dithering: bool,
// TODO(#5506): Replace this with the setup from https://github.com/emilk/egui/pull/5506
pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup {
egui_wgpu::WgpuSetup::CreateNew {
supported_backends: Backends::all(),
device_descriptor: Arc::new(|_| wgpu::DeviceDescriptor::default()),
power_preference: wgpu::PowerPreference::default(),
}
}
impl Default for TestRenderer {
pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState {
let instance = match &setup {
WgpuSetup::Existing { instance, .. } => instance.clone(),
WgpuSetup::CreateNew { .. } => Default::default(),
};
pollster::block_on(egui_wgpu::RenderState::create(
&egui_wgpu::WgpuConfiguration {
wgpu_setup: setup,
..Default::default()
},
&instance,
None,
None,
1,
false,
))
.expect("Failed to create render state")
}
/// Utility to render snapshots from a [`crate::Harness`] using [`egui_wgpu`].
pub struct WgpuTestRenderer {
render_state: RenderState,
}
impl Default for WgpuTestRenderer {
fn default() -> Self {
Self::new()
}
}
impl TestRenderer {
/// Create a new [`TestRenderer`] using a default [`wgpu::Instance`].
impl WgpuTestRenderer {
/// Create a new [`WgpuTestRenderer`] with the default setup.
pub fn new() -> Self {
let instance = wgpu::Instance::new(InstanceDescriptor::default());
let adapters = instance.enumerate_adapters(Backends::all());
let adapter = adapters.first().expect("No adapter found");
let (device, queue) = pollster::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("Egui Device"),
memory_hints: Default::default(),
required_limits: Default::default(),
required_features: Default::default(),
},
None,
))
.expect("Failed to create device");
Self::create(device, queue)
}
/// Create a new [`TestRenderer`] using the provided [`wgpu::Device`] and [`wgpu::Queue`].
pub fn create(device: wgpu::Device, queue: wgpu::Queue) -> Self {
Self {
device,
queue,
dithering: false,
render_state: create_render_state(default_wgpu_setup()),
}
}
/// Enable or disable dithering.
/// Create a new [`WgpuTestRenderer`] with the given setup.
pub fn from_setup(setup: WgpuSetup) -> Self {
Self {
render_state: create_render_state(setup),
}
}
/// Create a new [`WgpuTestRenderer`] from an existing [`RenderState`].
///
/// Disabled by default.
#[inline]
pub fn with_dithering(mut self, dithering: bool) -> Self {
self.dithering = dithering;
self
/// # Panics
/// Panics if the [`RenderState`] has been used before.
pub fn from_render_state(render_state: RenderState) -> Self {
assert!(
render_state
.renderer
.read()
.texture(&TextureId::Managed(0))
.is_none(),
"The RenderState passed in has been used before, pass in a fresh RenderState instead."
);
Self { render_state }
}
}
impl crate::TestRenderer for WgpuTestRenderer {
#[cfg(feature = "eframe")]
fn setup_eframe(&self, cc: &mut eframe::CreationContext<'_>, frame: &mut eframe::Frame) {
cc.wgpu_render_state = Some(self.render_state.clone());
frame.wgpu_render_state = Some(self.render_state.clone());
}
/// Render the [`Harness`] and return the resulting image.
pub fn render<State>(&self, harness: &Harness<'_, State>) -> RgbaImage {
// We need to create a new renderer each time we render, since the renderer stores
// textures related to the Harnesses' egui Context.
// Calling the renderer from different Harnesses would cause problems if we store the renderer.
let mut renderer = egui_wgpu::Renderer::new(
&self.device,
TextureFormat::Rgba8Unorm,
None,
1,
self.dithering,
);
for delta in &harness.texture_deltas {
for (id, image_delta) in &delta.set {
renderer.update_texture(&self.device, &self.queue, *id, image_delta);
}
fn handle_delta(&mut self, delta: &TexturesDelta) {
let mut renderer = self.render_state.renderer.write();
for (id, image) in &delta.set {
renderer.update_texture(
&self.render_state.device,
&self.render_state.queue,
*id,
image,
);
}
}
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Egui Command Encoder"),
});
/// Render the [`crate::Harness`] and return the resulting image.
fn render(
&mut self,
ctx: &egui::Context,
output: &egui::FullOutput,
) -> Result<RgbaImage, String> {
let mut renderer = self.render_state.renderer.write();
let size = harness.ctx.screen_rect().size() * harness.ctx.pixels_per_point();
let mut encoder =
self.render_state
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Egui Command Encoder"),
});
let size = ctx.screen_rect().size() * ctx.pixels_per_point();
let screen = ScreenDescriptor {
pixels_per_point: harness.ctx.pixels_per_point(),
pixels_per_point: ctx.pixels_per_point(),
size_in_pixels: [size.x.round() as u32, size.y.round() as u32],
};
let tessellated = harness.ctx.tessellate(
harness.output().shapes.clone(),
harness.ctx.pixels_per_point(),
);
let tessellated = ctx.tessellate(output.shapes.clone(), ctx.pixels_per_point());
let user_buffers = renderer.update_buffers(
&self.device,
&self.queue,
&self.render_state.device,
&self.render_state.queue,
&mut encoder,
&tessellated,
&screen,
);
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Egui Texture"),
size: wgpu::Extent3d {
width: screen.size_in_pixels[0],
height: screen.size_in_pixels[1],
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let texture = self
.render_state
.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("Egui Texture"),
size: wgpu::Extent3d {
width: screen.size_in_pixels[0],
height: screen.size_in_pixels[1],
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
@@ -141,11 +171,16 @@ impl TestRenderer {
renderer.render(&mut pass, &tessellated, &screen);
}
self.queue
self.render_state
.queue
.submit(user_buffers.into_iter().chain(once(encoder.finish())));
self.device.poll(Maintain::Wait);
self.render_state.device.poll(Maintain::Wait);
texture_to_image(&self.device, &self.queue, &texture)
Ok(texture_to_image(
&self.render_state.device,
&self.render_state.queue,
&texture,
))
}
}

View File

@@ -41,5 +41,5 @@ fn image_failed() {
harness.fit_contents();
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
harness.wgpu_snapshot("image_snapshots");
harness.snapshot("image_snapshots");
}

View File

@@ -11,5 +11,5 @@ fn test_shrink() {
harness.fit_contents();
#[cfg(all(feature = "snapshot", feature = "wgpu"))]
harness.wgpu_snapshot("test_shrink");
harness.snapshot("test_shrink");
}

View File

@@ -0,0 +1,19 @@
use crate::{Rect, TextureId};
/// Controls texturing of a [`crate::RectShape`].
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Brush {
/// If the rect should be filled with a texture, which one?
///
/// The texture is multiplied with [`crate::RectShape::fill`].
pub fill_texture_id: TextureId,
/// What UV coordinates to use for the texture?
///
/// To display a texture, set [`Self::fill_texture_id`],
/// and set this to `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`.
///
/// Use [`Rect::ZERO`] to turn off texturing.
pub uv: Rect,
}

View File

@@ -23,15 +23,18 @@
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
mod bezier;
mod brush;
pub mod color;
pub mod image;
mod margin;
mod marginf;
mod mesh;
pub mod mutex;
mod rounding;
mod roundingf;
mod shadow;
mod shape;
pub mod shape_transform;
mod shapes;
pub mod stats;
mod stroke;
pub mod tessellator;
@@ -40,17 +43,21 @@ mod texture_atlas;
mod texture_handle;
pub mod textures;
pub mod util;
mod viewport;
pub use self::{
bezier::{CubicBezierShape, QuadraticBezierShape},
brush::Brush,
color::ColorMode,
image::{ColorImage, FontImage, ImageData, ImageDelta},
margin::Margin,
marginf::Marginf,
mesh::{Mesh, Mesh16, Vertex},
rounding::Rounding,
roundingf::Roundingf,
shadow::Shadow,
shape::{
CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape,
Rounding, Shape, TextShape,
shapes::{
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape,
QuadraticBezierShape, RectShape, Shape, TextShape,
},
stats::PaintStats,
stroke::{PathStroke, Stroke, StrokeKind},
@@ -59,6 +66,7 @@ pub use self::{
texture_atlas::TextureAtlas,
texture_handle::TextureHandle,
textures::TextureManager,
viewport::ViewportInPixels,
};
#[allow(deprecated)]

View File

@@ -4,27 +4,33 @@ use emath::{vec2, Rect, Vec2};
/// often used to express padding or spacing.
///
/// Can be added and subtracted to/from [`Rect`]s.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
///
/// Negative margins are possible, but may produce weird behavior.
/// Use with care.
///
/// All values are stored as [`i8`] to keep the size of [`Margin`] small.
/// If you want floats, use [`crate::Marginf`] instead.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Margin {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
pub left: i8,
pub right: i8,
pub top: i8,
pub bottom: i8,
}
impl Margin {
pub const ZERO: Self = Self {
left: 0.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
left: 0,
right: 0,
top: 0,
bottom: 0,
};
/// The same margin on every side.
#[doc(alias = "symmetric")]
#[inline]
pub const fn same(margin: f32) -> Self {
pub const fn same(margin: i8) -> Self {
Self {
left: margin,
right: margin,
@@ -35,7 +41,7 @@ impl Margin {
/// Margins with the same size on opposing sides
#[inline]
pub const fn symmetric(x: f32, y: f32) -> Self {
pub const fn symmetric(x: i8, y: i8) -> Self {
Self {
left: x,
right: x,
@@ -44,53 +50,84 @@ impl Margin {
}
}
/// Left margin, as `f32`
#[inline]
pub const fn leftf(self) -> f32 {
self.left as _
}
/// Right margin, as `f32`
#[inline]
pub const fn rightf(self) -> f32 {
self.right as _
}
/// Top margin, as `f32`
#[inline]
pub const fn topf(self) -> f32 {
self.top as _
}
/// Bottom margin, as `f32`
#[inline]
pub const fn bottomf(self) -> f32 {
self.bottom as _
}
/// Total margins on both sides
#[inline]
pub fn sum(&self) -> Vec2 {
vec2(self.left + self.right, self.top + self.bottom)
pub fn sum(self) -> Vec2 {
vec2(self.leftf() + self.rightf(), self.topf() + self.bottomf())
}
#[inline]
pub const fn left_top(&self) -> Vec2 {
vec2(self.left, self.top)
pub const fn left_top(self) -> Vec2 {
vec2(self.leftf(), self.topf())
}
#[inline]
pub const fn right_bottom(&self) -> Vec2 {
vec2(self.right, self.bottom)
pub const fn right_bottom(self) -> Vec2 {
vec2(self.rightf(), self.bottomf())
}
/// Are the margin on every side the same?
#[doc(alias = "symmetric")]
#[inline]
pub fn is_same(&self) -> bool {
pub const fn is_same(self) -> bool {
self.left == self.right && self.left == self.top && self.left == self.bottom
}
#[deprecated = "Use `rect + margin` instead"]
#[inline]
pub fn expand_rect(&self, rect: Rect) -> Rect {
pub fn expand_rect(self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom())
}
#[deprecated = "Use `rect - margin` instead"]
#[inline]
pub fn shrink_rect(&self, rect: Rect) -> Rect {
pub fn shrink_rect(self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom())
}
}
impl From<i8> for Margin {
#[inline]
fn from(v: i8) -> Self {
Self::same(v)
}
}
impl From<f32> for Margin {
#[inline]
fn from(v: f32) -> Self {
Self::same(v)
Self::same(v.round() as _)
}
}
impl From<Vec2> for Margin {
#[inline]
fn from(v: Vec2) -> Self {
Self::symmetric(v.x, v.y)
Self::symmetric(v.x.round() as _, v.y.round() as _)
}
}
@@ -101,37 +138,34 @@ impl std::ops::Add for Margin {
#[inline]
fn add(self, other: Self) -> Self {
Self {
left: self.left + other.left,
right: self.right + other.right,
top: self.top + other.top,
bottom: self.bottom + other.bottom,
left: self.left.saturating_add(other.left),
right: self.right.saturating_add(other.right),
top: self.top.saturating_add(other.top),
bottom: self.bottom.saturating_add(other.bottom),
}
}
}
/// `Margin + f32`
impl std::ops::Add<f32> for Margin {
/// `Margin + i8`
impl std::ops::Add<i8> for Margin {
type Output = Self;
#[inline]
fn add(self, v: f32) -> Self {
fn add(self, v: i8) -> Self {
Self {
left: self.left + v,
right: self.right + v,
top: self.top + v,
bottom: self.bottom + v,
left: self.left.saturating_add(v),
right: self.right.saturating_add(v),
top: self.top.saturating_add(v),
bottom: self.bottom.saturating_add(v),
}
}
}
/// `Margind += f32`
impl std::ops::AddAssign<f32> for Margin {
/// `Margin += i8`
impl std::ops::AddAssign<i8> for Margin {
#[inline]
fn add_assign(&mut self, v: f32) {
self.left += v;
self.right += v;
self.top += v;
self.bottom += v;
fn add_assign(&mut self, v: i8) {
*self = *self + v;
}
}
@@ -142,10 +176,10 @@ impl std::ops::Mul<f32> for Margin {
#[inline]
fn mul(self, v: f32) -> Self {
Self {
left: self.left * v,
right: self.right * v,
top: self.top * v,
bottom: self.bottom * v,
left: (self.leftf() * v).round() as _,
right: (self.rightf() * v).round() as _,
top: (self.topf() * v).round() as _,
bottom: (self.bottomf() * v).round() as _,
}
}
}
@@ -154,10 +188,7 @@ impl std::ops::Mul<f32> for Margin {
impl std::ops::MulAssign<f32> for Margin {
#[inline]
fn mul_assign(&mut self, v: f32) {
self.left *= v;
self.right *= v;
self.top *= v;
self.bottom *= v;
*self = *self * v;
}
}
@@ -167,12 +198,8 @@ impl std::ops::Div<f32> for Margin {
#[inline]
fn div(self, v: f32) -> Self {
Self {
left: self.left / v,
right: self.right / v,
top: self.top / v,
bottom: self.bottom / v,
}
#![allow(clippy::suspicious_arithmetic_impl)]
self * v.recip()
}
}
@@ -180,10 +207,7 @@ impl std::ops::Div<f32> for Margin {
impl std::ops::DivAssign<f32> for Margin {
#[inline]
fn div_assign(&mut self, v: f32) {
self.left /= v;
self.right /= v;
self.top /= v;
self.bottom /= v;
*self = *self / v;
}
}
@@ -194,37 +218,34 @@ impl std::ops::Sub for Margin {
#[inline]
fn sub(self, other: Self) -> Self {
Self {
left: self.left - other.left,
right: self.right - other.right,
top: self.top - other.top,
bottom: self.bottom - other.bottom,
left: self.left.saturating_sub(other.left),
right: self.right.saturating_sub(other.right),
top: self.top.saturating_sub(other.top),
bottom: self.bottom.saturating_sub(other.bottom),
}
}
}
/// `Margin - f32`
impl std::ops::Sub<f32> for Margin {
/// `Margin - i8`
impl std::ops::Sub<i8> for Margin {
type Output = Self;
#[inline]
fn sub(self, v: f32) -> Self {
fn sub(self, v: i8) -> Self {
Self {
left: self.left - v,
right: self.right - v,
top: self.top - v,
bottom: self.bottom - v,
left: self.left.saturating_sub(v),
right: self.right.saturating_sub(v),
top: self.top.saturating_sub(v),
bottom: self.bottom.saturating_sub(v),
}
}
}
/// `Margin -= f32`
impl std::ops::SubAssign<f32> for Margin {
/// `Margin -= i8`
impl std::ops::SubAssign<i8> for Margin {
#[inline]
fn sub_assign(&mut self, v: f32) {
self.left -= v;
self.right -= v;
self.top -= v;
self.bottom -= v;
fn sub_assign(&mut self, v: i8) {
*self = *self - v;
}
}

View File

@@ -0,0 +1,299 @@
use emath::{vec2, Rect, Vec2};
use crate::Margin;
/// A value for all four sides of a rectangle,
/// often used to express padding or spacing.
///
/// Can be added and subtracted to/from [`Rect`]s.
///
/// For storage, use [`crate::Margin`] instead.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Marginf {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
impl From<Margin> for Marginf {
#[inline]
fn from(margin: Margin) -> Self {
Self {
left: margin.left as _,
right: margin.right as _,
top: margin.top as _,
bottom: margin.bottom as _,
}
}
}
impl From<Marginf> for Margin {
#[inline]
fn from(marginf: Marginf) -> Self {
Self {
left: marginf.left as _,
right: marginf.right as _,
top: marginf.top as _,
bottom: marginf.bottom as _,
}
}
}
impl Marginf {
pub const ZERO: Self = Self {
left: 0.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
};
/// The same margin on every side.
#[doc(alias = "symmetric")]
#[inline]
pub const fn same(margin: f32) -> Self {
Self {
left: margin,
right: margin,
top: margin,
bottom: margin,
}
}
/// Margins with the same size on opposing sides
#[inline]
pub const fn symmetric(x: f32, y: f32) -> Self {
Self {
left: x,
right: x,
top: y,
bottom: y,
}
}
/// Total margins on both sides
#[inline]
pub fn sum(&self) -> Vec2 {
vec2(self.left + self.right, self.top + self.bottom)
}
#[inline]
pub const fn left_top(&self) -> Vec2 {
vec2(self.left, self.top)
}
#[inline]
pub const fn right_bottom(&self) -> Vec2 {
vec2(self.right, self.bottom)
}
/// Are the margin on every side the same?
#[doc(alias = "symmetric")]
#[inline]
pub fn is_same(&self) -> bool {
self.left == self.right && self.left == self.top && self.left == self.bottom
}
#[deprecated = "Use `rect + margin` instead"]
#[inline]
pub fn expand_rect(&self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom())
}
#[deprecated = "Use `rect - margin` instead"]
#[inline]
pub fn shrink_rect(&self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom())
}
}
impl From<f32> for Marginf {
#[inline]
fn from(v: f32) -> Self {
Self::same(v)
}
}
impl From<Vec2> for Marginf {
#[inline]
fn from(v: Vec2) -> Self {
Self::symmetric(v.x, v.y)
}
}
/// `Marginf + Marginf`
impl std::ops::Add for Marginf {
type Output = Self;
#[inline]
fn add(self, other: Self) -> Self {
Self {
left: self.left + other.left,
right: self.right + other.right,
top: self.top + other.top,
bottom: self.bottom + other.bottom,
}
}
}
/// `Marginf + f32`
impl std::ops::Add<f32> for Marginf {
type Output = Self;
#[inline]
fn add(self, v: f32) -> Self {
Self {
left: self.left + v,
right: self.right + v,
top: self.top + v,
bottom: self.bottom + v,
}
}
}
/// `Margind += f32`
impl std::ops::AddAssign<f32> for Marginf {
#[inline]
fn add_assign(&mut self, v: f32) {
self.left += v;
self.right += v;
self.top += v;
self.bottom += v;
}
}
/// `Marginf * f32`
impl std::ops::Mul<f32> for Marginf {
type Output = Self;
#[inline]
fn mul(self, v: f32) -> Self {
Self {
left: self.left * v,
right: self.right * v,
top: self.top * v,
bottom: self.bottom * v,
}
}
}
/// `Marginf *= f32`
impl std::ops::MulAssign<f32> for Marginf {
#[inline]
fn mul_assign(&mut self, v: f32) {
self.left *= v;
self.right *= v;
self.top *= v;
self.bottom *= v;
}
}
/// `Marginf / f32`
impl std::ops::Div<f32> for Marginf {
type Output = Self;
#[inline]
fn div(self, v: f32) -> Self {
Self {
left: self.left / v,
right: self.right / v,
top: self.top / v,
bottom: self.bottom / v,
}
}
}
/// `Marginf /= f32`
impl std::ops::DivAssign<f32> for Marginf {
#[inline]
fn div_assign(&mut self, v: f32) {
self.left /= v;
self.right /= v;
self.top /= v;
self.bottom /= v;
}
}
/// `Marginf - Marginf`
impl std::ops::Sub for Marginf {
type Output = Self;
#[inline]
fn sub(self, other: Self) -> Self {
Self {
left: self.left - other.left,
right: self.right - other.right,
top: self.top - other.top,
bottom: self.bottom - other.bottom,
}
}
}
/// `Marginf - f32`
impl std::ops::Sub<f32> for Marginf {
type Output = Self;
#[inline]
fn sub(self, v: f32) -> Self {
Self {
left: self.left - v,
right: self.right - v,
top: self.top - v,
bottom: self.bottom - v,
}
}
}
/// `Marginf -= f32`
impl std::ops::SubAssign<f32> for Marginf {
#[inline]
fn sub_assign(&mut self, v: f32) {
self.left -= v;
self.right -= v;
self.top -= v;
self.bottom -= v;
}
}
/// `Rect + Marginf`
impl std::ops::Add<Marginf> for Rect {
type Output = Self;
#[inline]
fn add(self, margin: Marginf) -> Self {
Self::from_min_max(
self.min - margin.left_top(),
self.max + margin.right_bottom(),
)
}
}
/// `Rect += Marginf`
impl std::ops::AddAssign<Marginf> for Rect {
#[inline]
fn add_assign(&mut self, margin: Marginf) {
*self = *self + margin;
}
}
/// `Rect - Marginf`
impl std::ops::Sub<Marginf> for Rect {
type Output = Self;
#[inline]
fn sub(self, margin: Marginf) -> Self {
Self::from_min_max(
self.min + margin.left_top(),
self.max - margin.right_bottom(),
)
}
}
/// `Rect -= Marginf`
impl std::ops::SubAssign<Marginf> for Rect {
#[inline]
fn sub_assign(&mut self, margin: Marginf) {
*self = *self - margin;
}
}

View File

@@ -0,0 +1,220 @@
/// How rounded the corners of things should be.
///
/// The rounding uses `u8` to save space,
/// so the amount of rounding is limited to integers in the range `[0, 255]`.
///
/// For calculations, you may want to use [`crate::Roundingf`] instead, which uses `f32`.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Rounding {
/// Radius of the rounding of the North-West (left top) corner.
pub nw: u8,
/// Radius of the rounding of the North-East (right top) corner.
pub ne: u8,
/// Radius of the rounding of the South-West (left bottom) corner.
pub sw: u8,
/// Radius of the rounding of the South-East (right bottom) corner.
pub se: u8,
}
impl Default for Rounding {
#[inline]
fn default() -> Self {
Self::ZERO
}
}
impl From<u8> for Rounding {
#[inline]
fn from(radius: u8) -> Self {
Self::same(radius)
}
}
impl From<f32> for Rounding {
#[inline]
fn from(radius: f32) -> Self {
Self::same(radius.round() as u8)
}
}
impl Rounding {
/// No rounding on any corner.
pub const ZERO: Self = Self {
nw: 0,
ne: 0,
sw: 0,
se: 0,
};
/// Same rounding on all four corners.
#[inline]
pub const fn same(radius: u8) -> Self {
Self {
nw: radius,
ne: radius,
sw: radius,
se: radius,
}
}
/// Do all corners have the same rounding?
#[inline]
pub fn is_same(self) -> bool {
self.nw == self.ne && self.nw == self.sw && self.nw == self.se
}
/// Make sure each corner has a rounding of at least this.
#[inline]
pub fn at_least(self, min: u8) -> Self {
Self {
nw: self.nw.max(min),
ne: self.ne.max(min),
sw: self.sw.max(min),
se: self.se.max(min),
}
}
/// Make sure each corner has a rounding of at most this.
#[inline]
pub fn at_most(self, max: u8) -> Self {
Self {
nw: self.nw.min(max),
ne: self.ne.min(max),
sw: self.sw.min(max),
se: self.se.min(max),
}
}
/// Average rounding of the corners.
pub fn average(&self) -> f32 {
(self.nw as f32 + self.ne as f32 + self.sw as f32 + self.se as f32) / 4.0
}
}
impl std::ops::Add for Rounding {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self {
Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
}
}
}
impl std::ops::AddAssign for Rounding {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
};
}
}
impl std::ops::AddAssign<u8> for Rounding {
#[inline]
fn add_assign(&mut self, rhs: u8) {
*self = Self {
nw: self.nw.saturating_add(rhs),
ne: self.ne.saturating_add(rhs),
sw: self.sw.saturating_add(rhs),
se: self.se.saturating_add(rhs),
};
}
}
impl std::ops::Sub for Rounding {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self {
Self {
nw: self.nw.saturating_sub(rhs.nw),
ne: self.ne.saturating_sub(rhs.ne),
sw: self.sw.saturating_sub(rhs.sw),
se: self.se.saturating_sub(rhs.se),
}
}
}
impl std::ops::SubAssign for Rounding {
#[inline]
fn sub_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw.saturating_sub(rhs.nw),
ne: self.ne.saturating_sub(rhs.ne),
sw: self.sw.saturating_sub(rhs.sw),
se: self.se.saturating_sub(rhs.se),
};
}
}
impl std::ops::SubAssign<u8> for Rounding {
#[inline]
fn sub_assign(&mut self, rhs: u8) {
*self = Self {
nw: self.nw.saturating_sub(rhs),
ne: self.ne.saturating_sub(rhs),
sw: self.sw.saturating_sub(rhs),
se: self.se.saturating_sub(rhs),
};
}
}
impl std::ops::Div<f32> for Rounding {
type Output = Self;
#[inline]
fn div(self, rhs: f32) -> Self {
Self {
nw: (self.nw as f32 / rhs) as u8,
ne: (self.ne as f32 / rhs) as u8,
sw: (self.sw as f32 / rhs) as u8,
se: (self.se as f32 / rhs) as u8,
}
}
}
impl std::ops::DivAssign<f32> for Rounding {
#[inline]
fn div_assign(&mut self, rhs: f32) {
*self = Self {
nw: (self.nw as f32 / rhs) as u8,
ne: (self.ne as f32 / rhs) as u8,
sw: (self.sw as f32 / rhs) as u8,
se: (self.se as f32 / rhs) as u8,
};
}
}
impl std::ops::Mul<f32> for Rounding {
type Output = Self;
#[inline]
fn mul(self, rhs: f32) -> Self {
Self {
nw: (self.nw as f32 * rhs) as u8,
ne: (self.ne as f32 * rhs) as u8,
sw: (self.sw as f32 * rhs) as u8,
se: (self.se as f32 * rhs) as u8,
}
}
}
impl std::ops::MulAssign<f32> for Rounding {
#[inline]
fn mul_assign(&mut self, rhs: f32) {
*self = Self {
nw: (self.nw as f32 * rhs) as u8,
ne: (self.ne as f32 * rhs) as u8,
sw: (self.sw as f32 * rhs) as u8,
se: (self.se as f32 * rhs) as u8,
};
}
}

View File

@@ -0,0 +1,236 @@
use crate::Rounding;
/// How rounded the corners of things should be, in `f32`.
///
/// This is used for calculations, but storage is usually done with the more compact [`Rounding`].
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Roundingf {
/// Radius of the rounding of the North-West (left top) corner.
pub nw: f32,
/// Radius of the rounding of the North-East (right top) corner.
pub ne: f32,
/// Radius of the rounding of the South-West (left bottom) corner.
pub sw: f32,
/// Radius of the rounding of the South-East (right bottom) corner.
pub se: f32,
}
impl From<Rounding> for Roundingf {
#[inline]
fn from(rounding: Rounding) -> Self {
Self {
nw: rounding.nw as f32,
ne: rounding.ne as f32,
sw: rounding.sw as f32,
se: rounding.se as f32,
}
}
}
impl From<Roundingf> for Rounding {
#[inline]
fn from(rounding: Roundingf) -> Self {
Self {
nw: rounding.nw.round() as u8,
ne: rounding.ne.round() as u8,
sw: rounding.sw.round() as u8,
se: rounding.se.round() as u8,
}
}
}
impl Default for Roundingf {
#[inline]
fn default() -> Self {
Self::ZERO
}
}
impl From<f32> for Roundingf {
#[inline]
fn from(radius: f32) -> Self {
Self {
nw: radius,
ne: radius,
sw: radius,
se: radius,
}
}
}
impl Roundingf {
/// No rounding on any corner.
pub const ZERO: Self = Self {
nw: 0.0,
ne: 0.0,
sw: 0.0,
se: 0.0,
};
/// Same rounding on all four corners.
#[inline]
pub const fn same(radius: f32) -> Self {
Self {
nw: radius,
ne: radius,
sw: radius,
se: radius,
}
}
/// Do all corners have the same rounding?
#[inline]
pub fn is_same(&self) -> bool {
self.nw == self.ne && self.nw == self.sw && self.nw == self.se
}
/// Make sure each corner has a rounding of at least this.
#[inline]
pub fn at_least(&self, min: f32) -> Self {
Self {
nw: self.nw.max(min),
ne: self.ne.max(min),
sw: self.sw.max(min),
se: self.se.max(min),
}
}
/// Make sure each corner has a rounding of at most this.
#[inline]
pub fn at_most(&self, max: f32) -> Self {
Self {
nw: self.nw.min(max),
ne: self.ne.min(max),
sw: self.sw.min(max),
se: self.se.min(max),
}
}
}
impl std::ops::Add for Roundingf {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self {
Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
}
}
}
impl std::ops::AddAssign for Roundingf {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
};
}
}
impl std::ops::AddAssign<f32> for Roundingf {
#[inline]
fn add_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw + rhs,
ne: self.ne + rhs,
sw: self.sw + rhs,
se: self.se + rhs,
};
}
}
impl std::ops::Sub for Roundingf {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self {
Self {
nw: self.nw - rhs.nw,
ne: self.ne - rhs.ne,
sw: self.sw - rhs.sw,
se: self.se - rhs.se,
}
}
}
impl std::ops::SubAssign for Roundingf {
#[inline]
fn sub_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw - rhs.nw,
ne: self.ne - rhs.ne,
sw: self.sw - rhs.sw,
se: self.se - rhs.se,
};
}
}
impl std::ops::SubAssign<f32> for Roundingf {
#[inline]
fn sub_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw - rhs,
ne: self.ne - rhs,
sw: self.sw - rhs,
se: self.se - rhs,
};
}
}
impl std::ops::Div<f32> for Roundingf {
type Output = Self;
#[inline]
fn div(self, rhs: f32) -> Self {
Self {
nw: self.nw / rhs,
ne: self.ne / rhs,
sw: self.sw / rhs,
se: self.se / rhs,
}
}
}
impl std::ops::DivAssign<f32> for Roundingf {
#[inline]
fn div_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw / rhs,
ne: self.ne / rhs,
sw: self.sw / rhs,
se: self.se / rhs,
};
}
}
impl std::ops::Mul<f32> for Roundingf {
type Output = Self;
#[inline]
fn mul(self, rhs: f32) -> Self {
Self {
nw: self.nw * rhs,
ne: self.ne * rhs,
sw: self.sw * rhs,
se: self.se * rhs,
}
}
}
impl std::ops::MulAssign<f32> for Roundingf {
#[inline]
fn mul_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw * rhs,
ne: self.ne * rhs,
sw: self.sw * rhs,
se: self.se * rhs,
};
}
}

View File

@@ -1,37 +1,45 @@
use super::{Color32, Margin, Rect, RectShape, Rounding, Vec2};
use crate::{Color32, Marginf, Rect, RectShape, Rounding, Vec2};
/// The color and fuzziness of a fuzzy shape.
///
/// Can be used for a rectangular shadow with a soft penumbra.
///
/// Very similar to a box-shadow in CSS.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Shadow {
/// Move the shadow by this much.
///
/// For instance, a value of `[1.0, 2.0]` will move the shadow 1 point to the right and 2 points down,
/// causing a drop-shadow effect.
pub offset: Vec2,
pub offset: [i8; 2],
/// The width of the blur, i.e. the width of the fuzzy penumbra.
///
/// A value of 0.0 means a sharp shadow.
pub blur: f32,
/// A value of 0 means a sharp shadow.
pub blur: u8,
/// Expand the shadow in all directions by this much.
pub spread: f32,
pub spread: u8,
/// Color of the opaque center of the shadow.
pub color: Color32,
}
#[test]
fn shadow_size() {
assert_eq!(
std::mem::size_of::<Shadow>(), 8,
"Shadow changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
}
impl Shadow {
/// No shadow at all.
pub const NONE: Self = Self {
offset: Vec2::ZERO,
blur: 0.0,
spread: 0.0,
offset: [0, 0],
blur: 0,
spread: 0,
color: Color32::TRANSPARENT,
};
@@ -45,26 +53,32 @@ impl Shadow {
spread,
color,
} = *self;
let [offset_x, offset_y] = offset;
let rect = rect.translate(offset).expand(spread);
let rounding = rounding.into() + Rounding::same(spread.abs());
let rect = rect
.translate(Vec2::new(offset_x as _, offset_y as _))
.expand(spread as _);
let rounding = rounding.into() + Rounding::from(spread);
RectShape::filled(rect, rounding, color).with_blur_width(blur)
RectShape::filled(rect, rounding, color).with_blur_width(blur as _)
}
/// How much larger than the parent rect are we in each direction?
pub fn margin(&self) -> Margin {
pub fn margin(&self) -> Marginf {
let Self {
offset,
blur,
spread,
color: _,
} = *self;
Margin {
left: spread + 0.5 * blur - offset.x,
right: spread + 0.5 * blur + offset.x,
top: spread + 0.5 * blur - offset.y,
bottom: spread + 0.5 * blur + offset.y,
let spread = spread as f32;
let blur = blur as f32;
let [offset_x, offset_y] = offset;
Marginf {
left: spread + 0.5 * blur - offset_x as f32,
right: spread + 0.5 * blur + offset_x as f32,
top: spread + 0.5 * blur - offset_y as f32,
bottom: spread + 0.5 * blur + offset_y as f32,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -64,8 +64,7 @@ pub fn adjust_colors(
fill,
stroke,
blur_width: _,
fill_texture_id: _,
uv: _,
brush: _,
}) => {
adjust_color(fill);
adjust_color(&mut stroke.color);
@@ -87,7 +86,7 @@ pub fn adjust_colors(
}
if !galley.is_empty() {
let galley = std::sync::Arc::make_mut(galley);
let galley = Arc::make_mut(galley);
for placed_row in &mut galley.rows {
let row = Arc::make_mut(&mut placed_row.row);
for vertex in &mut row.visuals.mesh.vertices {
@@ -97,11 +96,13 @@ pub fn adjust_colors(
}
}
Shape::Mesh(Mesh {
indices: _,
vertices,
texture_id: _,
}) => {
Shape::Mesh(mesh) => {
let Mesh {
indices: _,
vertices,
texture_id: _,
} = Arc::make_mut(mesh);
for v in vertices {
adjust_color(&mut v.color);
}

View File

@@ -3,7 +3,7 @@
use std::ops::Range;
use crate::{shape::Shape, Color32, PathShape, PathStroke};
use crate::{Color32, PathShape, PathStroke, Shape};
use emath::{Pos2, Rect, RectTransform};
// ----------------------------------------------------------------------------

View File

@@ -0,0 +1,52 @@
use crate::{Color32, Pos2, Rect, Shape, Stroke, Vec2};
/// How to paint a circle.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct CircleShape {
pub center: Pos2,
pub radius: f32,
pub fill: Color32,
pub stroke: Stroke,
}
impl CircleShape {
#[inline]
pub fn filled(center: Pos2, radius: f32, fill_color: impl Into<Color32>) -> Self {
Self {
center,
radius,
fill: fill_color.into(),
stroke: Default::default(),
}
}
#[inline]
pub fn stroke(center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> Self {
Self {
center,
radius,
fill: Default::default(),
stroke: stroke.into(),
}
}
/// The visual bounding rectangle (includes stroke width)
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
Rect::from_center_size(
self.center,
Vec2::splat(self.radius * 2.0 + self.stroke.width),
)
}
}
}
impl From<CircleShape> for Shape {
#[inline(always)]
fn from(shape: CircleShape) -> Self {
Self::Circle(shape)
}
}

View File

@@ -0,0 +1,54 @@
use crate::*;
/// How to paint an ellipse.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct EllipseShape {
pub center: Pos2,
/// Radius is the vector (a, b) where the width of the Ellipse is 2a and the height is 2b
pub radius: Vec2,
pub fill: Color32,
pub stroke: Stroke,
}
impl EllipseShape {
#[inline]
pub fn filled(center: Pos2, radius: Vec2, fill_color: impl Into<Color32>) -> Self {
Self {
center,
radius,
fill: fill_color.into(),
stroke: Default::default(),
}
}
#[inline]
pub fn stroke(center: Pos2, radius: Vec2, stroke: impl Into<Stroke>) -> Self {
Self {
center,
radius,
fill: Default::default(),
stroke: stroke.into(),
}
}
/// The visual bounding rectangle (includes stroke width)
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
Rect::from_center_size(
self.center,
self.radius * 2.0 + Vec2::splat(self.stroke.width),
)
}
}
}
impl From<EllipseShape> for Shape {
#[inline(always)]
fn from(shape: EllipseShape) -> Self {
Self::Ellipse(shape)
}
}

View File

@@ -0,0 +1,19 @@
mod bezier_shape;
mod circle_shape;
mod ellipse_shape;
mod paint_callback;
mod path_shape;
mod rect_shape;
mod shape;
mod text_shape;
pub use self::{
bezier_shape::{CubicBezierShape, QuadraticBezierShape},
circle_shape::CircleShape,
ellipse_shape::EllipseShape,
paint_callback::{PaintCallback, PaintCallbackInfo},
path_shape::PathShape,
rect_shape::RectShape,
shape::Shape,
text_shape::TextShape,
};

View File

@@ -0,0 +1,103 @@
use std::{any::Any, sync::Arc};
use crate::*;
/// Information passed along with [`PaintCallback`] ([`Shape::Callback`]).
pub struct PaintCallbackInfo {
/// Viewport in points.
///
/// This specifies where on the screen to paint, and the borders of this
/// Rect is the [-1, +1] of the Normalized Device Coordinates.
///
/// Note than only a portion of this may be visible due to [`Self::clip_rect`].
///
/// This comes from [`PaintCallback::rect`].
pub viewport: Rect,
/// Clip rectangle in points.
pub clip_rect: Rect,
/// Pixels per point.
pub pixels_per_point: f32,
/// Full size of the screen, in pixels.
pub screen_size_px: [u32; 2],
}
#[test]
fn test_viewport_rounding() {
for i in 0..=10_000 {
// Two adjacent viewports should never overlap:
let x = i as f32 / 97.0;
let left = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_max_x(x);
let right = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_min_x(x);
for pixels_per_point in [0.618, 1.0, std::f32::consts::PI] {
let left = ViewportInPixels::from_points(&left, pixels_per_point, [100, 100]);
let right = ViewportInPixels::from_points(&right, pixels_per_point, [100, 100]);
assert_eq!(left.left_px + left.width_px, right.left_px);
}
}
}
impl PaintCallbackInfo {
/// The viewport rectangle. This is what you would use in e.g. `glViewport`.
pub fn viewport_in_pixels(&self) -> ViewportInPixels {
ViewportInPixels::from_points(&self.viewport, self.pixels_per_point, self.screen_size_px)
}
/// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`.
pub fn clip_rect_in_pixels(&self) -> ViewportInPixels {
ViewportInPixels::from_points(&self.clip_rect, self.pixels_per_point, self.screen_size_px)
}
}
/// If you want to paint some 3D shapes inside an egui region, you can use this.
///
/// This is advanced usage, and is backend specific.
#[derive(Clone)]
pub struct PaintCallback {
/// Where to paint.
///
/// This will become [`PaintCallbackInfo::viewport`].
pub rect: Rect,
/// Paint something custom (e.g. 3D stuff).
///
/// The concrete value of `callback` depends on the rendering backend used. For instance, the
/// `glow` backend requires that callback be an `egui_glow::CallbackFn` while the `wgpu`
/// backend requires a `egui_wgpu::Callback`.
///
/// If the type cannot be downcast to the type expected by the current backend the callback
/// will not be drawn.
///
/// The rendering backend is responsible for first setting the active viewport to
/// [`Self::rect`].
///
/// The rendering backend is also responsible for restoring any state, such as the bound shader
/// program, vertex array, etc.
///
/// Shape has to be clone, therefore this has to be an `Arc` instead of a `Box`.
pub callback: Arc<dyn Any + Send + Sync>,
}
impl std::fmt::Debug for PaintCallback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomShape")
.field("rect", &self.rect)
.finish_non_exhaustive()
}
}
impl std::cmp::PartialEq for PaintCallback {
fn eq(&self, other: &Self) -> bool {
self.rect.eq(&other.rect) && Arc::ptr_eq(&self.callback, &other.callback)
}
}
impl From<PaintCallback> for Shape {
#[inline(always)]
fn from(shape: PaintCallback) -> Self {
Self::Callback(shape)
}
}

View File

@@ -0,0 +1,81 @@
use crate::*;
/// A path which can be stroked and/or filled (if closed).
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PathShape {
/// Filled paths should prefer clockwise order.
pub points: Vec<Pos2>,
/// If true, connect the first and last of the points together.
/// This is required if `fill != TRANSPARENT`.
pub closed: bool,
/// Fill is only supported for convex polygons.
pub fill: Color32,
/// Color and thickness of the line.
pub stroke: PathStroke,
// TODO(emilk): Add texture support either by supplying uv for each point,
// or by some transform from points to uv (e.g. a callback or a linear transform matrix).
}
impl PathShape {
/// A line through many points.
///
/// Use [`Shape::line_segment`] instead if your line only connects two points.
#[inline]
pub fn line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self {
points,
closed: false,
fill: Default::default(),
stroke: stroke.into(),
}
}
/// A line that closes back to the start point again.
#[inline]
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self {
points,
closed: true,
fill: Default::default(),
stroke: stroke.into(),
}
}
/// A convex polygon with a fill and optional stroke.
///
/// The most performant winding order is clockwise.
#[inline]
pub fn convex_polygon(
points: Vec<Pos2>,
fill: impl Into<Color32>,
stroke: impl Into<PathStroke>,
) -> Self {
Self {
points,
closed: true,
fill: fill.into(),
stroke: stroke.into(),
}
}
/// The visual bounding rectangle (includes stroke width)
#[inline]
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
Rect::from_points(&self.points).expand(self.stroke.width / 2.0)
}
}
}
impl From<PathShape> for Shape {
#[inline(always)]
fn from(shape: PathShape) -> Self {
Self::Path(shape)
}
}

View File

@@ -0,0 +1,135 @@
use std::sync::Arc;
use crate::*;
/// How to paint a rectangle.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RectShape {
pub rect: Rect,
/// How rounded the corners are. Use `Rounding::ZERO` for no rounding.
pub rounding: Rounding,
/// How to fill the rectangle.
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
/// (for both fill and stroke) will be blurred.
///
/// This can be used to produce shadows and glow effects.
///
/// The blur is currently implemented using a simple linear blur in sRGBA gamma space.
pub blur_width: f32,
/// Controls texturing, if any.
///
/// Since most rectangles do not have a texture, this is optional and in an `Arc`,
/// so that [`RectShape`] is kept small..
pub brush: Option<Arc<Brush>>,
}
#[test]
fn rect_shape_size() {
assert_eq!(
std::mem::size_of::<RectShape>(), 48,
"RectShape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(
std::mem::size_of::<RectShape>() <= 64,
"RectShape is getting way too big!"
);
}
impl RectShape {
/// The stroke extends _outside_ the [`Rect`].
#[inline]
pub fn new(
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) -> Self {
Self {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: stroke.into(),
blur_width: 0.0,
brush: Default::default(),
}
}
#[inline]
pub fn filled(
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
) -> Self {
Self::new(rect, rounding, fill_color, Stroke::NONE)
}
/// The stroke extends _outside_ the [`Rect`].
#[inline]
pub fn stroke(rect: Rect, rounding: impl Into<Rounding>, stroke: impl Into<Stroke>) -> Self {
let fill = Color32::TRANSPARENT;
Self::new(rect, rounding, fill, stroke)
}
/// If larger than zero, the edges of the rectangle
/// (for both fill and stroke) will be blurred.
///
/// This can be used to produce shadows and glow effects.
///
/// The blur is currently implemented using a simple linear blur in `sRGBA` gamma space.
#[inline]
pub fn with_blur_width(mut self, blur_width: f32) -> Self {
self.blur_width = blur_width;
self
}
/// Set the texture to use when painting this rectangle, if any.
#[inline]
pub fn with_texture(mut self, fill_texture_id: TextureId, uv: Rect) -> Self {
self.brush = Some(Arc::new(Brush {
fill_texture_id,
uv,
}));
self
}
/// The visual bounding rectangle (includes stroke width)
#[inline]
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
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)
}
}
/// The texture to use when painting this rectangle, if any.
///
/// If no texture is set, this will return [`TextureId::default`].
pub fn fill_texture_id(&self) -> TextureId {
self.brush
.as_ref()
.map_or_else(TextureId::default, |brush| brush.fill_texture_id)
}
}
impl From<RectShape> for Shape {
#[inline(always)]
fn from(shape: RectShape) -> Self {
Self::Rect(shape)
}
}

View File

@@ -0,0 +1,563 @@
//! The different shapes that can be painted.
use std::sync::Arc;
use emath::{pos2, Align2, Pos2, Rangef, Rect, TSTransform, Vec2};
use crate::{
stroke::PathStroke,
text::{FontId, Fonts, Galley},
Color32, Mesh, Rounding, Stroke, TextureId,
};
use super::{
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PathShape, QuadraticBezierShape,
RectShape, TextShape,
};
/// A paint primitive such as a circle or a piece of text.
/// Coordinates are all screen space points (not physical pixels).
///
/// You should generally recreate your [`Shape`]s each frame,
/// but storing them should also be fine with one exception:
/// [`Shape::Text`] depends on the current `pixels_per_point` (dpi scale)
/// and so must be recreated every time `pixels_per_point` changes.
#[must_use = "Add a Shape to a Painter"]
#[derive(Clone, Debug, PartialEq)]
pub enum Shape {
/// Paint nothing. This can be useful as a placeholder.
Noop,
/// Recursively nest more shapes - sometimes a convenience to be able to do.
/// For performance reasons it is better to avoid it.
Vec(Vec<Shape>),
/// Circle with optional outline and fill.
Circle(CircleShape),
/// Ellipse with optional outline and fill.
Ellipse(EllipseShape),
/// A line between two points.
LineSegment { points: [Pos2; 2], stroke: Stroke },
/// A series of lines between points.
/// The path can have a stroke and/or fill (if closed).
Path(PathShape),
/// Rectangle with optional outline and fill.
Rect(RectShape),
/// Text.
///
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
Text(TextShape),
/// A general triangle mesh.
///
/// Can be used to display images.
///
/// Wrapped in an [`Arc`] to minimize the size of [`Shape`].
Mesh(Arc<Mesh>),
/// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
QuadraticBezier(QuadraticBezierShape),
/// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
CubicBezier(CubicBezierShape),
/// Backend-specific painting.
Callback(PaintCallback),
}
#[test]
fn shape_size() {
assert_eq!(
std::mem::size_of::<Shape>(), 64,
"Shape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(
std::mem::size_of::<Shape>() <= 64,
"Shape is getting way too big!"
);
}
#[test]
fn shape_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Shape>();
}
impl From<Vec<Self>> for Shape {
#[inline(always)]
fn from(shapes: Vec<Self>) -> Self {
Self::Vec(shapes)
}
}
impl From<Mesh> for Shape {
#[inline(always)]
fn from(mesh: Mesh) -> Self {
Self::Mesh(mesh.into())
}
}
impl From<Arc<Mesh>> for Shape {
#[inline(always)]
fn from(mesh: Arc<Mesh>) -> Self {
Self::Mesh(mesh)
}
}
/// ## Constructors
impl Shape {
/// A line between two points.
/// More efficient than calling [`Self::line`].
#[inline]
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<Stroke>) -> Self {
Self::LineSegment {
points,
stroke: stroke.into(),
}
}
/// A horizontal line.
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)],
stroke: stroke.into(),
}
}
/// A vertical line.
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)],
stroke: stroke.into(),
}
}
/// A line through many points.
///
/// Use [`Self::line_segment`] instead if your line only connects two points.
#[inline]
pub fn line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self::Path(PathShape::line(points, stroke))
}
/// A line that closes back to the start point again.
#[inline]
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> Self {
Self::Path(PathShape::closed_line(points, stroke))
}
/// Turn a line into equally spaced dots.
pub fn dotted_line(
path: &[Pos2],
color: impl Into<Color32>,
spacing: f32,
radius: f32,
) -> Vec<Self> {
let mut shapes = Vec::new();
points_from_line(path, spacing, radius, color.into(), &mut shapes);
shapes
}
/// Turn a line into dashes.
pub fn dashed_line(
path: &[Pos2],
stroke: impl Into<Stroke>,
dash_length: f32,
gap_length: f32,
) -> Vec<Self> {
let mut shapes = Vec::new();
dashes_from_line(
path,
stroke.into(),
&[dash_length],
&[gap_length],
&mut shapes,
0.,
);
shapes
}
/// Turn a line into dashes with different dash/gap lengths and a start offset.
pub fn dashed_line_with_offset(
path: &[Pos2],
stroke: impl Into<Stroke>,
dash_lengths: &[f32],
gap_lengths: &[f32],
dash_offset: f32,
) -> Vec<Self> {
let mut shapes = Vec::new();
dashes_from_line(
path,
stroke.into(),
dash_lengths,
gap_lengths,
&mut shapes,
dash_offset,
);
shapes
}
/// Turn a line into dashes. If you need to create many dashed lines use this instead of
/// [`Self::dashed_line`].
pub fn dashed_line_many(
points: &[Pos2],
stroke: impl Into<Stroke>,
dash_length: f32,
gap_length: f32,
shapes: &mut Vec<Self>,
) {
dashes_from_line(
points,
stroke.into(),
&[dash_length],
&[gap_length],
shapes,
0.,
);
}
/// Turn a line into dashes with different dash/gap lengths and a start offset. If you need to
/// create many dashed lines use this instead of [`Self::dashed_line_with_offset`].
pub fn dashed_line_many_with_offset(
points: &[Pos2],
stroke: impl Into<Stroke>,
dash_lengths: &[f32],
gap_lengths: &[f32],
dash_offset: f32,
shapes: &mut Vec<Self>,
) {
dashes_from_line(
points,
stroke.into(),
dash_lengths,
gap_lengths,
shapes,
dash_offset,
);
}
/// A convex polygon with a fill and optional stroke.
///
/// The most performant winding order is clockwise.
#[inline]
pub fn convex_polygon(
points: Vec<Pos2>,
fill: impl Into<Color32>,
stroke: impl Into<PathStroke>,
) -> Self {
Self::Path(PathShape::convex_polygon(points, fill, stroke))
}
#[inline]
pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into<Color32>) -> Self {
Self::Circle(CircleShape::filled(center, radius, fill_color))
}
#[inline]
pub fn circle_stroke(center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> Self {
Self::Circle(CircleShape::stroke(center, radius, stroke))
}
#[inline]
pub fn ellipse_filled(center: Pos2, radius: Vec2, fill_color: impl Into<Color32>) -> Self {
Self::Ellipse(EllipseShape::filled(center, radius, fill_color))
}
#[inline]
pub fn ellipse_stroke(center: Pos2, radius: Vec2, stroke: impl Into<Stroke>) -> Self {
Self::Ellipse(EllipseShape::stroke(center, radius, stroke))
}
#[inline]
pub fn rect_filled(
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
) -> Self {
Self::Rect(RectShape::filled(rect, rounding, fill_color))
}
/// The stroke extends _outside_ the [`Rect`].
#[inline]
pub fn rect_stroke(
rect: Rect,
rounding: impl Into<Rounding>,
stroke: impl Into<Stroke>,
) -> Self {
Self::Rect(RectShape::stroke(rect, rounding, stroke))
}
#[allow(clippy::needless_pass_by_value)]
pub fn text(
fonts: &Fonts,
pos: Pos2,
anchor: Align2,
text: impl ToString,
font_id: FontId,
color: Color32,
) -> Self {
let galley = fonts.layout_no_wrap(text.to_string(), font_id, color);
let rect = anchor.anchor_size(pos, galley.size());
Self::galley(rect.min, galley, color)
}
/// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color.
///
/// Any non-placeholder color in the galley takes precedence over this fallback color.
#[inline]
pub fn galley(pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) -> Self {
TextShape::new(pos, galley, fallback_color).into()
}
/// All text color in the [`Galley`] will be replaced with the given color.
#[inline]
pub fn galley_with_override_text_color(
pos: Pos2,
galley: Arc<Galley>,
text_color: Color32,
) -> Self {
TextShape::new(pos, galley, text_color)
.with_override_text_color(text_color)
.into()
}
#[inline]
#[deprecated = "Use `Shape::galley` or `Shape::galley_with_override_text_color` instead"]
pub fn galley_with_color(pos: Pos2, galley: Arc<Galley>, text_color: Color32) -> Self {
Self::galley_with_override_text_color(pos, galley, text_color)
}
#[inline]
pub fn mesh(mesh: impl Into<Arc<Mesh>>) -> Self {
let mesh = mesh.into();
debug_assert!(mesh.is_valid());
Self::Mesh(mesh)
}
/// An image at the given position.
///
/// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`
/// unless you want to crop or flip the image.
///
/// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
pub fn image(texture_id: TextureId, rect: Rect, uv: Rect, tint: Color32) -> Self {
let mut mesh = Mesh::with_texture(texture_id);
mesh.add_rect_with_uv(rect, uv, tint);
Self::mesh(mesh)
}
/// The visual bounding rectangle (includes stroke widths)
pub fn visual_bounding_rect(&self) -> Rect {
match self {
Self::Noop => Rect::NOTHING,
Self::Vec(shapes) => {
let mut rect = Rect::NOTHING;
for shape in shapes {
rect = rect.union(shape.visual_bounding_rect());
}
rect
}
Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(),
Self::Ellipse(ellipse_shape) => ellipse_shape.visual_bounding_rect(),
Self::LineSegment { points, stroke } => {
if stroke.is_empty() {
Rect::NOTHING
} else {
Rect::from_two_pos(points[0], points[1]).expand(stroke.width / 2.0)
}
}
Self::Path(path_shape) => path_shape.visual_bounding_rect(),
Self::Rect(rect_shape) => rect_shape.visual_bounding_rect(),
Self::Text(text_shape) => text_shape.visual_bounding_rect(),
Self::Mesh(mesh) => mesh.calc_bounds(),
Self::QuadraticBezier(bezier) => bezier.visual_bounding_rect(),
Self::CubicBezier(bezier) => bezier.visual_bounding_rect(),
Self::Callback(custom) => custom.rect,
}
}
}
/// ## Inspection and transforms
impl Shape {
#[inline(always)]
pub fn texture_id(&self) -> crate::TextureId {
if let Self::Mesh(mesh) = self {
mesh.texture_id
} else if let Self::Rect(rect_shape) = self {
rect_shape.fill_texture_id()
} else {
crate::TextureId::default()
}
}
/// Scale the shape by `factor`, in-place.
///
/// A wrapper around [`Self::transform`].
#[inline(always)]
pub fn scale(&mut self, factor: f32) {
self.transform(TSTransform::from_scaling(factor));
}
/// Move the shape by `delta`, in-place.
///
/// A wrapper around [`Self::transform`].
#[inline(always)]
pub fn translate(&mut self, delta: Vec2) {
self.transform(TSTransform::from_translation(delta));
}
/// Move the shape by this many points, in-place.
///
/// If using a [`PaintCallback`], note that only the rect is scaled as opposed
/// to other shapes where the stroke is also scaled.
pub fn transform(&mut self, transform: TSTransform) {
match self {
Self::Noop => {}
Self::Vec(shapes) => {
for shape in shapes {
shape.transform(transform);
}
}
Self::Circle(circle_shape) => {
circle_shape.center = transform * circle_shape.center;
circle_shape.radius *= transform.scaling;
circle_shape.stroke.width *= transform.scaling;
}
Self::Ellipse(ellipse_shape) => {
ellipse_shape.center = transform * ellipse_shape.center;
ellipse_shape.radius *= transform.scaling;
ellipse_shape.stroke.width *= transform.scaling;
}
Self::LineSegment { points, stroke } => {
for p in points {
*p = transform * *p;
}
stroke.width *= transform.scaling;
}
Self::Path(path_shape) => {
for p in &mut path_shape.points {
*p = transform * *p;
}
path_shape.stroke.width *= transform.scaling;
}
Self::Rect(rect_shape) => {
rect_shape.rect = transform * rect_shape.rect;
rect_shape.stroke.width *= transform.scaling;
rect_shape.rounding *= transform.scaling;
}
Self::Text(text_shape) => {
text_shape.pos = transform * text_shape.pos;
// Scale text:
let galley = Arc::make_mut(&mut text_shape.galley);
for placed_row in &mut galley.rows {
let row = Arc::make_mut(&mut placed_row.row);
row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds;
for v in &mut row.visuals.mesh.vertices {
v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y);
}
}
galley.mesh_bounds = transform.scaling * galley.mesh_bounds;
galley.rect = transform.scaling * galley.rect;
}
Self::Mesh(mesh) => {
Arc::make_mut(mesh).transform(transform);
}
Self::QuadraticBezier(bezier_shape) => {
bezier_shape.points[0] = transform * bezier_shape.points[0];
bezier_shape.points[1] = transform * bezier_shape.points[1];
bezier_shape.points[2] = transform * bezier_shape.points[2];
bezier_shape.stroke.width *= transform.scaling;
}
Self::CubicBezier(cubic_curve) => {
for p in &mut cubic_curve.points {
*p = transform * *p;
}
cubic_curve.stroke.width *= transform.scaling;
}
Self::Callback(shape) => {
shape.rect = transform * shape.rect;
}
}
}
}
// ----------------------------------------------------------------------------
/// Creates equally spaced filled circles from a line.
fn points_from_line(
path: &[Pos2],
spacing: f32,
radius: f32,
color: Color32,
shapes: &mut Vec<Shape>,
) {
let mut position_on_segment = 0.0;
path.windows(2).for_each(|window| {
let (start, end) = (window[0], window[1]);
let vector = end - start;
let segment_length = vector.length();
while position_on_segment < segment_length {
let new_point = start + vector * (position_on_segment / segment_length);
shapes.push(Shape::circle_filled(new_point, radius, color));
position_on_segment += spacing;
}
position_on_segment -= segment_length;
});
}
/// Creates dashes from a line.
fn dashes_from_line(
path: &[Pos2],
stroke: Stroke,
dash_lengths: &[f32],
gap_lengths: &[f32],
shapes: &mut Vec<Shape>,
dash_offset: f32,
) {
assert_eq!(dash_lengths.len(), gap_lengths.len());
let mut position_on_segment = dash_offset;
let mut drawing_dash = false;
let mut step = 0;
let steps = dash_lengths.len();
path.windows(2).for_each(|window| {
let (start, end) = (window[0], window[1]);
let vector = end - start;
let segment_length = vector.length();
let mut start_point = start;
while position_on_segment < segment_length {
let new_point = start + vector * (position_on_segment / segment_length);
if drawing_dash {
// This is the end point.
shapes.push(Shape::line_segment([start_point, new_point], stroke));
position_on_segment += gap_lengths[step];
// Increment step counter
step += 1;
if step >= steps {
step = 0;
}
} else {
// Start a new dash.
start_point = new_point;
position_on_segment += dash_lengths[step];
}
drawing_dash = !drawing_dash;
}
// If the segment ends and the dash is not finished, add the segment's end point.
if drawing_dash {
shapes.push(Shape::line_segment([start_point, end], stroke));
}
position_on_segment -= segment_length;
});
}

View File

@@ -0,0 +1,97 @@
use std::sync::Arc;
use crate::*;
/// How to paint some text on screen.
///
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TextShape {
/// Top left corner of the first character.
pub pos: Pos2,
/// The laid out text, from [`Fonts::layout_job`].
pub galley: Arc<Galley>,
/// Add this underline to the whole text.
/// You can also set an underline when creating the galley.
pub underline: Stroke,
/// Any [`Color32::PLACEHOLDER`] in the galley will be replaced by the given color.
/// Affects everything: backgrounds, glyphs, strikethrough, underline, etc.
pub fallback_color: Color32,
/// If set, the text color in the galley will be ignored and replaced
/// with the given color.
///
/// This only affects the glyphs and will NOT replace background color nor strikethrough/underline color.
pub override_text_color: Option<Color32>,
/// If set, the text will be rendered with the given opacity in gamma space
/// Affects everything: backgrounds, glyphs, strikethrough, underline, etc.
pub opacity_factor: f32,
/// Rotate text by this many radians clockwise.
/// The pivot is `pos` (the upper left corner of the text).
pub angle: f32,
}
impl TextShape {
/// The given fallback color will be used for any uncolored part of the galley (using [`Color32::PLACEHOLDER`]).
///
/// Any non-placeholder color in the galley takes precedence over this fallback color.
#[inline]
pub fn new(pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) -> Self {
Self {
pos,
galley,
underline: Stroke::NONE,
fallback_color,
override_text_color: None,
opacity_factor: 1.0,
angle: 0.0,
}
}
/// The visual bounding rectangle
#[inline]
pub fn visual_bounding_rect(&self) -> Rect {
self.galley.mesh_bounds.translate(self.pos.to_vec2())
}
#[inline]
pub fn with_underline(mut self, underline: Stroke) -> Self {
self.underline = underline;
self
}
/// Use the given color for the text, regardless of what color is already in the galley.
#[inline]
pub fn with_override_text_color(mut self, override_text_color: Color32) -> Self {
self.override_text_color = Some(override_text_color);
self
}
/// Rotate text by this many radians clockwise.
/// The pivot is `pos` (the upper left corner of the text).
#[inline]
pub fn with_angle(mut self, angle: f32) -> Self {
self.angle = angle;
self
}
/// Render text with this opacity in gamma space
#[inline]
pub fn with_opacity_factor(mut self, opacity_factor: f32) -> Self {
self.opacity_factor = opacity_factor;
self
}
}
impl From<TextShape> for Shape {
#[inline(always)]
fn from(shape: TextShape) -> Self {
Self::Text(shape)
}
}

View File

@@ -525,7 +525,7 @@ impl Path {
pub mod path {
//! Helpers for constructing paths
use crate::shape::Rounding;
use crate::Rounding;
use emath::{pos2, Pos2, Rect};
/// overwrites existing points
@@ -548,6 +548,8 @@ pub mod path {
// Duplicated vertices can happen when one side is all rounding, with no straight edge between.
let eps = f32::EPSILON * rect.size().max_elem();
let r = crate::Roundingf::from(r);
add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); // south east
if rect.width() <= r.se + r.sw + eps {
@@ -628,7 +630,7 @@ pub mod path {
let half_width = rect.width() * 0.5;
let half_height = rect.height() * 0.5;
let max_cr = half_width.min(half_height);
rounding.at_most(max_cr).at_least(0.0)
rounding.at_most(max_cr.floor() as _).at_least(0)
}
}
@@ -1289,29 +1291,6 @@ impl Tessellator {
self.clip_rect = clip_rect;
}
#[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
(point * self.pixels_per_point).round() / self.pixels_per_point
}
#[inline(always)]
pub fn round_to_pixel_center(&self, point: f32) -> f32 {
((point * self.pixels_per_point - 0.5).round() + 0.5) / self.pixels_per_point
}
#[inline(always)]
pub fn round_pos_to_pixel(&self, pos: Pos2) -> Pos2 {
pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y))
}
#[inline(always)]
pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
pos2(
self.round_to_pixel_center(pos.x),
self.round_to_pixel_center(pos.y),
)
}
/// Tessellate a clipped shape into a list of primitives.
pub fn tessellate_clipped_shape(
&mut self,
@@ -1404,7 +1383,7 @@ impl Tessellator {
return;
}
out.append(mesh);
out.append_ref(&mesh);
}
Shape::LineSegment { points, stroke } => {
self.tessellate_line_segment(points, stroke, out);
@@ -1691,14 +1670,14 @@ impl Tessellator {
/// * `rect`: the rectangle to tessellate.
/// * `out`: triangles are appended to this.
pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) {
let brush = rect.brush.as_ref();
let RectShape {
mut rect,
mut rounding,
fill,
stroke,
mut blur_width,
fill_texture_id,
uv,
..
} = *rect;
if self.options.coarse_tessellation_culling
@@ -1714,8 +1693,16 @@ impl Tessellator {
// 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);
let Stroke { width, .. } = stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke`
if width <= self.feathering && !stroke.is_empty() {
// If the stroke is thin, make sure its center is in the center of the pixel:
rect = rect
.expand(width / 2.0)
.round_to_pixel_center(self.pixels_per_point)
.shrink(width / 2.0);
} else {
rect = rect.round_to_pixels(self.pixels_per_point);
}
}
// It is common to (sometimes accidentally) create an infinitely sized rectangle.
@@ -1725,7 +1712,7 @@ impl Tessellator {
let old_feathering = self.feathering;
if old_feathering < blur_width {
if self.feathering < blur_width {
// We accomplish the blur by using a larger-than-normal feathering.
// Feathering is usually used to make the edges of a shape softer for anti-aliasing.
@@ -1741,7 +1728,7 @@ impl Tessellator {
.at_most(rect.size().min_elem() - eps)
.at_least(0.0);
rounding += Rounding::same(0.5 * blur_width);
rounding += Rounding::from(0.5 * blur_width);
self.feathering = self.feathering.max(blur_width);
}
@@ -1773,7 +1760,11 @@ impl Tessellator {
path.add_line_loop(&self.scratchpad_points);
let path_stroke = PathStroke::from(stroke).outside();
if uv.is_positive() {
if let Some(brush) = brush {
let crate::Brush {
fill_texture_id,
uv,
} = **brush;
// Textured
let uv_from_pos = |p: Pos2| {
pos2(
@@ -1830,10 +1821,7 @@ impl Tessellator {
// The contents of the galley are already snapped to pixel coordinates,
// but we need to make sure the galley ends up on the start of a physical pixel:
let galley_pos = if self.options.round_text_to_pixels {
pos2(
self.round_to_pixel(galley_pos.x),
self.round_to_pixel(galley_pos.y),
)
galley_pos.round_to_pixels(self.pixels_per_point)
} else {
*galley_pos
};
@@ -1913,13 +1901,11 @@ impl Tessellator {
);
if *underline != Stroke::NONE {
self.scratchpad_path.clear();
self.scratchpad_path.add_line_segment([
self.round_pos_to_pixel_center(row_rect.left_bottom()),
self.round_pos_to_pixel_center(row_rect.right_bottom()),
]);
self.scratchpad_path
.stroke_open(0.0, &PathStroke::from(*underline), out);
self.tessellate_line_segment(
[row_rect.left_bottom(), row_rect.right_bottom()],
*underline,
out,
);
}
}
}
@@ -2173,7 +2159,7 @@ impl Tessellator {
profiling::scope!("distribute results", tessellated.len().to_string());
for (index, mesh) in tessellated {
shapes[index].shape = Shape::Mesh(mesh);
shapes[index].shape = Shape::Mesh(mesh.into());
}
}

View File

@@ -0,0 +1,54 @@
use crate::Rect;
/// Size of the viewport in whole, physical pixels.
pub struct ViewportInPixels {
/// Physical pixel offset for left side of the viewport.
pub left_px: i32,
/// Physical pixel offset for top side of the viewport.
pub top_px: i32,
/// Physical pixel offset for bottom side of the viewport.
///
/// This is what `glViewport`, `glScissor` etc expects for the y axis.
pub from_bottom_px: i32,
/// Viewport width in physical pixels.
pub width_px: i32,
/// Viewport height in physical pixels.
pub height_px: i32,
}
impl ViewportInPixels {
/// Convert from ui points.
pub fn from_points(rect: &Rect, pixels_per_point: f32, screen_size_px: [u32; 2]) -> Self {
// Fractional pixel values for viewports are generally valid, but may cause sampling issues
// and rounding errors might cause us to get out of bounds.
// Round:
let left_px = (pixels_per_point * rect.min.x).round() as i32; // inclusive
let top_px = (pixels_per_point * rect.min.y).round() as i32; // inclusive
let right_px = (pixels_per_point * rect.max.x).round() as i32; // exclusive
let bottom_px = (pixels_per_point * rect.max.y).round() as i32; // exclusive
// Clamp to screen:
let screen_width = screen_size_px[0] as i32;
let screen_height = screen_size_px[1] as i32;
let left_px = left_px.clamp(0, screen_width);
let right_px = right_px.clamp(left_px, screen_width);
let top_px = top_px.clamp(0, screen_height);
let bottom_px = bottom_px.clamp(top_px, screen_height);
let width_px = right_px - left_px;
let height_px = bottom_px - top_px;
Self {
left_px,
top_px,
from_bottom_px: screen_height - height_px - top_px,
width_px,
height_px,
}
}
}

View File

@@ -92,10 +92,10 @@ impl Keypad {
ui.vertical(|ui| {
let window_margin = ui.spacing().window_margin;
let size_1x1 = vec2(32.0, 26.0);
let _size_1x2 = vec2(32.0, 52.0 + window_margin.top);
let _size_2x1 = vec2(64.0 + window_margin.left, 26.0);
let _size_1x2 = vec2(32.0, 52.0 + window_margin.topf());
let _size_2x1 = vec2(64.0 + window_margin.leftf(), 26.0);
ui.spacing_mut().item_spacing = Vec2::splat(window_margin.left);
ui.spacing_mut().item_spacing = Vec2::splat(window_margin.leftf());
ui.horizontal(|ui| {
if ui.add_sized(size_1x1, Button::new("1")).clicked() {

View File

@@ -49,7 +49,7 @@ fn custom_window_frame(ctx: &egui::Context, title: &str, add_contents: impl FnOn
fill: ctx.style().visuals.window_fill(),
rounding: 10.0.into(),
stroke: ctx.style().visuals.widgets.noninteractive.fg_stroke,
outer_margin: 0.5.into(), // so the stroke is within the bounds
outer_margin: 1.0.into(), // so the stroke is within the bounds
..Default::default()
};

View File

@@ -64,8 +64,8 @@ impl eframe::App for MyApp {
ui.add_space(20.0);
egui::Frame {
stroke: ui.visuals().noninteractive().bg_stroke,
inner_margin: egui::Margin::same(4.0),
outer_margin: egui::Margin::same(4.0),
inner_margin: egui::Margin::same(4),
outer_margin: egui::Margin::same(4),
..Default::default()
}
.show(ui, |ui| {
@@ -74,8 +74,8 @@ impl eframe::App for MyApp {
egui::Frame {
stroke: ui.visuals().noninteractive().bg_stroke,
inner_margin: egui::Margin::same(8.0),
outer_margin: egui::Margin::same(6.0),
inner_margin: egui::Margin::same(8),
outer_margin: egui::Margin::same(6),
..Default::default()
}
.show(ui, |ui| {
@@ -128,7 +128,7 @@ impl eframe::App for MyApp {
ui.label("UI nesting test:");
egui::Frame {
stroke: ui.visuals().noninteractive().bg_stroke,
inner_margin: egui::Margin::same(4.0),
inner_margin: egui::Margin::same(4),
..Default::default()
}
.show(ui, |ui| {
@@ -267,7 +267,7 @@ fn stack_ui(ui: &mut egui::Ui) {
fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) {
egui::Frame {
stroke: ui.style().noninteractive().fg_stroke,
inner_margin: egui::Margin::same(4.0),
inner_margin: egui::Margin::same(4),
..Default::default()
}
.show(ui, |ui| {