mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 13:20:05 -04:00
Merge branch 'master' into cache_galley_lines
This commit is contained in:
@@ -102,6 +102,7 @@ While using an immediate mode gui is simple, implementing one is a lot more tric
|
|||||||
* Avoid double negatives
|
* Avoid double negatives
|
||||||
* Flip `if !condition {} else {}`
|
* Flip `if !condition {} else {}`
|
||||||
* Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`)
|
* 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
|
* Break the above rules when it makes sense
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1319,6 +1319,7 @@ dependencies = [
|
|||||||
"egui",
|
"egui",
|
||||||
"egui_demo_lib",
|
"egui_demo_lib",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
|
"egui_kittest",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"image",
|
"image",
|
||||||
@@ -1396,6 +1397,7 @@ version = "0.30.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"dify",
|
"dify",
|
||||||
"document-features",
|
"document-features",
|
||||||
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -266,11 +266,10 @@ use_self = "warn"
|
|||||||
useless_transmute = "warn"
|
useless_transmute = "warn"
|
||||||
verbose_file_reads = "warn"
|
verbose_file_reads = "warn"
|
||||||
wildcard_dependencies = "warn"
|
wildcard_dependencies = "warn"
|
||||||
wildcard_imports = "warn"
|
|
||||||
zero_sized_map_values = "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"
|
iter_over_hash_type = "allow"
|
||||||
missing_assert_message = "allow"
|
missing_assert_message = "allow"
|
||||||
should_panic_without_expect = "allow"
|
should_panic_without_expect = "allow"
|
||||||
@@ -284,3 +283,4 @@ let_underscore_untyped = "allow"
|
|||||||
manual_range_contains = "allow" # this one is just worse imho
|
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
|
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
|
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).
|
/// 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.
|
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||||
#[cfg(feature = "wgpu")]
|
#[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
|
/// Raw platform window handle
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -651,6 +674,25 @@ impl HasDisplayHandle for Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// True if you are in a web environment.
|
||||||
///
|
///
|
||||||
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
||||||
@@ -794,6 +836,29 @@ pub struct IntegrationInfo {
|
|||||||
pub cpu_usage: Option<f32>,
|
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.
|
/// 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(
|
let render_state = RenderState::create(
|
||||||
&options.wgpu_options,
|
&options.wgpu_options,
|
||||||
&instance,
|
&instance,
|
||||||
&surface,
|
Some(&surface),
|
||||||
depth_format,
|
depth_format,
|
||||||
1,
|
1,
|
||||||
options.dithering,
|
options.dithering,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub use wgpu;
|
|||||||
mod renderer;
|
mod renderer;
|
||||||
|
|
||||||
pub use renderer::*;
|
pub use renderer::*;
|
||||||
use wgpu::{Adapter, Device, Instance, Queue};
|
use wgpu::{Adapter, Device, Instance, Queue, TextureFormat};
|
||||||
|
|
||||||
/// Helpers for capturing screenshots of the UI.
|
/// Helpers for capturing screenshots of the UI.
|
||||||
pub mod capture;
|
pub mod capture;
|
||||||
@@ -91,7 +91,7 @@ impl RenderState {
|
|||||||
pub async fn create(
|
pub async fn create(
|
||||||
config: &WgpuConfiguration,
|
config: &WgpuConfiguration,
|
||||||
instance: &wgpu::Instance,
|
instance: &wgpu::Instance,
|
||||||
surface: &wgpu::Surface<'static>,
|
compatible_surface: Option<&wgpu::Surface<'static>>,
|
||||||
depth_format: Option<wgpu::TextureFormat>,
|
depth_format: Option<wgpu::TextureFormat>,
|
||||||
msaa_samples: u32,
|
msaa_samples: u32,
|
||||||
dithering: bool,
|
dithering: bool,
|
||||||
@@ -113,7 +113,7 @@ impl RenderState {
|
|||||||
instance
|
instance
|
||||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
power_preference,
|
power_preference,
|
||||||
compatible_surface: Some(surface),
|
compatible_surface,
|
||||||
force_fallback_adapter: false,
|
force_fallback_adapter: false,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -186,11 +186,14 @@ impl RenderState {
|
|||||||
} => (adapter, device, queue),
|
} => (adapter, device, queue),
|
||||||
};
|
};
|
||||||
|
|
||||||
let capabilities = {
|
let surface_formats = {
|
||||||
profiling::scope!("get_capabilities");
|
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(
|
let renderer = Renderer::new(
|
||||||
&device,
|
&device,
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ impl Painter {
|
|||||||
let render_state = RenderState::create(
|
let render_state = RenderState::create(
|
||||||
&self.configuration,
|
&self.configuration,
|
||||||
&self.instance,
|
&self.instance,
|
||||||
&surface,
|
Some(&surface),
|
||||||
self.depth_format,
|
self.depth_format,
|
||||||
self.msaa_samples,
|
self.msaa_samples,
|
||||||
self.dithering,
|
self.dithering,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
|
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
|
||||||
UiStackInfo,
|
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`].
|
/// Add a background, frame and/or margin to a rectangular background of a [`Ui`].
|
||||||
///
|
///
|
||||||
@@ -73,6 +73,18 @@ pub struct Frame {
|
|||||||
pub stroke: Stroke,
|
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 {
|
impl Frame {
|
||||||
pub fn none() -> Self {
|
pub fn none() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
@@ -81,7 +93,7 @@ impl Frame {
|
|||||||
/// For when you want to group a few widgets together within a frame.
|
/// For when you want to group a few widgets together within a frame.
|
||||||
pub fn group(style: &Style) -> Self {
|
pub fn group(style: &Style) -> Self {
|
||||||
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,
|
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||||
stroke: style.visuals.widgets.noninteractive.bg_stroke,
|
stroke: style.visuals.widgets.noninteractive.bg_stroke,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -90,7 +102,7 @@ impl Frame {
|
|||||||
|
|
||||||
pub fn side_top_panel(style: &Style) -> Self {
|
pub fn side_top_panel(style: &Style) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner_margin: Margin::symmetric(8.0, 2.0),
|
inner_margin: Margin::symmetric(8, 2),
|
||||||
fill: style.visuals.panel_fill,
|
fill: style.visuals.panel_fill,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -98,7 +110,7 @@ impl Frame {
|
|||||||
|
|
||||||
pub fn central_panel(style: &Style) -> Self {
|
pub fn central_panel(style: &Style) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner_margin: Margin::same(8.0),
|
inner_margin: Margin::same(8),
|
||||||
fill: style.visuals.panel_fill,
|
fill: style.visuals.panel_fill,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -143,7 +155,7 @@ impl Frame {
|
|||||||
/// and in dark mode this will be very dark.
|
/// and in dark mode this will be very dark.
|
||||||
pub fn canvas(style: &Style) -> Self {
|
pub fn canvas(style: &Style) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner_margin: Margin::same(2.0),
|
inner_margin: Margin::same(2),
|
||||||
rounding: style.visuals.widgets.noninteractive.rounding,
|
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||||
fill: style.visuals.extreme_bg_color,
|
fill: style.visuals.extreme_bg_color,
|
||||||
stroke: style.visuals.window_stroke(),
|
stroke: style.visuals.window_stroke(),
|
||||||
@@ -213,10 +225,10 @@ impl Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Frame {
|
impl Frame {
|
||||||
/// inner margin plus outer margin.
|
/// Inner margin plus outer margin.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn total_margin(&self) -> Margin {
|
pub fn total_margin(&self) -> Marginf {
|
||||||
self.inner_margin + self.outer_margin
|
Marginf::from(self.inner_margin) + Marginf::from(self.outer_margin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use crate::{
|
|||||||
TextStyle, Ui, UiKind, Vec2b, WidgetInfo, WidgetRect, WidgetText, WidgetType,
|
TextStyle, Ui, UiKind, Vec2b, WidgetInfo, WidgetRect, WidgetText, WidgetType,
|
||||||
};
|
};
|
||||||
use emath::GuiRounding as _;
|
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::scroll_area::ScrollBarVisibility;
|
||||||
use super::{area, resize, Area, Frame, Resize, ScrollArea};
|
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
|
// 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 (title_bar_height, title_content_spacing) = if with_title_bar {
|
||||||
let style = ctx.style();
|
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;
|
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);
|
let half_height = (height / 2.0).round() as _;
|
||||||
window_frame.rounding.nw = window_frame.rounding.nw.clamp(0.0, height / 2.0);
|
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)
|
(height, spacing)
|
||||||
} else {
|
} else {
|
||||||
(0.0, 0.0)
|
(0.0, 0.0)
|
||||||
@@ -603,8 +606,8 @@ impl<'open> Window<'open> {
|
|||||||
let mut round = window_frame.rounding;
|
let mut round = window_frame.rounding;
|
||||||
|
|
||||||
if !is_collapsed {
|
if !is_collapsed {
|
||||||
round.se = 0.0;
|
round.se = 0;
|
||||||
round.sw = 0.0;
|
round.sw = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
area_content_ui.painter().set(
|
area_content_ui.painter().set(
|
||||||
@@ -682,6 +685,7 @@ fn paint_resize_corner(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Adjust the corner offset to accommodate for window rounding
|
// Adjust the corner offset to accommodate for window rounding
|
||||||
|
let radius = radius as f32;
|
||||||
let offset =
|
let offset =
|
||||||
((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0);
|
((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;
|
bottom = interaction.bottom.hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rounding = ui.visuals().window_rounding;
|
let rounding = Roundingf::from(ui.visuals().window_rounding);
|
||||||
let Rect { min, max } = rect;
|
let Rect { min, max } = rect;
|
||||||
|
|
||||||
let mut points = Vec::new();
|
let mut points = Vec::new();
|
||||||
|
|||||||
@@ -1238,8 +1238,8 @@ impl Default for Spacing {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
item_spacing: vec2(8.0, 3.0),
|
item_spacing: vec2(8.0, 3.0),
|
||||||
window_margin: Margin::same(6.0),
|
window_margin: Margin::same(6),
|
||||||
menu_margin: Margin::same(6.0),
|
menu_margin: Margin::same(6),
|
||||||
button_padding: vec2(4.0, 1.0),
|
button_padding: vec2(4.0, 1.0),
|
||||||
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
|
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
|
||||||
interact_size: vec2(40.0, 18.0),
|
interact_size: vec2(40.0, 18.0),
|
||||||
@@ -1291,25 +1291,25 @@ impl Visuals {
|
|||||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||||
|
|
||||||
window_rounding: Rounding::same(6.0),
|
window_rounding: Rounding::same(6),
|
||||||
window_shadow: Shadow {
|
window_shadow: Shadow {
|
||||||
offset: vec2(10.0, 20.0),
|
offset: [10, 20],
|
||||||
blur: 15.0,
|
blur: 15,
|
||||||
spread: 0.0,
|
spread: 0,
|
||||||
color: Color32::from_black_alpha(96),
|
color: Color32::from_black_alpha(96),
|
||||||
},
|
},
|
||||||
window_fill: Color32::from_gray(27),
|
window_fill: Color32::from_gray(27),
|
||||||
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||||
window_highlight_topmost: true,
|
window_highlight_topmost: true,
|
||||||
|
|
||||||
menu_rounding: Rounding::same(6.0),
|
menu_rounding: Rounding::same(6),
|
||||||
|
|
||||||
panel_fill: Color32::from_gray(27),
|
panel_fill: Color32::from_gray(27),
|
||||||
|
|
||||||
popup_shadow: Shadow {
|
popup_shadow: Shadow {
|
||||||
offset: vec2(6.0, 10.0),
|
offset: [6, 10],
|
||||||
blur: 8.0,
|
blur: 8,
|
||||||
spread: 0.0,
|
spread: 0,
|
||||||
color: Color32::from_black_alpha(96),
|
color: Color32::from_black_alpha(96),
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1349,9 +1349,9 @@ impl Visuals {
|
|||||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||||
|
|
||||||
window_shadow: Shadow {
|
window_shadow: Shadow {
|
||||||
offset: vec2(10.0, 20.0),
|
offset: [10, 20],
|
||||||
blur: 15.0,
|
blur: 15,
|
||||||
spread: 0.0,
|
spread: 0,
|
||||||
color: Color32::from_black_alpha(25),
|
color: Color32::from_black_alpha(25),
|
||||||
},
|
},
|
||||||
window_fill: Color32::from_gray(248),
|
window_fill: Color32::from_gray(248),
|
||||||
@@ -1360,9 +1360,9 @@ impl Visuals {
|
|||||||
panel_fill: Color32::from_gray(248),
|
panel_fill: Color32::from_gray(248),
|
||||||
|
|
||||||
popup_shadow: Shadow {
|
popup_shadow: Shadow {
|
||||||
offset: vec2(6.0, 10.0),
|
offset: [6, 10],
|
||||||
blur: 8.0,
|
blur: 8,
|
||||||
spread: 0.0,
|
spread: 0,
|
||||||
color: Color32::from_black_alpha(25),
|
color: Color32::from_black_alpha(25),
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1412,7 +1412,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(27),
|
bg_fill: Color32::from_gray(27),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
|
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
|
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,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
inactive: WidgetVisuals {
|
inactive: WidgetVisuals {
|
||||||
@@ -1420,7 +1420,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(60), // checkbox background
|
bg_fill: Color32::from_gray(60), // checkbox background
|
||||||
bg_stroke: Default::default(),
|
bg_stroke: Default::default(),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
hovered: WidgetVisuals {
|
hovered: WidgetVisuals {
|
||||||
@@ -1428,7 +1428,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(70),
|
bg_fill: Color32::from_gray(70),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
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)),
|
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||||
rounding: Rounding::same(3.0),
|
rounding: Rounding::same(3),
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
active: WidgetVisuals {
|
active: WidgetVisuals {
|
||||||
@@ -1436,7 +1436,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(55),
|
bg_fill: Color32::from_gray(55),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2),
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
open: WidgetVisuals {
|
open: WidgetVisuals {
|
||||||
@@ -1444,7 +1444,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(27),
|
bg_fill: Color32::from_gray(27),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1457,7 +1457,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(248),
|
bg_fill: Color32::from_gray(248),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
|
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
|
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,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
inactive: WidgetVisuals {
|
inactive: WidgetVisuals {
|
||||||
@@ -1465,7 +1465,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(230), // checkbox background
|
bg_fill: Color32::from_gray(230), // checkbox background
|
||||||
bg_stroke: Default::default(),
|
bg_stroke: Default::default(),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
hovered: WidgetVisuals {
|
hovered: WidgetVisuals {
|
||||||
@@ -1473,7 +1473,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(220),
|
bg_fill: Color32::from_gray(220),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
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),
|
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||||
rounding: Rounding::same(3.0),
|
rounding: Rounding::same(3),
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
active: WidgetVisuals {
|
active: WidgetVisuals {
|
||||||
@@ -1481,7 +1481,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(165),
|
bg_fill: Color32::from_gray(165),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2),
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
open: WidgetVisuals {
|
open: WidgetVisuals {
|
||||||
@@ -1489,7 +1489,7 @@ impl Widgets {
|
|||||||
bg_fill: Color32::from_gray(220),
|
bg_fill: Color32::from_gray(220),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -2371,9 +2371,17 @@ impl Widget for &mut Margin {
|
|||||||
|
|
||||||
// Apply the checkbox:
|
// Apply the checkbox:
|
||||||
if same {
|
if same {
|
||||||
*self = Margin::same((self.left + self.right + self.top + self.bottom) / 4.0);
|
*self =
|
||||||
} else if self.is_same() {
|
Margin::from((self.leftf() + self.rightf() + self.topf() + self.bottomf()) / 4.0);
|
||||||
self.right *= 1.00001; // prevent collapsing into sameness
|
} 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
|
response
|
||||||
@@ -2420,9 +2428,16 @@ impl Widget for &mut Rounding {
|
|||||||
|
|
||||||
// Apply the checkbox:
|
// Apply the checkbox:
|
||||||
if same {
|
if same {
|
||||||
*self = Rounding::same((self.nw + self.ne + self.sw + self.se) / 4.0);
|
*self = Rounding::from(self.average());
|
||||||
} else if self.is_same() {
|
} else {
|
||||||
self.se *= 1.00001; // prevent collapsing into sameness
|
// Make sure we aren't same:
|
||||||
|
if self.is_same() {
|
||||||
|
if self.average() == 0.0 {
|
||||||
|
self.se = 1;
|
||||||
|
} else {
|
||||||
|
self.se -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
@@ -2441,13 +2456,13 @@ impl Widget for &mut Shadow {
|
|||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
crate::Grid::new("shadow_ui").show(ui, |ui| {
|
crate::Grid::new("shadow_ui").show(ui, |ui| {
|
||||||
ui.add(
|
ui.add(
|
||||||
DragValue::new(&mut offset.x)
|
DragValue::new(&mut offset[0])
|
||||||
.speed(1.0)
|
.speed(1.0)
|
||||||
.range(-100.0..=100.0)
|
.range(-100.0..=100.0)
|
||||||
.prefix("x: "),
|
.prefix("x: "),
|
||||||
);
|
);
|
||||||
ui.add(
|
ui.add(
|
||||||
DragValue::new(&mut offset.y)
|
DragValue::new(&mut offset[1])
|
||||||
.speed(1.0)
|
.speed(1.0)
|
||||||
.range(-100.0..=100.0)
|
.range(-100.0..=100.0)
|
||||||
.prefix("y: "),
|
.prefix("y: "),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
|
|||||||
|
|
||||||
show_color_at(ui.painter(), color, rect);
|
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()
|
ui.painter()
|
||||||
.rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border
|
.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::{
|
use crate::{
|
||||||
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
|
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
|
||||||
pos2, Color32, Context, Id, Mesh, Painter, Rect, Response, Rounding, Sense, Shape, Spinner,
|
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.
|
/// A widget which displays an image.
|
||||||
@@ -822,15 +822,10 @@ pub fn paint_texture_at(
|
|||||||
painter.add(Shape::mesh(mesh));
|
painter.add(Shape::mesh(mesh));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
painter.add(RectShape {
|
painter.add(
|
||||||
rect,
|
RectShape::filled(rect, options.rounding, options.tint)
|
||||||
rounding: options.rounding,
|
.with_texture(texture.id, options.uv),
|
||||||
fill: options.tint,
|
);
|
||||||
stroke: Stroke::NONE,
|
|
||||||
blur_width: 0.0,
|
|
||||||
fill_texture_id: texture.id,
|
|
||||||
uv: options.uv,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ impl Widget for ProgressBar {
|
|||||||
let rounding = rounding.unwrap_or_else(|| corner_radius.into());
|
let rounding = rounding.unwrap_or_else(|| corner_radius.into());
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.rect(outer_rect, rounding, visuals.extreme_bg_color, Stroke::NONE);
|
.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 filled_width = (outer_rect.width() * progress).at_least(min_width);
|
||||||
let inner_rect =
|
let inner_rect =
|
||||||
Rect::from_min_size(outer_rect.min, vec2(filled_width, outer_rect.height()));
|
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.
|
// The trailing rect has to be drawn differently depending on the orientation.
|
||||||
match self.orientation {
|
match self.orientation {
|
||||||
SliderOrientation::Horizontal => {
|
SliderOrientation::Horizontal => {
|
||||||
trailing_rail_rect.max.x = center.x + rounding.nw;
|
trailing_rail_rect.max.x = center.x + rounding.nw as f32;
|
||||||
}
|
}
|
||||||
SliderOrientation::Vertical => {
|
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,
|
layouter: None,
|
||||||
password: false,
|
password: false,
|
||||||
frame: true,
|
frame: true,
|
||||||
margin: Margin::symmetric(4.0, 2.0),
|
margin: Margin::symmetric(4, 2),
|
||||||
multiline: true,
|
multiline: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
desired_width: None,
|
desired_width: None,
|
||||||
|
|||||||
@@ -93,3 +93,6 @@ rfd = { version = "0.15", optional = true }
|
|||||||
wasm-bindgen = "=0.2.95"
|
wasm-bindgen = "=0.2.95"
|
||||||
wasm-bindgen-futures.workspace = true
|
wasm-bindgen-futures.workspace = true
|
||||||
web-sys.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) {
|
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
||||||
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
|
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
|
||||||
ui.horizontal_centered(|ui| {
|
ui.horizontal_centered(|ui| {
|
||||||
ui.label("URI:");
|
let label = ui.label("URI:");
|
||||||
ui.text_edit_singleline(&mut self.uri_edit_text);
|
ui.text_edit_singleline(&mut self.uri_edit_text)
|
||||||
|
.labelled_by(label.id);
|
||||||
if ui.small_button("✔").clicked() {
|
if ui.small_button("✔").clicked() {
|
||||||
ctx.forget_image(&self.current_uri);
|
ctx.forget_image(&self.current_uri);
|
||||||
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
|
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ mod backend_panel;
|
|||||||
mod frame_history;
|
mod frame_history;
|
||||||
mod wrap_app;
|
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.
|
/// Time of day as seconds since midnight. Used for clock in demo app.
|
||||||
pub(crate) fn seconds_since_midnight() -> f64 {
|
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))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct FractalClockApp {
|
pub struct FractalClockApp {
|
||||||
fractal_clock: crate::apps::FractalClock,
|
fractal_clock: crate::apps::FractalClock,
|
||||||
|
pub mock_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for FractalClockApp {
|
impl eframe::App for FractalClockApp {
|
||||||
@@ -46,7 +47,7 @@ impl eframe::App for FractalClockApp {
|
|||||||
.frame(egui::Frame::dark_canvas(&ctx.style()))
|
.frame(egui::Frame::dark_canvas(&ctx.style()))
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
self.fractal_clock
|
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)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
enum Anchor {
|
pub enum Anchor {
|
||||||
Demo,
|
Demo,
|
||||||
|
|
||||||
EasyMarkEditor,
|
EasyMarkEditor,
|
||||||
@@ -161,7 +162,7 @@ pub struct State {
|
|||||||
http: crate::apps::HttpApp,
|
http: crate::apps::HttpApp,
|
||||||
#[cfg(feature = "image_viewer")]
|
#[cfg(feature = "image_viewer")]
|
||||||
image_viewer: crate::apps::ImageViewer,
|
image_viewer: crate::apps::ImageViewer,
|
||||||
clock: FractalClockApp,
|
pub clock: FractalClockApp,
|
||||||
rendering_test: ColorTestApp,
|
rendering_test: ColorTestApp,
|
||||||
|
|
||||||
selected_anchor: Anchor,
|
selected_anchor: Anchor,
|
||||||
@@ -170,7 +171,7 @@ pub struct State {
|
|||||||
|
|
||||||
/// Wraps many demo/test apps into one.
|
/// Wraps many demo/test apps into one.
|
||||||
pub struct WrapApp {
|
pub struct WrapApp {
|
||||||
state: State,
|
pub state: State,
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
custom3d: Option<crate::apps::Custom3d>,
|
custom3d: Option<crate::apps::Custom3d>,
|
||||||
@@ -203,7 +204,9 @@ impl WrapApp {
|
|||||||
slf
|
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![
|
let mut vec = vec![
|
||||||
(
|
(
|
||||||
"✨ Demos",
|
"✨ 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;
|
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 {
|
if let Err(err) = result {
|
||||||
errors.push(err.to_string());
|
errors.push(err.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ impl Default for FrameDemo {
|
|||||||
outer_margin: 24.0.into(),
|
outer_margin: 24.0.into(),
|
||||||
rounding: 14.0.into(),
|
rounding: 14.0.into(),
|
||||||
shadow: egui::Shadow {
|
shadow: egui::Shadow {
|
||||||
offset: [8.0, 12.0].into(),
|
offset: [8, 12],
|
||||||
blur: 16.0,
|
blur: 16,
|
||||||
spread: 0.0,
|
spread: 0,
|
||||||
color: egui::Color32::from_black_alpha(180),
|
color: egui::Color32::from_black_alpha(180),
|
||||||
},
|
},
|
||||||
fill: egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128),
|
fill: egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128),
|
||||||
|
|||||||
@@ -235,21 +235,21 @@ mod tests {
|
|||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
harness.run();
|
harness.run();
|
||||||
results.push(harness.try_wgpu_snapshot("modals_1"));
|
results.push(harness.try_snapshot("modals_1"));
|
||||||
|
|
||||||
harness.get_by_label("Save").click();
|
harness.get_by_label("Save").click();
|
||||||
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
||||||
harness.run();
|
harness.run();
|
||||||
harness.run();
|
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();
|
harness.get_by_label("Yes Please").click();
|
||||||
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
||||||
harness.run();
|
harness.run();
|
||||||
harness.run();
|
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 {
|
for result in results {
|
||||||
result.unwrap();
|
result.unwrap();
|
||||||
@@ -282,6 +282,6 @@ mod tests {
|
|||||||
harness.run();
|
harness.run();
|
||||||
|
|
||||||
// This snapshots should show the progress bar modal on top of the save modal.
|
// 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| {
|
.show(ui.ctx(), |ui| {
|
||||||
ui.set_clip_rect(transform.inverse() * rect);
|
ui.set_clip_rect(transform.inverse() * rect);
|
||||||
egui::Frame::default()
|
egui::Frame::default()
|
||||||
.rounding(egui::Rounding::same(4.0))
|
.rounding(egui::Rounding::same(4))
|
||||||
.inner_margin(egui::Margin::same(8.0))
|
.inner_margin(egui::Margin::same(8))
|
||||||
.stroke(ui.ctx().style().visuals.window_stroke)
|
.stroke(ui.ctx().style().visuals.window_stroke)
|
||||||
.fill(ui.style().visuals.panel_fill)
|
.fill(ui.style().visuals.panel_fill)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
|||||||
@@ -307,6 +307,6 @@ mod tests {
|
|||||||
|
|
||||||
harness.fit_contents();
|
harness.fit_contents();
|
||||||
|
|
||||||
harness.wgpu_snapshot("widget_gallery");
|
harness.snapshot("widget_gallery");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -703,7 +703,7 @@ mod tests {
|
|||||||
|
|
||||||
harness.fit_contents();
|
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 {
|
if let Err(err) = result {
|
||||||
errors.push(err);
|
errors.push(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,14 +134,14 @@ impl<'a> Widget for DatePickerButton<'a> {
|
|||||||
let mut pos = button_response.rect.left_bottom();
|
let mut pos = button_response.rect.left_bottom();
|
||||||
let width_with_padding = width
|
let width_with_padding = width
|
||||||
+ ui.style().spacing.item_spacing.x
|
+ ui.style().spacing.item_spacing.x
|
||||||
+ ui.style().spacing.window_margin.left
|
+ ui.style().spacing.window_margin.leftf()
|
||||||
+ ui.style().spacing.window_margin.right;
|
+ ui.style().spacing.window_margin.rightf();
|
||||||
if pos.x + width_with_padding > ui.clip_rect().right() {
|
if pos.x + width_with_padding > ui.clip_rect().right() {
|
||||||
pos.x = button_response.rect.right() - width_with_padding;
|
pos.x = button_response.rect.right() - width_with_padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to make sure the calendar never is displayed out of window
|
// 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
|
//TODO(elwerene): Better positioning
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,19 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Adds a wgpu-based test renderer.
|
# 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.
|
# Adds a dify-based image snapshot utility.
|
||||||
snapshot = ["dep:dify", "dep:image", "image/png"]
|
snapshot = ["dep:dify", "dep:image", "image/png"]
|
||||||
|
|
||||||
|
# Allows testing eframe::App
|
||||||
|
eframe = ["dep:eframe", "eframe/accesskit"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
kittest.workspace = true
|
kittest.workspace = true
|
||||||
egui = { workspace = true, features = ["accesskit"] }
|
egui = { workspace = true, features = ["accesskit"] }
|
||||||
|
eframe = { workspace = true, optional = true }
|
||||||
|
|
||||||
# wgpu dependencies
|
# wgpu dependencies
|
||||||
egui-wgpu = { workspace = true, optional = true }
|
egui-wgpu = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ fn main() {
|
|||||||
|
|
||||||
// You can even render the ui and do image snapshot tests
|
// You can even render the ui and do image snapshot tests
|
||||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
||||||
harness.wgpu_snapshot("readme_example");
|
harness.snapshot("readme_example");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Snapshot testing
|
## Snapshot testing
|
||||||
There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features.
|
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`.
|
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.
|
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 AppKindContext<'a> = Box<dyn FnMut(&egui::Context) + 'a>;
|
||||||
type AppKindUi<'a> = Box<dyn FnMut(&mut egui::Ui) + '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> {
|
pub(crate) enum AppKind<'a, State> {
|
||||||
Context(AppKindContext<'a>),
|
Context(AppKindContext<'a>),
|
||||||
Ui(AppKindUi<'a>),
|
Ui(AppKindUi<'a>),
|
||||||
ContextState(AppKindContextState<'a, State>),
|
ContextState(AppKindContextState<'a, State>),
|
||||||
UiState(AppKindUiState<'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> {
|
impl<'a, State> AppKind<'a, State> {
|
||||||
pub fn run(
|
pub fn run(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -54,6 +39,12 @@ impl<'a, State> AppKind<'a, State> {
|
|||||||
f(ctx, state);
|
f(ctx, state);
|
||||||
None
|
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)),
|
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 {
|
.show(ui, |ui| match self {
|
||||||
AppKind::Ui(f) => f(ui),
|
AppKind::Ui(f) => f(ui),
|
||||||
AppKind::UiState(f) => f(ui, state),
|
AppKind::UiState(f) => f(ui, state),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(
|
||||||
|
"run_ui should only be called with AppKind::Ui or AppKind UiState"
|
||||||
|
),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.response
|
.response
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::app_kind::AppKind;
|
use crate::app_kind::AppKind;
|
||||||
use crate::Harness;
|
use crate::wgpu::WgpuTestRenderer;
|
||||||
|
use crate::{Harness, LazyRenderer, TestRenderer};
|
||||||
use egui::{Pos2, Rect, Vec2};
|
use egui::{Pos2, Rect, Vec2};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ pub struct HarnessBuilder<State = ()> {
|
|||||||
pub(crate) screen_rect: Rect,
|
pub(crate) screen_rect: Rect,
|
||||||
pub(crate) pixels_per_point: f32,
|
pub(crate) pixels_per_point: f32,
|
||||||
pub(crate) state: PhantomData<State>,
|
pub(crate) state: PhantomData<State>,
|
||||||
|
pub(crate) renderer: Box<dyn TestRenderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State> Default for HarnessBuilder<State> {
|
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)),
|
screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)),
|
||||||
pixels_per_point: 1.0,
|
pixels_per_point: 1.0,
|
||||||
state: PhantomData,
|
state: PhantomData,
|
||||||
|
renderer: Box::new(LazyRenderer::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,29 @@ impl<State> HarnessBuilder<State> {
|
|||||||
self
|
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.
|
/// 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.
|
/// 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,
|
app: impl FnMut(&egui::Context, &mut State) + 'a,
|
||||||
state: State,
|
state: State,
|
||||||
) -> Harness<'a, 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.
|
/// 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,
|
app: impl FnMut(&mut egui::Ui, &mut State) + 'a,
|
||||||
state: State,
|
state: State,
|
||||||
) -> Harness<'a, 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> {
|
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.
|
/// 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> {
|
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::*;
|
pub use snapshot::*;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
mod app_kind;
|
mod app_kind;
|
||||||
|
mod renderer;
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
mod texture_to_image;
|
mod texture_to_image;
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
pub mod wgpu;
|
pub mod wgpu;
|
||||||
|
|
||||||
pub use kittest;
|
pub use kittest;
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
use crate::app_kind::AppKind;
|
use crate::app_kind::AppKind;
|
||||||
use crate::event::EventState;
|
use crate::event::EventState;
|
||||||
|
|
||||||
pub use builder::*;
|
pub use builder::*;
|
||||||
use egui::{Pos2, Rect, TexturesDelta, Vec2, ViewportId};
|
pub use renderer::*;
|
||||||
|
|
||||||
|
use egui::{Modifiers, Pos2, Rect, Vec2, ViewportId};
|
||||||
use kittest::{Node, Queryable};
|
use kittest::{Node, Queryable};
|
||||||
|
|
||||||
/// The test Harness. This contains everything needed to run the test.
|
/// The test Harness. This contains everything needed to run the test.
|
||||||
@@ -37,11 +40,11 @@ pub struct Harness<'a, State = ()> {
|
|||||||
input: egui::RawInput,
|
input: egui::RawInput,
|
||||||
kittest: kittest::State,
|
kittest: kittest::State,
|
||||||
output: egui::FullOutput,
|
output: egui::FullOutput,
|
||||||
texture_deltas: Vec<TexturesDelta>,
|
|
||||||
app: AppKind<'a, State>,
|
app: AppKind<'a, State>,
|
||||||
event_state: EventState,
|
event_state: EventState,
|
||||||
response: Option<egui::Response>,
|
response: Option<egui::Response>,
|
||||||
state: State,
|
state: State,
|
||||||
|
renderer: Box<dyn TestRenderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, State> Debug for Harness<'a, State> {
|
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> {
|
impl<'a, State> Harness<'a, State> {
|
||||||
pub(crate) fn from_builder(
|
pub(crate) fn from_builder(
|
||||||
builder: &HarnessBuilder<State>,
|
builder: HarnessBuilder<State>,
|
||||||
mut app: AppKind<'a, State>,
|
mut app: AppKind<'a, State>,
|
||||||
mut state: State,
|
mut state: State,
|
||||||
|
ctx: Option<egui::Context>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ctx = egui::Context::default();
|
let ctx = ctx.unwrap_or_default();
|
||||||
ctx.enable_accesskit();
|
ctx.enable_accesskit();
|
||||||
let mut input = egui::RawInput {
|
let mut input = egui::RawInput {
|
||||||
screen_rect: Some(builder.screen_rect),
|
screen_rect: Some(builder.screen_rect),
|
||||||
@@ -73,6 +77,9 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
response = app.run(ctx, &mut state, false);
|
response = app.run(ctx, &mut state, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut renderer = builder.renderer;
|
||||||
|
renderer.handle_delta(&output.textures_delta);
|
||||||
|
|
||||||
let mut harness = Self {
|
let mut harness = Self {
|
||||||
app,
|
app,
|
||||||
ctx,
|
ctx,
|
||||||
@@ -84,11 +91,11 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
.take()
|
.take()
|
||||||
.expect("AccessKit was disabled"),
|
.expect("AccessKit was disabled"),
|
||||||
),
|
),
|
||||||
texture_deltas: vec![mem::take(&mut output.textures_delta)],
|
|
||||||
output,
|
output,
|
||||||
response,
|
response,
|
||||||
event_state: EventState::default(),
|
event_state: EventState::default(),
|
||||||
state,
|
state,
|
||||||
|
renderer,
|
||||||
};
|
};
|
||||||
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
||||||
harness.run();
|
harness.run();
|
||||||
@@ -153,6 +160,15 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
Self::builder().build_ui_state(app, 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.
|
/// Set the size of the window.
|
||||||
/// Note: If you only want to set the size once at the beginning,
|
/// Note: If you only want to set the size once at the beginning,
|
||||||
/// prefer using [`HarnessBuilder::with_size`].
|
/// prefer using [`HarnessBuilder::with_size`].
|
||||||
@@ -194,8 +210,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
.take()
|
.take()
|
||||||
.expect("AccessKit was disabled"),
|
.expect("AccessKit was disabled"),
|
||||||
);
|
);
|
||||||
self.texture_deltas
|
self.renderer.handle_delta(&output.textures_delta);
|
||||||
.push(mem::take(&mut output.textures_delta));
|
|
||||||
self.output = output;
|
self.output = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,21 +268,35 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// Press a key.
|
/// Press a key.
|
||||||
/// This will create a key down event and a key up event.
|
/// This will create a key down event and a key up event.
|
||||||
pub fn press_key(&mut self, key: egui::Key) {
|
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 {
|
self.input.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers: Default::default(),
|
modifiers,
|
||||||
repeat: false,
|
repeat: false,
|
||||||
physical_key: None,
|
physical_key: None,
|
||||||
});
|
});
|
||||||
self.input.events.push(egui::Event::Key {
|
self.input.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
modifiers: Default::default(),
|
modifiers,
|
||||||
repeat: false,
|
repeat: false,
|
||||||
physical_key: None,
|
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.
|
/// 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
|
/// The error that occurred
|
||||||
err: ImageError,
|
err: ImageError,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Error rendering the image
|
||||||
|
RenderError {
|
||||||
|
/// The error that occurred
|
||||||
|
err: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOW_TO_UPDATE_SCREENSHOTS: &str =
|
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 {
|
impl Display for SnapshotError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -106,24 +112,28 @@ impl Display for SnapshotError {
|
|||||||
diff,
|
diff,
|
||||||
diff_path,
|
diff_path,
|
||||||
} => {
|
} => {
|
||||||
|
let diff_path = std::path::absolute(diff_path).unwrap_or(diff_path.clone());
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"'{name}' Image did not match snapshot. Diff: {diff}, {diff_path:?}. {HOW_TO_UPDATE_SCREENSHOTS}"
|
"'{name}' Image did not match snapshot. Diff: {diff}, {diff_path:?}. {HOW_TO_UPDATE_SCREENSHOTS}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Self::OpenSnapshot { path, err } => match err {
|
Self::OpenSnapshot { path, err } => {
|
||||||
ImageError::IoError(io) => match io.kind() {
|
let path = std::path::absolute(path).unwrap_or(path.clone());
|
||||||
ErrorKind::NotFound => {
|
match err {
|
||||||
write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
|
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 => {
|
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 {
|
Self::SizeMismatch {
|
||||||
name,
|
name,
|
||||||
expected,
|
expected,
|
||||||
@@ -135,8 +145,12 @@ impl Display for SnapshotError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Self::WriteSnapshot { path, err } => {
|
Self::WriteSnapshot { path, err } => {
|
||||||
|
let path = std::path::absolute(path).unwrap_or(path.clone());
|
||||||
write!(f, "Error writing snapshot: {err:?}\nAt: {path:?}")
|
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")]
|
#[cfg(feature = "wgpu")]
|
||||||
impl<State> Harness<'_, State> {
|
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.
|
/// with custom options.
|
||||||
///
|
///
|
||||||
/// If you want to change the default options for your whole project, you could create an
|
/// 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.
|
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
|
||||||
/// You could additionally use the
|
/// You could additionally use the
|
||||||
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
|
/// [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 files will be saved under [`SnapshotOptions::output_path`].
|
||||||
/// The snapshot will be saved under `{output_path}/{name}.png`.
|
/// 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`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
|
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
|
||||||
/// reading or writing the snapshot.
|
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
|
||||||
pub fn try_wgpu_snapshot_options(
|
pub fn try_snapshot_options(
|
||||||
&self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
options: &SnapshotOptions,
|
options: &SnapshotOptions,
|
||||||
) -> Result<(), SnapshotError> {
|
) -> 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)
|
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 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`.
|
/// 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`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error
|
/// Returns a [`SnapshotError`] if the image does not match the snapshot, if there was an
|
||||||
/// reading or writing the snapshot.
|
/// error reading or writing the snapshot, if the rendering fails or if no default renderer is available.
|
||||||
pub fn try_wgpu_snapshot(&self, name: &str) -> Result<(), SnapshotError> {
|
pub fn try_snapshot(&mut self, name: &str) -> Result<(), SnapshotError> {
|
||||||
let image = crate::wgpu::TestRenderer::new().render(self);
|
let image = self
|
||||||
|
.render()
|
||||||
|
.map_err(|err| SnapshotError::RenderError { err })?;
|
||||||
try_image_snapshot(&image, name)
|
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.
|
/// with custom options.
|
||||||
///
|
///
|
||||||
/// If you want to change the default options for your whole project, you could create an
|
/// 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.
|
/// new `my_image_snapshot` function on the Harness that calls this function with the desired options.
|
||||||
/// You could additionally use the
|
/// You could additionally use the
|
||||||
/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods)
|
/// [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 files will be saved under [`SnapshotOptions::output_path`].
|
||||||
/// The snapshot will be saved under `{output_path}/{name}.png`.
|
/// 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`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
|
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
|
||||||
/// snapshot.
|
/// snapshot, if the rendering fails or if no default renderer is available.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn wgpu_snapshot_options(&self, name: &str, options: &SnapshotOptions) {
|
pub fn snapshot_options(&mut self, name: &str, options: &SnapshotOptions) {
|
||||||
match self.try_wgpu_snapshot_options(name, options) {
|
match self.try_snapshot_options(name, options) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
panic!("{}", 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 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`.
|
/// 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`.
|
/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if the image does not match the snapshot or if there was an error reading or writing the
|
/// Panics if the image does not match the snapshot, if there was an error reading or writing the
|
||||||
/// snapshot.
|
/// snapshot, if the rendering fails or if no default renderer is available.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn wgpu_snapshot(&self, name: &str) {
|
pub fn snapshot(&mut self, name: &str) {
|
||||||
match self.try_wgpu_snapshot(name) {
|
match self.try_snapshot(name) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
panic!("{}", 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::texture_to_image::texture_to_image;
|
||||||
use crate::Harness;
|
use eframe::epaint::TextureId;
|
||||||
use egui_wgpu::wgpu::{Backends, InstanceDescriptor, StoreOp, TextureFormat};
|
use egui::TexturesDelta;
|
||||||
use egui_wgpu::{wgpu, ScreenDescriptor};
|
use egui_wgpu::wgpu::{Backends, StoreOp, TextureFormat};
|
||||||
|
use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
|
use std::sync::Arc;
|
||||||
use wgpu::Maintain;
|
use wgpu::Maintain;
|
||||||
|
|
||||||
/// Utility to render snapshots from a [`Harness`] using [`egui_wgpu`].
|
// TODO(#5506): Replace this with the setup from https://github.com/emilk/egui/pull/5506
|
||||||
pub struct TestRenderer {
|
pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup {
|
||||||
device: wgpu::Device,
|
egui_wgpu::WgpuSetup::CreateNew {
|
||||||
queue: wgpu::Queue,
|
supported_backends: Backends::all(),
|
||||||
dithering: bool,
|
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 {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestRenderer {
|
impl WgpuTestRenderer {
|
||||||
/// Create a new [`TestRenderer`] using a default [`wgpu::Instance`].
|
/// Create a new [`WgpuTestRenderer`] with the default setup.
|
||||||
pub fn new() -> Self {
|
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 {
|
Self {
|
||||||
device,
|
render_state: create_render_state(default_wgpu_setup()),
|
||||||
queue,
|
|
||||||
dithering: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
/// # Panics
|
||||||
#[inline]
|
/// Panics if the [`RenderState`] has been used before.
|
||||||
pub fn with_dithering(mut self, dithering: bool) -> Self {
|
pub fn from_render_state(render_state: RenderState) -> Self {
|
||||||
self.dithering = dithering;
|
assert!(
|
||||||
self
|
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.
|
fn handle_delta(&mut self, delta: &TexturesDelta) {
|
||||||
pub fn render<State>(&self, harness: &Harness<'_, State>) -> RgbaImage {
|
let mut renderer = self.render_state.renderer.write();
|
||||||
// We need to create a new renderer each time we render, since the renderer stores
|
for (id, image) in &delta.set {
|
||||||
// textures related to the Harnesses' egui Context.
|
renderer.update_texture(
|
||||||
// Calling the renderer from different Harnesses would cause problems if we store the renderer.
|
&self.render_state.device,
|
||||||
let mut renderer = egui_wgpu::Renderer::new(
|
&self.render_state.queue,
|
||||||
&self.device,
|
*id,
|
||||||
TextureFormat::Rgba8Unorm,
|
image,
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut encoder = self
|
/// Render the [`crate::Harness`] and return the resulting image.
|
||||||
.device
|
fn render(
|
||||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
&mut self,
|
||||||
label: Some("Egui Command Encoder"),
|
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 {
|
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],
|
size_in_pixels: [size.x.round() as u32, size.y.round() as u32],
|
||||||
};
|
};
|
||||||
|
|
||||||
let tessellated = harness.ctx.tessellate(
|
let tessellated = ctx.tessellate(output.shapes.clone(), ctx.pixels_per_point());
|
||||||
harness.output().shapes.clone(),
|
|
||||||
harness.ctx.pixels_per_point(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let user_buffers = renderer.update_buffers(
|
let user_buffers = renderer.update_buffers(
|
||||||
&self.device,
|
&self.render_state.device,
|
||||||
&self.queue,
|
&self.render_state.queue,
|
||||||
&mut encoder,
|
&mut encoder,
|
||||||
&tessellated,
|
&tessellated,
|
||||||
&screen,
|
&screen,
|
||||||
);
|
);
|
||||||
|
|
||||||
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
let texture = self
|
||||||
label: Some("Egui Texture"),
|
.render_state
|
||||||
size: wgpu::Extent3d {
|
.device
|
||||||
width: screen.size_in_pixels[0],
|
.create_texture(&wgpu::TextureDescriptor {
|
||||||
height: screen.size_in_pixels[1],
|
label: Some("Egui Texture"),
|
||||||
depth_or_array_layers: 1,
|
size: wgpu::Extent3d {
|
||||||
},
|
width: screen.size_in_pixels[0],
|
||||||
mip_level_count: 1,
|
height: screen.size_in_pixels[1],
|
||||||
sample_count: 1,
|
depth_or_array_layers: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
},
|
||||||
format: TextureFormat::Rgba8Unorm,
|
mip_level_count: 1,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
sample_count: 1,
|
||||||
view_formats: &[],
|
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());
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
@@ -141,11 +171,16 @@ impl TestRenderer {
|
|||||||
renderer.render(&mut pass, &tessellated, &screen);
|
renderer.render(&mut pass, &tessellated, &screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.queue
|
self.render_state
|
||||||
|
.queue
|
||||||
.submit(user_buffers.into_iter().chain(once(encoder.finish())));
|
.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();
|
harness.fit_contents();
|
||||||
|
|
||||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
#[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();
|
harness.fit_contents();
|
||||||
|
|
||||||
#[cfg(all(feature = "snapshot", feature = "wgpu"))]
|
#[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::float_cmp)]
|
||||||
#![allow(clippy::manual_range_contains)]
|
#![allow(clippy::manual_range_contains)]
|
||||||
|
|
||||||
mod bezier;
|
mod brush;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
mod margin;
|
mod margin;
|
||||||
|
mod marginf;
|
||||||
mod mesh;
|
mod mesh;
|
||||||
pub mod mutex;
|
pub mod mutex;
|
||||||
|
mod rounding;
|
||||||
|
mod roundingf;
|
||||||
mod shadow;
|
mod shadow;
|
||||||
mod shape;
|
|
||||||
pub mod shape_transform;
|
pub mod shape_transform;
|
||||||
|
mod shapes;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
mod stroke;
|
mod stroke;
|
||||||
pub mod tessellator;
|
pub mod tessellator;
|
||||||
@@ -40,17 +43,21 @@ mod texture_atlas;
|
|||||||
mod texture_handle;
|
mod texture_handle;
|
||||||
pub mod textures;
|
pub mod textures;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
mod viewport;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
bezier::{CubicBezierShape, QuadraticBezierShape},
|
brush::Brush,
|
||||||
color::ColorMode,
|
color::ColorMode,
|
||||||
image::{ColorImage, FontImage, ImageData, ImageDelta},
|
image::{ColorImage, FontImage, ImageData, ImageDelta},
|
||||||
margin::Margin,
|
margin::Margin,
|
||||||
|
marginf::Marginf,
|
||||||
mesh::{Mesh, Mesh16, Vertex},
|
mesh::{Mesh, Mesh16, Vertex},
|
||||||
|
rounding::Rounding,
|
||||||
|
roundingf::Roundingf,
|
||||||
shadow::Shadow,
|
shadow::Shadow,
|
||||||
shape::{
|
shapes::{
|
||||||
CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape,
|
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape,
|
||||||
Rounding, Shape, TextShape,
|
QuadraticBezierShape, RectShape, Shape, TextShape,
|
||||||
},
|
},
|
||||||
stats::PaintStats,
|
stats::PaintStats,
|
||||||
stroke::{PathStroke, Stroke, StrokeKind},
|
stroke::{PathStroke, Stroke, StrokeKind},
|
||||||
@@ -59,6 +66,7 @@ pub use self::{
|
|||||||
texture_atlas::TextureAtlas,
|
texture_atlas::TextureAtlas,
|
||||||
texture_handle::TextureHandle,
|
texture_handle::TextureHandle,
|
||||||
textures::TextureManager,
|
textures::TextureManager,
|
||||||
|
viewport::ViewportInPixels,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
|
|||||||
@@ -4,27 +4,33 @@ use emath::{vec2, Rect, Vec2};
|
|||||||
/// often used to express padding or spacing.
|
/// often used to express padding or spacing.
|
||||||
///
|
///
|
||||||
/// Can be added and subtracted to/from [`Rect`]s.
|
/// 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))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct Margin {
|
pub struct Margin {
|
||||||
pub left: f32,
|
pub left: i8,
|
||||||
pub right: f32,
|
pub right: i8,
|
||||||
pub top: f32,
|
pub top: i8,
|
||||||
pub bottom: f32,
|
pub bottom: i8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Margin {
|
impl Margin {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
left: 0.0,
|
left: 0,
|
||||||
right: 0.0,
|
right: 0,
|
||||||
top: 0.0,
|
top: 0,
|
||||||
bottom: 0.0,
|
bottom: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The same margin on every side.
|
/// The same margin on every side.
|
||||||
#[doc(alias = "symmetric")]
|
#[doc(alias = "symmetric")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn same(margin: f32) -> Self {
|
pub const fn same(margin: i8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: margin,
|
left: margin,
|
||||||
right: margin,
|
right: margin,
|
||||||
@@ -35,7 +41,7 @@ impl Margin {
|
|||||||
|
|
||||||
/// Margins with the same size on opposing sides
|
/// Margins with the same size on opposing sides
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn symmetric(x: f32, y: f32) -> Self {
|
pub const fn symmetric(x: i8, y: i8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: x,
|
left: x,
|
||||||
right: 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
|
/// Total margins on both sides
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn sum(&self) -> Vec2 {
|
pub fn sum(self) -> Vec2 {
|
||||||
vec2(self.left + self.right, self.top + self.bottom)
|
vec2(self.leftf() + self.rightf(), self.topf() + self.bottomf())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn left_top(&self) -> Vec2 {
|
pub const fn left_top(self) -> Vec2 {
|
||||||
vec2(self.left, self.top)
|
vec2(self.leftf(), self.topf())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn right_bottom(&self) -> Vec2 {
|
pub const fn right_bottom(self) -> Vec2 {
|
||||||
vec2(self.right, self.bottom)
|
vec2(self.rightf(), self.bottomf())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Are the margin on every side the same?
|
/// Are the margin on every side the same?
|
||||||
#[doc(alias = "symmetric")]
|
#[doc(alias = "symmetric")]
|
||||||
#[inline]
|
#[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
|
self.left == self.right && self.left == self.top && self.left == self.bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deprecated = "Use `rect + margin` instead"]
|
#[deprecated = "Use `rect + margin` instead"]
|
||||||
#[inline]
|
#[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())
|
Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deprecated = "Use `rect - margin` instead"]
|
#[deprecated = "Use `rect - margin` instead"]
|
||||||
#[inline]
|
#[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())
|
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 {
|
impl From<f32> for Margin {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(v: f32) -> Self {
|
fn from(v: f32) -> Self {
|
||||||
Self::same(v)
|
Self::same(v.round() as _)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec2> for Margin {
|
impl From<Vec2> for Margin {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(v: Vec2) -> Self {
|
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]
|
#[inline]
|
||||||
fn add(self, other: Self) -> Self {
|
fn add(self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: self.left + other.left,
|
left: self.left.saturating_add(other.left),
|
||||||
right: self.right + other.right,
|
right: self.right.saturating_add(other.right),
|
||||||
top: self.top + other.top,
|
top: self.top.saturating_add(other.top),
|
||||||
bottom: self.bottom + other.bottom,
|
bottom: self.bottom.saturating_add(other.bottom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Margin + f32`
|
/// `Margin + i8`
|
||||||
impl std::ops::Add<f32> for Margin {
|
impl std::ops::Add<i8> for Margin {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn add(self, v: f32) -> Self {
|
fn add(self, v: i8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: self.left + v,
|
left: self.left.saturating_add(v),
|
||||||
right: self.right + v,
|
right: self.right.saturating_add(v),
|
||||||
top: self.top + v,
|
top: self.top.saturating_add(v),
|
||||||
bottom: self.bottom + v,
|
bottom: self.bottom.saturating_add(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Margind += f32`
|
/// `Margin += i8`
|
||||||
impl std::ops::AddAssign<f32> for Margin {
|
impl std::ops::AddAssign<i8> for Margin {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn add_assign(&mut self, v: f32) {
|
fn add_assign(&mut self, v: i8) {
|
||||||
self.left += v;
|
*self = *self + v;
|
||||||
self.right += v;
|
|
||||||
self.top += v;
|
|
||||||
self.bottom += v;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +176,10 @@ impl std::ops::Mul<f32> for Margin {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn mul(self, v: f32) -> Self {
|
fn mul(self, v: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: self.left * v,
|
left: (self.leftf() * v).round() as _,
|
||||||
right: self.right * v,
|
right: (self.rightf() * v).round() as _,
|
||||||
top: self.top * v,
|
top: (self.topf() * v).round() as _,
|
||||||
bottom: self.bottom * v,
|
bottom: (self.bottomf() * v).round() as _,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,10 +188,7 @@ impl std::ops::Mul<f32> for Margin {
|
|||||||
impl std::ops::MulAssign<f32> for Margin {
|
impl std::ops::MulAssign<f32> for Margin {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn mul_assign(&mut self, v: f32) {
|
fn mul_assign(&mut self, v: f32) {
|
||||||
self.left *= v;
|
*self = *self * v;
|
||||||
self.right *= v;
|
|
||||||
self.top *= v;
|
|
||||||
self.bottom *= v;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,12 +198,8 @@ impl std::ops::Div<f32> for Margin {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn div(self, v: f32) -> Self {
|
fn div(self, v: f32) -> Self {
|
||||||
Self {
|
#![allow(clippy::suspicious_arithmetic_impl)]
|
||||||
left: self.left / v,
|
self * v.recip()
|
||||||
right: self.right / v,
|
|
||||||
top: self.top / v,
|
|
||||||
bottom: self.bottom / v,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,10 +207,7 @@ impl std::ops::Div<f32> for Margin {
|
|||||||
impl std::ops::DivAssign<f32> for Margin {
|
impl std::ops::DivAssign<f32> for Margin {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn div_assign(&mut self, v: f32) {
|
fn div_assign(&mut self, v: f32) {
|
||||||
self.left /= v;
|
*self = *self / v;
|
||||||
self.right /= v;
|
|
||||||
self.top /= v;
|
|
||||||
self.bottom /= v;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,37 +218,34 @@ impl std::ops::Sub for Margin {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn sub(self, other: Self) -> Self {
|
fn sub(self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: self.left - other.left,
|
left: self.left.saturating_sub(other.left),
|
||||||
right: self.right - other.right,
|
right: self.right.saturating_sub(other.right),
|
||||||
top: self.top - other.top,
|
top: self.top.saturating_sub(other.top),
|
||||||
bottom: self.bottom - other.bottom,
|
bottom: self.bottom.saturating_sub(other.bottom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Margin - f32`
|
/// `Margin - i8`
|
||||||
impl std::ops::Sub<f32> for Margin {
|
impl std::ops::Sub<i8> for Margin {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn sub(self, v: f32) -> Self {
|
fn sub(self, v: i8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: self.left - v,
|
left: self.left.saturating_sub(v),
|
||||||
right: self.right - v,
|
right: self.right.saturating_sub(v),
|
||||||
top: self.top - v,
|
top: self.top.saturating_sub(v),
|
||||||
bottom: self.bottom - v,
|
bottom: self.bottom.saturating_sub(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Margin -= f32`
|
/// `Margin -= i8`
|
||||||
impl std::ops::SubAssign<f32> for Margin {
|
impl std::ops::SubAssign<i8> for Margin {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn sub_assign(&mut self, v: f32) {
|
fn sub_assign(&mut self, v: i8) {
|
||||||
self.left -= v;
|
*self = *self - v;
|
||||||
self.right -= v;
|
|
||||||
self.top -= v;
|
|
||||||
self.bottom -= 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.
|
/// The color and fuzziness of a fuzzy shape.
|
||||||
///
|
///
|
||||||
/// Can be used for a rectangular shadow with a soft penumbra.
|
/// Can be used for a rectangular shadow with a soft penumbra.
|
||||||
///
|
///
|
||||||
/// Very similar to a box-shadow in CSS.
|
/// 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))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct Shadow {
|
pub struct Shadow {
|
||||||
/// Move the shadow by this much.
|
/// 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,
|
/// 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.
|
/// 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.
|
/// The width of the blur, i.e. the width of the fuzzy penumbra.
|
||||||
///
|
///
|
||||||
/// A value of 0.0 means a sharp shadow.
|
/// A value of 0 means a sharp shadow.
|
||||||
pub blur: f32,
|
pub blur: u8,
|
||||||
|
|
||||||
/// Expand the shadow in all directions by this much.
|
/// Expand the shadow in all directions by this much.
|
||||||
pub spread: f32,
|
pub spread: u8,
|
||||||
|
|
||||||
/// Color of the opaque center of the shadow.
|
/// Color of the opaque center of the shadow.
|
||||||
pub color: Color32,
|
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 {
|
impl Shadow {
|
||||||
/// No shadow at all.
|
/// No shadow at all.
|
||||||
pub const NONE: Self = Self {
|
pub const NONE: Self = Self {
|
||||||
offset: Vec2::ZERO,
|
offset: [0, 0],
|
||||||
blur: 0.0,
|
blur: 0,
|
||||||
spread: 0.0,
|
spread: 0,
|
||||||
color: Color32::TRANSPARENT,
|
color: Color32::TRANSPARENT,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,26 +53,32 @@ impl Shadow {
|
|||||||
spread,
|
spread,
|
||||||
color,
|
color,
|
||||||
} = *self;
|
} = *self;
|
||||||
|
let [offset_x, offset_y] = offset;
|
||||||
|
|
||||||
let rect = rect.translate(offset).expand(spread);
|
let rect = rect
|
||||||
let rounding = rounding.into() + Rounding::same(spread.abs());
|
.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?
|
/// How much larger than the parent rect are we in each direction?
|
||||||
pub fn margin(&self) -> Margin {
|
pub fn margin(&self) -> Marginf {
|
||||||
let Self {
|
let Self {
|
||||||
offset,
|
offset,
|
||||||
blur,
|
blur,
|
||||||
spread,
|
spread,
|
||||||
color: _,
|
color: _,
|
||||||
} = *self;
|
} = *self;
|
||||||
Margin {
|
let spread = spread as f32;
|
||||||
left: spread + 0.5 * blur - offset.x,
|
let blur = blur as f32;
|
||||||
right: spread + 0.5 * blur + offset.x,
|
let [offset_x, offset_y] = offset;
|
||||||
top: spread + 0.5 * blur - offset.y,
|
Marginf {
|
||||||
bottom: spread + 0.5 * blur + offset.y,
|
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,
|
fill,
|
||||||
stroke,
|
stroke,
|
||||||
blur_width: _,
|
blur_width: _,
|
||||||
fill_texture_id: _,
|
brush: _,
|
||||||
uv: _,
|
|
||||||
}) => {
|
}) => {
|
||||||
adjust_color(fill);
|
adjust_color(fill);
|
||||||
adjust_color(&mut stroke.color);
|
adjust_color(&mut stroke.color);
|
||||||
@@ -87,7 +86,7 @@ pub fn adjust_colors(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !galley.is_empty() {
|
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 {
|
for placed_row in &mut galley.rows {
|
||||||
let row = Arc::make_mut(&mut placed_row.row);
|
let row = Arc::make_mut(&mut placed_row.row);
|
||||||
for vertex in &mut row.visuals.mesh.vertices {
|
for vertex in &mut row.visuals.mesh.vertices {
|
||||||
@@ -97,11 +96,13 @@ pub fn adjust_colors(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Shape::Mesh(Mesh {
|
Shape::Mesh(mesh) => {
|
||||||
indices: _,
|
let Mesh {
|
||||||
vertices,
|
indices: _,
|
||||||
texture_id: _,
|
vertices,
|
||||||
}) => {
|
texture_id: _,
|
||||||
|
} = Arc::make_mut(mesh);
|
||||||
|
|
||||||
for v in vertices {
|
for v in vertices {
|
||||||
adjust_color(&mut v.color);
|
adjust_color(&mut v.color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use crate::{shape::Shape, Color32, PathShape, PathStroke};
|
use crate::{Color32, PathShape, PathStroke, Shape};
|
||||||
use emath::{Pos2, Rect, RectTransform};
|
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 {
|
pub mod path {
|
||||||
//! Helpers for constructing paths
|
//! Helpers for constructing paths
|
||||||
use crate::shape::Rounding;
|
use crate::Rounding;
|
||||||
use emath::{pos2, Pos2, Rect};
|
use emath::{pos2, Pos2, Rect};
|
||||||
|
|
||||||
/// overwrites existing points
|
/// 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.
|
// Duplicated vertices can happen when one side is all rounding, with no straight edge between.
|
||||||
let eps = f32::EPSILON * rect.size().max_elem();
|
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
|
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 {
|
if rect.width() <= r.se + r.sw + eps {
|
||||||
@@ -628,7 +630,7 @@ pub mod path {
|
|||||||
let half_width = rect.width() * 0.5;
|
let half_width = rect.width() * 0.5;
|
||||||
let half_height = rect.height() * 0.5;
|
let half_height = rect.height() * 0.5;
|
||||||
let max_cr = half_width.min(half_height);
|
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;
|
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.
|
/// Tessellate a clipped shape into a list of primitives.
|
||||||
pub fn tessellate_clipped_shape(
|
pub fn tessellate_clipped_shape(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1404,7 +1383,7 @@ impl Tessellator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.append(mesh);
|
out.append_ref(&mesh);
|
||||||
}
|
}
|
||||||
Shape::LineSegment { points, stroke } => {
|
Shape::LineSegment { points, stroke } => {
|
||||||
self.tessellate_line_segment(points, stroke, out);
|
self.tessellate_line_segment(points, stroke, out);
|
||||||
@@ -1691,14 +1670,14 @@ impl Tessellator {
|
|||||||
/// * `rect`: the rectangle to tessellate.
|
/// * `rect`: the rectangle to tessellate.
|
||||||
/// * `out`: triangles are appended to this.
|
/// * `out`: triangles are appended to this.
|
||||||
pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) {
|
pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) {
|
||||||
|
let brush = rect.brush.as_ref();
|
||||||
let RectShape {
|
let RectShape {
|
||||||
mut rect,
|
mut rect,
|
||||||
mut rounding,
|
mut rounding,
|
||||||
fill,
|
fill,
|
||||||
stroke,
|
stroke,
|
||||||
mut blur_width,
|
mut blur_width,
|
||||||
fill_texture_id,
|
..
|
||||||
uv,
|
|
||||||
} = *rect;
|
} = *rect;
|
||||||
|
|
||||||
if self.options.coarse_tessellation_culling
|
if self.options.coarse_tessellation_culling
|
||||||
@@ -1714,8 +1693,16 @@ impl Tessellator {
|
|||||||
// Since the stroke extends outside of the rectangle,
|
// Since the stroke extends outside of the rectangle,
|
||||||
// we can round the rectangle sides to the physical pixel edges,
|
// 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.
|
// 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`
|
let Stroke { width, .. } = stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke`
|
||||||
rect = rect.round_to_pixels(self.pixels_per_point);
|
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.
|
// It is common to (sometimes accidentally) create an infinitely sized rectangle.
|
||||||
@@ -1725,7 +1712,7 @@ impl Tessellator {
|
|||||||
|
|
||||||
let old_feathering = self.feathering;
|
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.
|
// 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.
|
// 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_most(rect.size().min_elem() - eps)
|
||||||
.at_least(0.0);
|
.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);
|
self.feathering = self.feathering.max(blur_width);
|
||||||
}
|
}
|
||||||
@@ -1773,7 +1760,11 @@ impl Tessellator {
|
|||||||
path.add_line_loop(&self.scratchpad_points);
|
path.add_line_loop(&self.scratchpad_points);
|
||||||
let path_stroke = PathStroke::from(stroke).outside();
|
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
|
// Textured
|
||||||
let uv_from_pos = |p: Pos2| {
|
let uv_from_pos = |p: Pos2| {
|
||||||
pos2(
|
pos2(
|
||||||
@@ -1830,10 +1821,7 @@ impl Tessellator {
|
|||||||
// The contents of the galley are already snapped to pixel coordinates,
|
// 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:
|
// 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 {
|
let galley_pos = if self.options.round_text_to_pixels {
|
||||||
pos2(
|
galley_pos.round_to_pixels(self.pixels_per_point)
|
||||||
self.round_to_pixel(galley_pos.x),
|
|
||||||
self.round_to_pixel(galley_pos.y),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
*galley_pos
|
*galley_pos
|
||||||
};
|
};
|
||||||
@@ -1913,13 +1901,11 @@ impl Tessellator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if *underline != Stroke::NONE {
|
if *underline != Stroke::NONE {
|
||||||
self.scratchpad_path.clear();
|
self.tessellate_line_segment(
|
||||||
self.scratchpad_path.add_line_segment([
|
[row_rect.left_bottom(), row_rect.right_bottom()],
|
||||||
self.round_pos_to_pixel_center(row_rect.left_bottom()),
|
*underline,
|
||||||
self.round_pos_to_pixel_center(row_rect.right_bottom()),
|
out,
|
||||||
]);
|
);
|
||||||
self.scratchpad_path
|
|
||||||
.stroke_open(0.0, &PathStroke::from(*underline), out);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2173,7 +2159,7 @@ impl Tessellator {
|
|||||||
|
|
||||||
profiling::scope!("distribute results", tessellated.len().to_string());
|
profiling::scope!("distribute results", tessellated.len().to_string());
|
||||||
for (index, mesh) in tessellated {
|
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| {
|
ui.vertical(|ui| {
|
||||||
let window_margin = ui.spacing().window_margin;
|
let window_margin = ui.spacing().window_margin;
|
||||||
let size_1x1 = vec2(32.0, 26.0);
|
let size_1x1 = vec2(32.0, 26.0);
|
||||||
let _size_1x2 = vec2(32.0, 52.0 + window_margin.top);
|
let _size_1x2 = vec2(32.0, 52.0 + window_margin.topf());
|
||||||
let _size_2x1 = vec2(64.0 + window_margin.left, 26.0);
|
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| {
|
ui.horizontal(|ui| {
|
||||||
if ui.add_sized(size_1x1, Button::new("1")).clicked() {
|
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(),
|
fill: ctx.style().visuals.window_fill(),
|
||||||
rounding: 10.0.into(),
|
rounding: 10.0.into(),
|
||||||
stroke: ctx.style().visuals.widgets.noninteractive.fg_stroke,
|
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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ impl eframe::App for MyApp {
|
|||||||
ui.add_space(20.0);
|
ui.add_space(20.0);
|
||||||
egui::Frame {
|
egui::Frame {
|
||||||
stroke: ui.visuals().noninteractive().bg_stroke,
|
stroke: ui.visuals().noninteractive().bg_stroke,
|
||||||
inner_margin: egui::Margin::same(4.0),
|
inner_margin: egui::Margin::same(4),
|
||||||
outer_margin: egui::Margin::same(4.0),
|
outer_margin: egui::Margin::same(4),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
@@ -74,8 +74,8 @@ impl eframe::App for MyApp {
|
|||||||
|
|
||||||
egui::Frame {
|
egui::Frame {
|
||||||
stroke: ui.visuals().noninteractive().bg_stroke,
|
stroke: ui.visuals().noninteractive().bg_stroke,
|
||||||
inner_margin: egui::Margin::same(8.0),
|
inner_margin: egui::Margin::same(8),
|
||||||
outer_margin: egui::Margin::same(6.0),
|
outer_margin: egui::Margin::same(6),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
@@ -128,7 +128,7 @@ impl eframe::App for MyApp {
|
|||||||
ui.label("UI nesting test:");
|
ui.label("UI nesting test:");
|
||||||
egui::Frame {
|
egui::Frame {
|
||||||
stroke: ui.visuals().noninteractive().bg_stroke,
|
stroke: ui.visuals().noninteractive().bg_stroke,
|
||||||
inner_margin: egui::Margin::same(4.0),
|
inner_margin: egui::Margin::same(4),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.show(ui, |ui| {
|
.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) {
|
fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) {
|
||||||
egui::Frame {
|
egui::Frame {
|
||||||
stroke: ui.style().noninteractive().fg_stroke,
|
stroke: ui.style().noninteractive().fg_stroke,
|
||||||
inner_margin: egui::Margin::same(4.0),
|
inner_margin: egui::Margin::same(4),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
|||||||
Reference in New Issue
Block a user