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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -115,7 +115,7 @@ impl WebPainterWgpu {
|
||||
let render_state = RenderState::create(
|
||||
&options.wgpu_options,
|
||||
&instance,
|
||||
&surface,
|
||||
Some(&surface),
|
||||
depth_format,
|
||||
1,
|
||||
options.dithering,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: "),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
3
crates/egui_demo_app/tests/snapshots/clock.png
Normal file
3
crates/egui_demo_app/tests/snapshots/clock.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c05cc3d48242e46a391af34cb56f72de7933bf2cead009b6cd477c21867a84e
|
||||
size 327802
|
||||
3
crates/egui_demo_app/tests/snapshots/custom3d.png
Normal file
3
crates/egui_demo_app/tests/snapshots/custom3d.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:61212e30fe1fecf5891ddad6ac795df510bfad76b21a7a8a13aa024fdad6d05e
|
||||
size 93118
|
||||
3
crates/egui_demo_app/tests/snapshots/easymarkeditor.png
Normal file
3
crates/egui_demo_app/tests/snapshots/easymarkeditor.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7bcf6e2977bed682d7bdaa0b6a6786e528662dd0791d2e6f83cf1b4852035838
|
||||
size 182833
|
||||
3
crates/egui_demo_app/tests/snapshots/imageviewer.png
Normal file
3
crates/egui_demo_app/tests/snapshots/imageviewer.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e6cc6ff64eb73ddac89ecdacd07c2176f3ab952c0db4593fccf6d11f155ec392
|
||||
size 103100
|
||||
78
crates/egui_demo_app/tests/test_demo_app.rs
Normal file
78
crates/egui_demo_app/tests/test_demo_app.rs
Normal 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}");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -307,6 +307,6 @@ mod tests {
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
harness.wgpu_snapshot("widget_gallery");
|
||||
harness.snapshot("widget_gallery");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
81
crates/egui_kittest/src/renderer.rs
Normal file
81
crates/egui_kittest/src/renderer.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
19
crates/epaint/src/brush.rs
Normal file
19
crates/epaint/src/brush.rs
Normal 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,
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
299
crates/epaint/src/marginf.rs
Normal file
299
crates/epaint/src/marginf.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
220
crates/epaint/src/rounding.rs
Normal file
220
crates/epaint/src/rounding.rs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
236
crates/epaint/src/roundingf.rs
Normal file
236
crates/epaint/src/roundingf.rs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
52
crates/epaint/src/shapes/circle_shape.rs
Normal file
52
crates/epaint/src/shapes/circle_shape.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
54
crates/epaint/src/shapes/ellipse_shape.rs
Normal file
54
crates/epaint/src/shapes/ellipse_shape.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
19
crates/epaint/src/shapes/mod.rs
Normal file
19
crates/epaint/src/shapes/mod.rs
Normal 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,
|
||||
};
|
||||
103
crates/epaint/src/shapes/paint_callback.rs
Normal file
103
crates/epaint/src/shapes/paint_callback.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
81
crates/epaint/src/shapes/path_shape.rs
Normal file
81
crates/epaint/src/shapes/path_shape.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
135
crates/epaint/src/shapes/rect_shape.rs
Normal file
135
crates/epaint/src/shapes/rect_shape.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
563
crates/epaint/src/shapes/shape.rs
Normal file
563
crates/epaint/src/shapes/shape.rs
Normal 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;
|
||||
});
|
||||
}
|
||||
97
crates/epaint/src/shapes/text_shape.rs
Normal file
97
crates/epaint/src/shapes/text_shape.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
crates/epaint/src/viewport.rs
Normal file
54
crates/epaint/src/viewport.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user