1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 05:40:03 -04:00

Merge branch 'emilk:master' into cache_galley_lines

This commit is contained in:
Hubert Głuchowski
2024-12-07 18:44:48 +01:00
committed by GitHub
97 changed files with 1368 additions and 328 deletions

View File

@@ -9,4 +9,4 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Machete - name: Machete
uses: bnjbvr/cargo-machete@main run: cargo install cargo-machete --locked && cargo machete

View File

@@ -39,7 +39,7 @@ jobs:
with: with:
profile: minimal profile: minimal
target: wasm32-unknown-unknown target: wasm32-unknown-unknown
toolchain: 1.77.0 toolchain: 1.79.0
override: true override: true
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2

View File

@@ -18,7 +18,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.77.0 toolchain: 1.79.0
- name: Install packages (Linux) - name: Install packages (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
@@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.77.0 toolchain: 1.79.0
targets: wasm32-unknown-unknown targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
with: with:
rust-version: "1.77.0" rust-version: "1.79.0"
log-level: error log-level: error
command: check command: check
arguments: --target ${{ matrix.target }} arguments: --target ${{ matrix.target }}
@@ -170,7 +170,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.77.0 toolchain: 1.79.0
targets: aarch64-linux-android targets: aarch64-linux-android
- name: Set up cargo cache - name: Set up cargo cache
@@ -189,7 +189,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.77.0 toolchain: 1.79.0
targets: aarch64-apple-ios targets: aarch64-apple-ios
- name: Set up cargo cache - name: Set up cargo cache
@@ -208,7 +208,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.77.0 toolchain: 1.79.0
- name: Set up cargo cache - name: Set up cargo cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@@ -232,7 +232,7 @@ jobs:
lfs: true lfs: true
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.77.0 toolchain: 1.79.0
- name: Set up cargo cache - name: Set up cargo cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2

View File

@@ -1 +1,2 @@
/crates/egui_kittest @lucasmerlin
/crates/egui-wgpu @Wumpf /crates/egui-wgpu @Wumpf

View File

@@ -2239,7 +2239,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]] [[package]]
name = "kittest" name = "kittest"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/rerun-io/kittest?branch=main#63c5b7d58178900e523428ca5edecbba007a2702" source = "git+https://github.com/rerun-io/kittest?branch=main#06e01f17fed36a997e1541f37b2d47e3771d7533"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_consumer", "accesskit_consumer",
@@ -4356,9 +4356,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]] [[package]]
name = "wgpu" name = "wgpu"
version = "23.0.0" version = "23.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ab52f2d3d18b70d5ab8dd270a1cff3ebe6dbe4a7d13c1cc2557138a9777fdc" checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"cfg_aliases 0.1.1", "cfg_aliases 0.1.1",
@@ -4381,9 +4381,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-core" name = "wgpu-core"
version = "23.0.0" version = "23.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c68e7b6322a03ee5b83fcd92caeac5c2a932f6457818179f4652ad2a9c065" checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-vec 0.8.0", "bit-vec 0.8.0",
@@ -4406,9 +4406,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-hal" name = "wgpu-hal"
version = "23.0.0" version = "23.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de6e7266b869de56c7e3ed72a954899f71d14fec6cc81c102b7530b92947601b" checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"arrayvec", "arrayvec",
@@ -4478,7 +4478,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View File

@@ -23,7 +23,7 @@ members = [
[workspace.package] [workspace.package]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
rust-version = "1.77" rust-version = "1.79"
version = "0.29.1" version = "0.29.1"
@@ -145,6 +145,7 @@ disallowed_types = "warn" # See clippy.toml
doc_link_with_quotes = "warn" doc_link_with_quotes = "warn"
doc_markdown = "warn" doc_markdown = "warn"
empty_enum = "warn" empty_enum = "warn"
empty_enum_variants_with_brackets = "warn"
enum_glob_use = "warn" enum_glob_use = "warn"
equatable_if_let = "warn" equatable_if_let = "warn"
exit = "warn" exit = "warn"
@@ -169,6 +170,8 @@ inefficient_to_string = "warn"
infinite_loop = "warn" infinite_loop = "warn"
into_iter_without_iter = "warn" into_iter_without_iter = "warn"
invalid_upcast_comparisons = "warn" invalid_upcast_comparisons = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn" iter_not_returning_iterator = "warn"
iter_on_empty_collections = "warn" iter_on_empty_collections = "warn"
iter_on_single_items = "warn" iter_on_single_items = "warn"
@@ -185,6 +188,7 @@ macro_use_imports = "warn"
manual_assert = "warn" manual_assert = "warn"
manual_clamp = "warn" manual_clamp = "warn"
manual_instant_elapsed = "warn" manual_instant_elapsed = "warn"
manual_is_variant_and = "warn"
manual_let_else = "warn" manual_let_else = "warn"
manual_ok_or = "warn" manual_ok_or = "warn"
manual_string_new = "warn" manual_string_new = "warn"
@@ -202,6 +206,7 @@ mismatching_type_param_order = "warn"
missing_enforced_import_renames = "warn" missing_enforced_import_renames = "warn"
missing_errors_doc = "warn" missing_errors_doc = "warn"
missing_safety_doc = "warn" missing_safety_doc = "warn"
mixed_attributes_style = "warn"
mut_mut = "warn" mut_mut = "warn"
mutex_integer = "warn" mutex_integer = "warn"
needless_borrow = "warn" needless_borrow = "warn"
@@ -211,21 +216,25 @@ needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn" needless_pass_by_value = "warn"
negative_feature_names = "warn" negative_feature_names = "warn"
nonstandard_macro_braces = "warn" nonstandard_macro_braces = "warn"
option_as_ref_cloned = "warn"
option_option = "warn" option_option = "warn"
path_buf_push_overwrite = "warn" path_buf_push_overwrite = "warn"
print_stderr = "warn" print_stderr = "warn"
ptr_as_ptr = "warn" ptr_as_ptr = "warn"
ptr_cast_constness = "warn" ptr_cast_constness = "warn"
pub_underscore_fields = "warn"
pub_without_shorthand = "warn" pub_without_shorthand = "warn"
rc_mutex = "warn" rc_mutex = "warn"
readonly_write_lock = "warn" readonly_write_lock = "warn"
redundant_type_annotations = "warn" redundant_type_annotations = "warn"
ref_as_ptr = "warn"
ref_option_ref = "warn" ref_option_ref = "warn"
ref_patterns = "warn" ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn" rest_pat_in_fully_bound_structs = "warn"
same_functions_in_if_condition = "warn" same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn" semicolon_if_nothing_returned = "warn"
single_match_else = "warn" single_match_else = "warn"
str_split_at_newline = "warn"
str_to_string = "warn" str_to_string = "warn"
string_add = "warn" string_add = "warn"
string_add_assign = "warn" string_add_assign = "warn"
@@ -261,12 +270,15 @@ zero_sized_map_values = "warn"
# TODO(emilk): enable more of these lints: # TODO(emilk): enable more of these lints:
iter_over_hash_type = "allow" iter_over_hash_type = "allow"
let_underscore_untyped = "allow"
missing_assert_message = "allow" missing_assert_message = "allow"
should_panic_without_expect = "allow" should_panic_without_expect = "allow"
too_many_lines = "allow" too_many_lines = "allow"
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
# These are meh:
assigning_clones = "allow" # No please
let_underscore_must_use = "allow"
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

View File

@@ -3,7 +3,7 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml: # Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.77" msrv = "1.79"
allow-unwrap-in-tests = true allow-unwrap-in-tests = true
@@ -69,9 +69,12 @@ disallowed-types = [
# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown # Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
doc-valid-idents = [ doc-valid-idents = [
# You must also update the same list in the root `clippy.toml`! # You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
"AccessKit", "AccessKit",
"WebGL", "WebGL",
"WebGL1",
"WebGL2",
"WebGPU", "WebGPU",
"VirtualBox",
"..", "..",
] ]

View File

@@ -269,3 +269,18 @@ impl Color32 {
) )
} }
} }
impl std::ops::Mul for Color32 {
type Output = Self;
/// Fast gamma-space multiplication.
#[inline]
fn mul(self, other: Self) -> Self {
Self([
fast_round(self[0] as f32 * other[0] as f32 / 255.0),
fast_round(self[1] as f32 * other[1] as f32 / 255.0),
fast_round(self[2] as f32 * other[2] as f32 / 255.0),
fast_round(self[3] as f32 * other[3] as f32 / 255.0),
])
}
}

View File

@@ -439,7 +439,7 @@ pub struct WebOptions {
/// Unused by webgl context as of writing. /// Unused by webgl context as of writing.
pub depth_buffer: u8, pub depth_buffer: u8,
/// Which version of WebGl context to select /// Which version of WebGL context to select
/// ///
/// Default: [`WebGlContextOption::BestFirst`]. /// Default: [`WebGlContextOption::BestFirst`].
#[cfg(feature = "glow")] #[cfg(feature = "glow")]

View File

@@ -14,7 +14,7 @@ impl EventLoopGuard {
cell.get().is_none(), cell.get().is_none(),
"Attempted to set a new event loop while one is already set" "Attempted to set a new event loop while one is already set"
); );
cell.set(Some(event_loop as *const ActiveEventLoop)); cell.set(Some(std::ptr::from_ref::<ActiveEventLoop>(event_loop)));
}); });
Self Self
} }

View File

@@ -661,13 +661,14 @@ impl<'app> GlowWinitRunning<'app> {
{ {
for action in viewport.actions_requested.drain() { for action in viewport.actions_requested.drain() {
match action { match action {
ActionRequested::Screenshot => { ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels); let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit egui_winit
.egui_input_mut() .egui_input_mut()
.events .events
.push(egui::Event::Screenshot { .push(egui::Event::Screenshot {
viewport_id, viewport_id,
user_data,
image: screenshot.into(), image: screenshot.into(),
}); });
} }
@@ -943,7 +944,7 @@ impl GlutinWindowContext {
// we might want to expose this option to users in the future. maybe using an env var or using native_options. // we might want to expose this option to users in the future. maybe using an env var or using native_options.
// //
// The justification for FallbackEgl over PreferEgl is at https://github.com/emilk/egui/pull/2526#issuecomment-1400229576 . // The justification for FallbackEgl over PreferEgl is at https://github.com/emilk/egui/pull/2526#issuecomment-1400229576 .
.with_preference(glutin_winit::ApiPreference::PreferEgl) .with_preference(glutin_winit::ApiPreference::FallbackEgl)
.with_window_attributes(Some(egui_winit::create_winit_window_attributes( .with_window_attributes(Some(egui_winit::create_winit_window_attributes(
egui_ctx, egui_ctx,
event_loop, event_loop,

View File

@@ -643,10 +643,16 @@ impl<'app> WgpuWinitRunning<'app> {
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
let screenshot_requested = viewport let mut screenshot_commands = vec![];
.actions_requested viewport.actions_requested.retain(|cmd| {
.take(&ActionRequested::Screenshot) if let ActionRequested::Screenshot(info) = cmd {
.is_some(); screenshot_commands.push(info.clone());
false
} else {
true
}
});
let screenshot_requested = !screenshot_commands.is_empty();
let (vsync_secs, screenshot) = painter.paint_and_update_textures( let (vsync_secs, screenshot) = painter.paint_and_update_textures(
viewport_id, viewport_id,
pixels_per_point, pixels_per_point,
@@ -655,19 +661,32 @@ impl<'app> WgpuWinitRunning<'app> {
&textures_delta, &textures_delta,
screenshot_requested, screenshot_requested,
); );
if let Some(screenshot) = screenshot { match (screenshot_requested, screenshot) {
egui_winit (false, None) => {}
.egui_input_mut() (true, Some(screenshot)) => {
.events let screenshot = Arc::new(screenshot);
.push(egui::Event::Screenshot { for user_data in screenshot_commands {
viewport_id, egui_winit
image: screenshot.into(), .egui_input_mut()
}); .events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.clone(),
});
}
}
(true, None) => {
log::error!("Bug in egui_wgpu: screenshot requested, but no screenshot was taken");
}
(false, Some(_)) => {
log::warn!("Bug in egui_wgpu: Got screenshot without requesting it");
}
} }
for action in viewport.actions_requested.drain() { for action in viewport.actions_requested.drain() {
match action { match action {
ActionRequested::Screenshot => { ActionRequested::Screenshot { .. } => {
// already handled above // already handled above
} }
ActionRequested::Cut => { ActionRequested::Cut => {

View File

@@ -1,42 +1,14 @@
use raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle,
};
use std::sync::Arc; use std::sync::Arc;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement; use web_sys::HtmlCanvasElement;
use crate::WebOptions;
use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup};
use crate::WebOptions;
use super::web_painter::WebPainter; use super::web_painter::WebPainter;
struct EguiWebWindow(u32);
#[allow(unsafe_code)]
impl HasWindowHandle for EguiWebWindow {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
// SAFETY: there is no lifetime here.
unsafe {
Ok(WindowHandle::borrow_raw(RawWindowHandle::Web(
WebWindowHandle::new(self.0),
)))
}
}
}
#[allow(unsafe_code)]
impl HasDisplayHandle for EguiWebWindow {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
// SAFETY: there is no lifetime here.
unsafe {
Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Web(
WebDisplayHandle::new(),
)))
}
}
}
pub(crate) struct WebPainterWgpu { pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement, canvas: HtmlCanvasElement,
surface: wgpu::Surface<'static>, surface: wgpu::Surface<'static>,

View File

@@ -16,7 +16,7 @@ pub struct WebRunner {
/// Have we ever panicked? /// Have we ever panicked?
panic_handler: PanicHandler, panic_handler: PanicHandler,
/// If we ever panic during running, this RefCell is poisoned. /// If we ever panic during running, this `RefCell` is poisoned.
/// So before we use it, we need to check [`Self::panic_handler`]. /// So before we use it, we need to check [`Self::panic_handler`].
runner: Rc<RefCell<Option<AppRunner>>>, runner: Rc<RefCell<Option<AppRunner>>>,

View File

@@ -125,7 +125,7 @@ pub struct ScreenDescriptor {
/// Size of the window in physical pixels. /// Size of the window in physical pixels.
pub size_in_pixels: [u32; 2], pub size_in_pixels: [u32; 2],
/// HiDPI scale factor (pixels per point). /// High-DPI scale factor (pixels per point).
pub pixels_per_point: f32, pub pixels_per_point: f32,
} }

View File

@@ -1301,7 +1301,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[derive(PartialEq, Eq, Hash, Debug)] #[derive(PartialEq, Eq, Hash, Debug)]
pub enum ActionRequested { pub enum ActionRequested {
Screenshot, Screenshot(egui::UserData),
Cut, Cut,
Copy, Copy,
Paste, Paste,
@@ -1516,8 +1516,8 @@ fn process_viewport_command(
log::warn!("{command:?}: {err}"); log::warn!("{command:?}: {err}");
} }
} }
ViewportCommand::Screenshot => { ViewportCommand::Screenshot(user_data) => {
actions_requested.insert(ActionRequested::Screenshot); actions_requested.insert(ActionRequested::Screenshot(user_data));
} }
ViewportCommand::RequestCut => { ViewportCommand::RequestCut => {
actions_requested.insert(ActionRequested::Cut); actions_requested.insert(ActionRequested::Cut);

69
crates/egui/src/cache/cache_storage.rs vendored Normal file
View File

@@ -0,0 +1,69 @@
use super::CacheTrait;
/// A typemap of many caches, all implemented with [`CacheTrait`].
///
/// You can access egui's caches via [`crate::Memory::caches`],
/// found with [`crate::Context::memory_mut`].
///
/// ```
/// use egui::cache::{CacheStorage, ComputerMut, FrameCache};
///
/// #[derive(Default)]
/// struct CharCounter {}
/// impl ComputerMut<&str, usize> for CharCounter {
/// fn compute(&mut self, s: &str) -> usize {
/// s.chars().count()
/// }
/// }
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut cache_storage = CacheStorage::default();
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// ```
#[derive(Default)]
pub struct CacheStorage {
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
}
impl CacheStorage {
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
self.caches
.entry(std::any::TypeId::of::<Cache>())
.or_insert_with(|| Box::<Cache>::default())
.as_any_mut()
.downcast_mut::<Cache>()
.unwrap()
}
/// Total number of cached values
fn num_values(&self) -> usize {
self.caches.values().map(|cache| cache.len()).sum()
}
/// Call once per frame to evict cache.
pub fn update(&mut self) {
self.caches.retain(|_, cache| {
cache.update();
cache.len() > 0
});
}
}
impl Clone for CacheStorage {
fn clone(&self) -> Self {
// We return an empty cache that can be filled in again.
Self::default()
}
}
impl std::fmt::Debug for CacheStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"FrameCacheStorage[{} caches with {} elements]",
self.caches.len(),
self.num_values()
)
}
}

11
crates/egui/src/cache/cache_trait.rs vendored Normal file
View File

@@ -0,0 +1,11 @@
/// A cache, storing some value for some length of time.
#[allow(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync {
/// Call once per frame to evict cache.
fn update(&mut self);
/// Number of values currently in the cache.
fn len(&self) -> usize;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}

View File

@@ -1,9 +1,4 @@
//! Computing the same thing each frame can be expensive, use super::CacheTrait;
//! so often you want to save the result from the previous frame and reuse it.
//!
//! Enter [`FrameCache`]: it caches the results of a computation for one frame.
//! If it is still used next frame, it is not recomputed.
//! If it is not used next frame, it is evicted from the cache to save memory.
/// Something that does an expensive computation that we want to cache /// Something that does an expensive computation that we want to cache
/// to save us from recomputing it each frame. /// to save us from recomputing it each frame.
@@ -74,17 +69,6 @@ impl<Value, Computer> FrameCache<Value, Computer> {
} }
} }
#[allow(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync {
/// Call once per frame to evict cache.
fn update(&mut self);
/// Number of values currently in the cache.
fn len(&self) -> usize;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}
impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
for FrameCache<Value, Computer> for FrameCache<Value, Computer>
{ {
@@ -100,65 +84,3 @@ impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
self self
} }
} }
/// ```
/// use egui::util::cache::{CacheStorage, ComputerMut, FrameCache};
///
/// #[derive(Default)]
/// struct CharCounter {}
/// impl ComputerMut<&str, usize> for CharCounter {
/// fn compute(&mut self, s: &str) -> usize {
/// s.chars().count()
/// }
/// }
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut cache_storage = CacheStorage::default();
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// ```
#[derive(Default)]
pub struct CacheStorage {
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
}
impl CacheStorage {
pub fn cache<FrameCache: CacheTrait + Default>(&mut self) -> &mut FrameCache {
self.caches
.entry(std::any::TypeId::of::<FrameCache>())
.or_insert_with(|| Box::<FrameCache>::default())
.as_any_mut()
.downcast_mut::<FrameCache>()
.unwrap()
}
/// Total number of cached values
fn num_values(&self) -> usize {
self.caches.values().map(|cache| cache.len()).sum()
}
/// Call once per frame to evict cache.
pub fn update(&mut self) {
for cache in self.caches.values_mut() {
cache.update();
}
}
}
impl Clone for CacheStorage {
fn clone(&self) -> Self {
// We return an empty cache that can be filled in again.
Self::default()
}
}
impl std::fmt::Debug for CacheStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"FrameCacheStorage[{} caches with {} elements]",
self.caches.len(),
self.num_values()
)
}
}

View File

@@ -0,0 +1,61 @@
use std::hash::Hash;
use super::CacheTrait;
/// Stores a key:value pair for the duration of this frame and the next.
pub struct FramePublisher<Key: Eq + Hash, Value> {
generation: u32,
cache: ahash::HashMap<Key, (u32, Value)>,
}
impl<Key: Eq + Hash, Value> Default for FramePublisher<Key, Value> {
fn default() -> Self {
Self::new()
}
}
impl<Key: Eq + Hash, Value> FramePublisher<Key, Value> {
pub fn new() -> Self {
Self {
generation: 0,
cache: Default::default(),
}
}
/// Publish the value. It will be available for the duration of this and the next frame.
pub fn set(&mut self, key: Key, value: Value) {
self.cache.insert(key, (self.generation, value));
}
/// Retrieve a value if it was published this or the previous frame.
pub fn get(&self, key: &Key) -> Option<&Value> {
self.cache.get(key).map(|(_, value)| value)
}
/// Must be called once per frame to clear the cache.
pub fn evict_cache(&mut self) {
let current_generation = self.generation;
self.cache.retain(|_key, cached| {
cached.0 == current_generation // only keep those that were published this frame
});
self.generation = self.generation.wrapping_add(1);
}
}
impl<Key, Value> CacheTrait for FramePublisher<Key, Value>
where
Key: 'static + Eq + Hash + Send + Sync,
Value: 'static + Send + Sync,
{
fn update(&mut self) {
self.evict_cache();
}
fn len(&self) -> usize {
self.cache.len()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}

21
crates/egui/src/cache/mod.rs vendored Normal file
View File

@@ -0,0 +1,21 @@
//! Caches for preventing the same value from being recomputed every frame.
//!
//! Computing the same thing each frame can be expensive,
//! so often you want to save the result from the previous frame and reuse it.
//!
//! Enter [`FrameCache`]: it caches the results of a computation for one frame.
//! If it is still used next frame, it is not recomputed.
//! If it is not used next frame, it is evicted from the cache to save memory.
//!
//! You can access egui's caches via [`crate::Memory::caches`],
//! found with [`crate::Context::memory_mut`].
mod cache_storage;
mod cache_trait;
mod frame_cache;
mod frame_publisher;
pub use cache_storage::CacheStorage;
pub use cache_trait::CacheTrait;
pub use frame_cache::{ComputerMut, FrameCache};
pub use frame_publisher::FramePublisher;

View File

@@ -6,6 +6,7 @@ pub(crate) mod area;
pub mod collapsing_header; pub mod collapsing_header;
mod combo_box; mod combo_box;
pub mod frame; pub mod frame;
pub mod modal;
pub mod panel; pub mod panel;
pub mod popup; pub mod popup;
pub(crate) mod resize; pub(crate) mod resize;
@@ -18,6 +19,7 @@ pub use {
collapsing_header::{CollapsingHeader, CollapsingResponse}, collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*, combo_box::*,
frame::Frame, frame::Frame,
modal::{Modal, ModalResponse},
panel::{CentralPanel, SidePanel, TopBottomPanel}, panel::{CentralPanel, SidePanel, TopBottomPanel},
popup::*, popup::*,
resize::Resize, resize::Resize,

View File

@@ -0,0 +1,165 @@
use crate::{
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
};
use emath::{Align2, Vec2};
/// A modal dialog.
/// Similar to a [`crate::Window`] but centered and with a backdrop that
/// blocks input to the rest of the UI.
///
/// You can show multiple modals on top of each other. The topmost modal will always be
/// the most recently shown one.
pub struct Modal {
pub area: Area,
pub backdrop_color: Color32,
pub frame: Option<Frame>,
}
impl Modal {
/// Create a new Modal. The id is passed to the area.
pub fn new(id: Id) -> Self {
Self {
area: Self::default_area(id),
backdrop_color: Color32::from_black_alpha(100),
frame: None,
}
}
/// Returns an area customized for a modal.
/// Makes these changes to the default area:
/// - sense: hover
/// - anchor: center
/// - order: foreground
pub fn default_area(id: Id) -> Area {
Area::new(id)
.kind(UiKind::Modal)
.sense(Sense::hover())
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.order(Order::Foreground)
.interactable(true)
}
/// Set the frame of the modal.
///
/// Default is [`Frame::popup`].
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
/// Set the backdrop color of the modal.
///
/// Default is `Color32::from_black_alpha(100)`.
#[inline]
pub fn backdrop_color(mut self, color: Color32) -> Self {
self.backdrop_color = color;
self
}
/// Set the area of the modal.
///
/// Default is [`Modal::default_area`].
#[inline]
pub fn area(mut self, area: Area) -> Self {
self.area = area;
self
}
/// Show the modal.
pub fn show<T>(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse<T> {
let Self {
area,
backdrop_color,
frame,
} = self;
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
mem.set_modal_layer(area.layer());
(
mem.top_modal_layer() == Some(area.layer()),
mem.any_popup_open(),
)
});
let InnerResponse {
inner: (inner, backdrop_response),
response,
} = area.show(ctx, |ui| {
let bg_rect = ui.ctx().screen_rect();
let bg_sense = Sense {
click: true,
drag: true,
focusable: false,
};
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
backdrop.set_min_size(bg_rect.size());
ui.painter().rect_filled(bg_rect, 0.0, backdrop_color);
let backdrop_response = backdrop.response();
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
// We need the extra scope with the sense since frame can't have a sense and since we
// need to prevent the clicks from passing through to the backdrop.
let inner = ui
.scope_builder(
UiBuilder::new().sense(Sense {
click: true,
drag: true,
focusable: false,
}),
|ui| frame.show(ui, content).inner,
)
.inner;
(inner, backdrop_response)
});
ModalResponse {
response,
backdrop_response,
inner,
is_top_modal,
any_popup_open,
}
}
}
/// The response of a modal dialog.
pub struct ModalResponse<T> {
/// The response of the modal contents
pub response: Response,
/// The response of the modal backdrop.
///
/// A click on this means the user clicked outside the modal,
/// in which case you might want to close the modal.
pub backdrop_response: Response,
/// The inner response from the content closure
pub inner: T,
/// Is this the topmost modal?
pub is_top_modal: bool,
/// Is there any popup open?
/// We need to check this before the modal contents are shown, so we can know if any popup
/// was open when checking if the escape key was clicked.
pub any_popup_open: bool,
}
impl<T> ModalResponse<T> {
/// Should the modal be closed?
/// Returns true if:
/// - the backdrop was clicked
/// - this is the topmost modal, no popup is open and the escape key was pressed
pub fn should_close(&self) -> bool {
let ctx = &self.response.ctx;
// this is a closure so that `Esc` is consumed only if the modal is topmost
let escape_clicked =
|| ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
self.backdrop_response.clicked()
|| (self.is_top_modal && !self.any_popup_open && escape_clicked())
}
}

View File

@@ -87,17 +87,22 @@ pub fn show_tooltip_at_pointer<R>(
// Add a small exclusion zone around the pointer to avoid tooltips // Add a small exclusion zone around the pointer to avoid tooltips
// covering what we're hovering over. // covering what we're hovering over.
let mut exclusion_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0)); let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
// Keep the left edge of the tooltip in line with the cursor: // Keep the left edge of the tooltip in line with the cursor:
exclusion_rect.min.x = pointer_pos.x; pointer_rect.min.x = pointer_pos.x;
// Transform global coords to layer coords:
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
pointer_rect = transform.inverse() * pointer_rect;
}
show_tooltip_at_dyn( show_tooltip_at_dyn(
ctx, ctx,
parent_layer, parent_layer,
widget_id, widget_id,
allow_placing_below, allow_placing_below,
&exclusion_rect, &pointer_rect,
Box::new(add_contents), Box::new(add_contents),
) )
}) })
@@ -155,6 +160,7 @@ fn show_tooltip_at_dyn<'c, R>(
widget_rect: &Rect, widget_rect: &Rect,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> R { ) -> R {
// Transform layer coords to global coords:
let mut widget_rect = *widget_rect; let mut widget_rect = *widget_rect;
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
widget_rect = transform * widget_rect; widget_rect = transform * widget_rect;

View File

@@ -39,7 +39,7 @@ pub struct State {
scroll_start_offset_from_top_left: [Option<f32>; 2], scroll_start_offset_from_top_left: [Option<f32>; 2],
/// Is the scroll sticky. This is true while scroll handle is in the end position /// Is the scroll sticky. This is true while scroll handle is in the end position
/// and remains that way until the user moves the scroll_handle. Once unstuck (false) /// and remains that way until the user moves the `scroll_handle`. Once unstuck (false)
/// it remains false until the scroll touches the end position, which reenables stickiness. /// it remains false until the scroll touches the end position, which reenables stickiness.
scroll_stuck_to_end: Vec2b, scroll_stuck_to_end: Vec2b,
@@ -499,6 +499,11 @@ struct Prepared {
scrolling_enabled: bool, scrolling_enabled: bool,
stick_to_end: Vec2b, stick_to_end: Vec2b,
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
animated: bool, animated: bool,
} }
@@ -693,6 +698,10 @@ impl ScrollArea {
} }
} }
let saved_scroll_target = content_ui
.ctx()
.pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
Prepared { Prepared {
id, id,
state, state,
@@ -707,6 +716,7 @@ impl ScrollArea {
viewport, viewport,
scrolling_enabled, scrolling_enabled,
stick_to_end, stick_to_end,
saved_scroll_target,
animated, animated,
} }
} }
@@ -820,6 +830,7 @@ impl Prepared {
viewport: _, viewport: _,
scrolling_enabled, scrolling_enabled,
stick_to_end, stick_to_end,
saved_scroll_target,
animated, animated,
} = self; } = self;
@@ -853,7 +864,7 @@ impl Prepared {
let (start, end) = (range.min, range.max); let (start, end) = (range.min, range.max);
let clip_start = clip_rect.min[d]; let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d]; let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d]; let mut spacing = content_ui.spacing().item_spacing[d];
let delta_update = if let Some(align) = align { let delta_update = if let Some(align) = align {
let center_factor = align.to_factor(); let center_factor = align.to_factor();
@@ -902,6 +913,15 @@ impl Prepared {
} }
} }
// Restore scroll target meant for ScrollAreas up the stack (if any)
ui.ctx().pass_state_mut(|state| {
for d in 0..2 {
if saved_scroll_target[d].is_some() {
state.scroll_target[d] = saved_scroll_target[d].clone();
};
}
});
let inner_rect = { let inner_rect = {
// At this point this is the available size for the inner rect. // At this point this is the available size for the inner rect.
let mut inner_size = inner_rect.size(); let mut inner_size = inner_rect.size();

View File

@@ -1160,7 +1160,8 @@ impl Context {
/// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)).
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response {
let interested_in_focus = w.enabled && w.sense.focusable && w.layer_id.allow_interaction(); let interested_in_focus =
w.enabled && w.sense.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id));
// Remember this widget // Remember this widget
self.write(|ctx| { self.write(|ctx| {
@@ -1172,7 +1173,7 @@ impl Context {
viewport.this_pass.widgets.insert(w.layer_id, w); viewport.this_pass.widgets.insert(w.layer_id, w);
if allow_focus && interested_in_focus { if allow_focus && interested_in_focus {
ctx.memory.interested_in_focus(w.id); ctx.memory.interested_in_focus(w.id, w.layer_id);
} }
}); });
@@ -3454,15 +3455,23 @@ impl Context {
return Err(load::LoadError::NoImageLoaders); return Err(load::LoadError::NoImageLoaders);
} }
let mut format = None;
// Try most recently added loaders first (hence `.rev()`) // Try most recently added loaders first (hence `.rev()`)
for loader in image_loaders.iter().rev() { for loader in image_loaders.iter().rev() {
match loader.load(self, uri, size_hint) { match loader.load(self, uri, size_hint) {
Err(load::LoadError::NotSupported) => continue, Err(load::LoadError::NotSupported) => continue,
Err(load::LoadError::FormatNotSupported { detected_format }) => {
format = format.or(detected_format);
continue;
}
result => return result, result => return result,
} }
} }
Err(load::LoadError::NoMatchingImageLoader) Err(load::LoadError::NoMatchingImageLoader {
detected_format: format,
})
} }
/// Try loading the texture from the given uri using any available texture loaders. /// Try loading the texture from the given uri using any available texture loaders.

View File

@@ -529,6 +529,10 @@ pub enum Event {
/// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`].
Screenshot { Screenshot {
viewport_id: crate::ViewportId, viewport_id: crate::ViewportId,
/// Whatever was passed to [`crate::ViewportCommand::Screenshot`].
user_data: crate::UserData,
image: std::sync::Arc<ColorImage>, image: std::sync::Arc<ColorImage>,
}, },
} }

View File

@@ -55,7 +55,7 @@ pub enum Key {
// `]` // `]`
CloseBracket, CloseBracket,
/// \`, also known as "backquote" or "grave" /// Also known as "backquote" or "grave"
Backtick, Backtick,
/// `-` /// `-`

View File

@@ -3,5 +3,7 @@
pub mod input; pub mod input;
mod key; mod key;
pub mod output; pub mod output;
mod user_data;
pub use key::Key; pub use key::Key;
pub use user_data::UserData;

View File

@@ -0,0 +1,74 @@
use std::{any::Any, sync::Arc};
/// A wrapper around `dyn Any`, used for passing custom user data
/// to [`crate::ViewportCommand::Screenshot`].
#[derive(Clone, Debug, Default)]
pub struct UserData {
/// A user value given to the screenshot command,
/// that will be returned in [`crate::Event::Screenshot`].
pub data: Option<Arc<dyn Any + Send + Sync>>,
}
impl UserData {
/// You can also use [`Self::default`].
pub fn new(user_info: impl Any + Send + Sync) -> Self {
Self {
data: Some(Arc::new(user_info)),
}
}
}
impl PartialEq for UserData {
fn eq(&self, other: &Self) -> bool {
match (&self.data, &other.data) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
}
}
}
impl Eq for UserData {}
impl std::hash::Hash for UserData {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.as_ref().map(Arc::as_ptr).hash(state);
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for UserData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_none() // can't serialize an `Any`
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for UserData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct UserDataVisitor;
impl<'de> serde::de::Visitor<'de> for UserDataVisitor {
type Value = UserData;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a None value")
}
fn visit_none<E>(self) -> Result<UserData, E>
where
E: serde::de::Error,
{
Ok(UserData::default())
}
}
deserializer.deserialize_option(UserDataVisitor)
}
}

View File

@@ -23,22 +23,30 @@ pub struct DragAndDrop {
impl DragAndDrop { impl DragAndDrop {
pub(crate) fn register(ctx: &Context) { pub(crate) fn register(ctx: &Context) {
ctx.on_end_pass("debug_text", std::sync::Arc::new(Self::end_pass)); ctx.on_begin_pass("drag_and_drop_begin_pass", Arc::new(Self::begin_pass));
ctx.on_end_pass("drag_and_drop_end_pass", Arc::new(Self::end_pass));
}
fn begin_pass(ctx: &Context) {
let has_any_payload = Self::has_any_payload(ctx);
if has_any_payload {
let abort_dnd = ctx.input_mut(|i| {
i.pointer.any_released()
|| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)
});
if abort_dnd {
Self::clear_payload(ctx);
}
}
} }
fn end_pass(ctx: &Context) { fn end_pass(ctx: &Context) {
let abort_dnd =
ctx.input(|i| i.pointer.any_released() || i.key_pressed(crate::Key::Escape));
let mut is_dragging = false; let mut is_dragging = false;
ctx.data_mut(|data| { ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL); let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
if abort_dnd {
state.payload = None;
}
is_dragging = state.payload.is_some(); is_dragging = state.payload.is_some();
}); });

View File

@@ -889,9 +889,9 @@ impl Default for PointerState {
press_start_time: None, press_start_time: None,
has_moved_too_much_for_a_click: false, has_moved_too_much_for_a_click: false,
started_decidedly_dragging: false, started_decidedly_dragging: false,
last_click_time: std::f64::NEG_INFINITY, last_click_time: f64::NEG_INFINITY,
last_last_click_time: std::f64::NEG_INFINITY, last_last_click_time: f64::NEG_INFINITY,
last_move_time: std::f64::NEG_INFINITY, last_move_time: f64::NEG_INFINITY,
pointer_events: vec![], pointer_events: vec![],
input_options: Default::default(), input_options: Default::default(),
} }

View File

@@ -95,6 +95,7 @@ impl LayerId {
} }
#[inline(always)] #[inline(always)]
#[deprecated = "Use `Memory::allows_interaction` instead"]
pub fn allow_interaction(&self) -> bool { pub fn allow_interaction(&self) -> bool {
self.order.allow_interaction() self.order.allow_interaction()
} }

View File

@@ -2,7 +2,7 @@ use crate::{
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2}, emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
Align, Align,
}; };
use std::f32::INFINITY; const INFINITY: f32 = f32::INFINITY;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@@ -3,7 +3,7 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>. //! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//! //!
//! `egui` is in heavy development, with each new version having breaking changes. //! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.77.0 or later to use `egui`. //! You need to have rust 1.79.0 or later to use `egui`.
//! //!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template) //! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe). //! which uses [`eframe`](https://docs.rs/eframe).
@@ -393,6 +393,7 @@
#![allow(clippy::manual_range_contains)] #![allow(clippy::manual_range_contains)]
mod animation_manager; mod animation_manager;
pub mod cache;
pub mod containers; pub mod containers;
mod context; mod context;
mod data; mod data;
@@ -471,7 +472,7 @@ pub use self::{
output::{ output::{
self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo, self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo,
}, },
Key, Key, UserData,
}, },
drag_and_drop::DragAndDrop, drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode, epaint::text::TextWrapMode,

View File

@@ -77,16 +77,19 @@ pub enum LoadError {
/// Programmer error: There are no image loaders installed. /// Programmer error: There are no image loaders installed.
NoImageLoaders, NoImageLoaders,
/// A specific loader does not support this scheme, protocol or image format. /// A specific loader does not support this scheme or protocol.
NotSupported, NotSupported,
/// A specific loader does not support the format of the image.
FormatNotSupported { detected_format: Option<String> },
/// Programmer error: Failed to find the bytes for this image because /// Programmer error: Failed to find the bytes for this image because
/// there was no [`BytesLoader`] supporting the scheme. /// there was no [`BytesLoader`] supporting the scheme.
NoMatchingBytesLoader, NoMatchingBytesLoader,
/// Programmer error: Failed to parse the bytes as an image because /// Programmer error: Failed to parse the bytes as an image because
/// there was no [`ImageLoader`] supporting the scheme. /// there was no [`ImageLoader`] supporting the format.
NoMatchingImageLoader, NoMatchingImageLoader { detected_format: Option<String> },
/// Programmer error: no matching [`TextureLoader`]. /// Programmer error: no matching [`TextureLoader`].
/// Because of the [`DefaultTextureLoader`], this error should never happen. /// Because of the [`DefaultTextureLoader`], this error should never happen.
@@ -96,6 +99,20 @@ pub enum LoadError {
Loading(String), Loading(String),
} }
impl LoadError {
/// Returns the (approximate) size of the error message in bytes.
pub fn byte_size(&self) -> usize {
match self {
Self::FormatNotSupported { detected_format }
| Self::NoMatchingImageLoader { detected_format } => {
detected_format.as_ref().map_or(0, |s| s.len())
}
Self::Loading(message) => message.len(),
_ => std::mem::size_of::<Self>(),
}
}
}
impl Display for LoadError { impl Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@@ -105,12 +122,15 @@ impl Display for LoadError {
Self::NoMatchingBytesLoader => f.write_str("No matching BytesLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), Self::NoMatchingBytesLoader => f.write_str("No matching BytesLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."),
Self::NoMatchingImageLoader => f.write_str("No matching ImageLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), Self::NoMatchingImageLoader { detected_format: None } => f.write_str("No matching ImageLoader. Either no ImageLoader is installed or the image is corrupted / has an unsupported format."),
Self::NoMatchingImageLoader { detected_format: Some(detected_format) } => write!(f, "No matching ImageLoader for format: {detected_format:?}. Make sure you enabled the necessary features on the image crate."),
Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"), Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"),
Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"), Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"),
Self::FormatNotSupported { detected_format } => write!(f, "Image format not supported by this loader: {detected_format:?}"),
Self::Loading(message) => f.write_str(message), Self::Loading(message) => f.write_str(message),
} }
} }

View File

@@ -54,7 +54,7 @@ pub struct Memory {
/// so as not to lock the UI thread. /// so as not to lock the UI thread.
/// ///
/// ``` /// ```
/// use egui::util::cache::{ComputerMut, FrameCache}; /// use egui::cache::{ComputerMut, FrameCache};
/// ///
/// #[derive(Default)] /// #[derive(Default)]
/// struct CharCounter {} /// struct CharCounter {}
@@ -72,7 +72,7 @@ pub struct Memory {
/// }); /// });
/// ``` /// ```
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub caches: crate::util::cache::CacheStorage, pub caches: crate::cache::CacheStorage,
// ------------------------------------------ // ------------------------------------------
/// new fonts that will be applied at the start of the next frame /// new fonts that will be applied at the start of the next frame
@@ -513,6 +513,12 @@ pub(crate) struct Focus {
/// Set when looking for widget with navigational keys like arrows, tab, shift+tab. /// Set when looking for widget with navigational keys like arrows, tab, shift+tab.
focus_direction: FocusDirection, focus_direction: FocusDirection,
/// The top-most modal layer from the previous frame.
top_modal_layer: Option<LayerId>,
/// The top-most modal layer from the current frame.
top_modal_layer_current_frame: Option<LayerId>,
/// A cache of widget IDs that are interested in focus with their corresponding rectangles. /// A cache of widget IDs that are interested in focus with their corresponding rectangles.
focus_widgets_cache: IdMap<Rect>, focus_widgets_cache: IdMap<Rect>,
} }
@@ -623,6 +629,8 @@ impl Focus {
self.focused_widget = None; self.focused_widget = None;
} }
} }
self.top_modal_layer = self.top_modal_layer_current_frame.take();
} }
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool { pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
@@ -676,6 +684,14 @@ impl Focus {
self.last_interested = Some(id); self.last_interested = Some(id);
} }
fn set_modal_layer(&mut self, layer_id: LayerId) {
self.top_modal_layer_current_frame = Some(layer_id);
}
pub(crate) fn top_modal_layer(&self) -> Option<LayerId> {
self.top_modal_layer
}
fn reset_focus(&mut self) { fn reset_focus(&mut self) {
self.focus_direction = FocusDirection::None; self.focus_direction = FocusDirection::None;
} }
@@ -720,7 +736,7 @@ impl Focus {
let current_rect = self.focus_widgets_cache.get(&current_focused.id)?; let current_rect = self.focus_widgets_cache.get(&current_focused.id)?;
let mut best_score = std::f32::INFINITY; let mut best_score = f32::INFINITY;
let mut best_id = None; let mut best_id = None;
for (candidate_id, candidate_rect) in &self.focus_widgets_cache { for (candidate_id, candidate_rect) in &self.focus_widgets_cache {
@@ -802,7 +818,15 @@ impl Memory {
/// Top-most layer at the given position. /// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> { pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
self.areas().layer_id_at(pos, &self.layer_transforms) self.areas()
.layer_id_at(pos, &self.layer_transforms)
.and_then(|layer_id| {
if self.is_above_modal_layer(layer_id) {
Some(layer_id)
} else {
self.top_modal_layer()
}
})
} }
/// An iterator over all layers. Back-to-front, top is last. /// An iterator over all layers. Back-to-front, top is last.
@@ -877,6 +901,30 @@ impl Memory {
} }
} }
/// Returns true if
/// - this layer is the top-most modal layer or above it
/// - there is no modal layer
pub fn is_above_modal_layer(&self, layer_id: LayerId) -> bool {
if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) {
matches!(
self.areas().compare_order(layer_id, modal_layer),
std::cmp::Ordering::Equal | std::cmp::Ordering::Greater
)
} else {
true
}
}
/// Does this layer allow interaction?
/// Returns true if
/// - the layer is not behind a modal layer
/// - the [`Order`] allows interaction
pub fn allows_interaction(&self, layer_id: LayerId) -> bool {
let is_above_modal_layer = self.is_above_modal_layer(layer_id);
let ordering_allows_interaction = layer_id.order.allow_interaction();
is_above_modal_layer && ordering_allows_interaction
}
/// Register this widget as being interested in getting keyboard focus. /// Register this widget as being interested in getting keyboard focus.
/// This will allow the user to select it with tab and shift-tab. /// This will allow the user to select it with tab and shift-tab.
/// This is normally done automatically when handling interactions, /// This is normally done automatically when handling interactions,
@@ -884,11 +932,36 @@ impl Memory {
/// e.g. before deciding which type of underlying widget to use, /// e.g. before deciding which type of underlying widget to use,
/// as in the [`crate::DragValue`] widget, so a widget can be focused /// as in the [`crate::DragValue`] widget, so a widget can be focused
/// and rendered correctly in a single frame. /// and rendered correctly in a single frame.
///
/// Pass in the `layer_id` of the layer that the widget is in.
#[inline(always)] #[inline(always)]
pub fn interested_in_focus(&mut self, id: Id) { pub fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) {
if !self.allows_interaction(layer_id) {
return;
}
self.focus_mut().interested_in_focus(id); self.focus_mut().interested_in_focus(id);
} }
/// Limit focus to widgets on the given layer and above.
/// If this is called multiple times per frame, the top layer wins.
pub fn set_modal_layer(&mut self, layer_id: LayerId) {
if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame) {
if matches!(
self.areas().compare_order(layer_id, current),
std::cmp::Ordering::Less
) {
return;
}
}
self.focus_mut().set_modal_layer(layer_id);
}
/// Get the top modal layer (from the previous frame).
pub fn top_modal_layer(&self) -> Option<LayerId> {
self.focus()?.top_modal_layer()
}
/// Stop editing the active [`TextEdit`](crate::TextEdit) (if any). /// Stop editing the active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)] #[inline(always)]
pub fn stop_text_input(&mut self) { pub fn stop_text_input(&mut self) {
@@ -1037,6 +1110,9 @@ impl Memory {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Map containing the index of each layer in the order list, for quick lookups.
type OrderMap = HashMap<LayerId, usize>;
/// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s. /// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s.
/// These [`Area`](crate::containers::area::Area)s can be in any [`Order`]. /// These [`Area`](crate::containers::area::Area)s can be in any [`Order`].
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@@ -1048,6 +1124,9 @@ pub struct Areas {
/// Back-to-front, top is last. /// Back-to-front, top is last.
order: Vec<LayerId>, order: Vec<LayerId>,
/// Actual order of the layers, pre-calculated each frame.
order_map: OrderMap,
visible_last_frame: ahash::HashSet<LayerId>, visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>, visible_current_frame: ahash::HashSet<LayerId>,
@@ -1079,12 +1158,28 @@ impl Areas {
} }
/// For each layer, which [`Self::order`] is it in? /// For each layer, which [`Self::order`] is it in?
pub(crate) fn order_map(&self) -> HashMap<LayerId, usize> { pub(crate) fn order_map(&self) -> &OrderMap {
self.order &self.order_map
}
/// Compare the order of two layers, based on the order list from last frame.
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering {
if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) {
a.cmp(b)
} else {
a.order.cmp(&b.order)
}
}
/// Calculates the order map.
fn calculate_order_map(&mut self) {
self.order_map = self
.order
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, id)| (*id, i)) .map(|(i, id)| (*id, i))
.collect() .collect();
} }
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) { pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) {
@@ -1209,6 +1304,7 @@ impl Areas {
}; };
order.splice(parent_pos..=parent_pos, moved_layers); order.splice(parent_pos..=parent_pos, moved_layers);
} }
self.calculate_order_map();
} }
} }

View File

@@ -288,7 +288,7 @@ pub struct Style {
/// If true and scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift /// If true and scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift
pub always_scroll_the_only_direction: bool, pub always_scroll_the_only_direction: bool,
/// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [Ui::scroll_to_rect]. /// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [`Ui::scroll_to_rect`].
pub scroll_animation: ScrollAnimation, pub scroll_animation: ScrollAnimation,
} }

View File

@@ -97,8 +97,19 @@ pub fn paint_text_selection(
pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
let stroke = visuals.text_cursor.stroke; let stroke = visuals.text_cursor.stroke;
let top = cursor_rect.center_top(); // Ensure the cursor is aligned to the pixel grid for whole number widths.
let bottom = cursor_rect.center_bottom(); // See https://github.com/emilk/egui/issues/5164
let (top, bottom) = if (stroke.width as usize) % 2 == 0 {
(
painter.round_pos_to_pixels(cursor_rect.center_top()),
painter.round_pos_to_pixels(cursor_rect.center_bottom()),
)
} else {
(
painter.round_pos_to_pixel_center(cursor_rect.center_top()),
painter.round_pos_to_pixel_center(cursor_rect.center_bottom()),
)
};
painter.line_segment([top, bottom], (stroke.width, stroke.color)); painter.line_segment([top, bottom], (stroke.width, stroke.color));
@@ -122,14 +133,14 @@ pub fn paint_text_cursor(
ui: &Ui, ui: &Ui,
painter: &Painter, painter: &Painter,
primary_cursor_rect: Rect, primary_cursor_rect: Rect,
time_since_last_edit: f64, time_since_last_interaction: f64,
) { ) {
if ui.visuals().text_cursor.blink { if ui.visuals().text_cursor.blink {
let on_duration = ui.visuals().text_cursor.on_duration; let on_duration = ui.visuals().text_cursor.on_duration;
let off_duration = ui.visuals().text_cursor.off_duration; let off_duration = ui.visuals().text_cursor.off_duration;
let total_duration = on_duration + off_duration; let total_duration = on_duration + off_duration;
let time_in_cycle = (time_since_last_edit % (total_duration as f64)) as f32; let time_in_cycle = (time_since_last_interaction % (total_duration as f64)) as f32;
let wake_in = if time_in_cycle < on_duration { let wake_in = if time_in_cycle < on_duration {
// Cursor is visible // Cursor is visible

View File

@@ -24,6 +24,9 @@ pub enum UiKind {
/// A bottom [`crate::TopBottomPanel`]. /// A bottom [`crate::TopBottomPanel`].
BottomPanel, BottomPanel,
/// A modal [`crate::Modal`].
Modal,
/// A [`crate::Frame`]. /// A [`crate::Frame`].
Frame, Frame,
@@ -82,6 +85,7 @@ impl UiKind {
Self::Window Self::Window
| Self::Menu | Self::Menu
| Self::Modal
| Self::Popup | Self::Popup
| Self::Tooltip | Self::Tooltip
| Self::Picker | Self::Picker
@@ -228,6 +232,12 @@ impl UiStack {
self.kind().map_or(false, |kind| kind.is_panel()) self.kind().map_or(false, |kind| kind.is_panel())
} }
/// Is this [`crate::Ui`] an [`crate::Area`]?
#[inline]
pub fn is_area_ui(&self) -> bool {
self.kind().map_or(false, |kind| kind.is_area())
}
/// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]? /// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]?
#[inline] #[inline]
pub fn is_root_ui(&self) -> bool { pub fn is_root_ui(&self) -> bool {

View File

@@ -1,6 +1,5 @@
//! Miscellaneous tools used by the rest of egui. //! Miscellaneous tools used by the rest of egui.
pub mod cache;
pub(crate) mod fixed_cache; pub(crate) mod fixed_cache;
pub mod id_type_map; pub mod id_type_map;
pub mod undoer; pub mod undoer;
@@ -9,3 +8,7 @@ pub use id_type_map::IdTypeMap;
pub use epaint::emath::History; pub use epaint::emath::History;
pub use epaint::util::{hash, hash_with}; pub use epaint::util::{hash, hash_with};
/// Deprecated alias for [`crate::cache`].
#[deprecated = "Use egui::cache instead"]
pub use crate::cache;

View File

@@ -1058,8 +1058,8 @@ pub enum ViewportCommand {
/// Take a screenshot. /// Take a screenshot.
/// ///
/// The results are returned in `crate::Event::Screenshot`. /// The results are returned in [`crate::Event::Screenshot`].
Screenshot, Screenshot(crate::UserData),
/// Request cut of the current selection /// Request cut of the current selection
/// ///
@@ -1100,6 +1100,8 @@ impl ViewportCommand {
} }
} }
// ----------------------------------------------------------------------------
/// Describes a viewport, i.e. a native window. /// Describes a viewport, i.e. a native window.
/// ///
/// This is returned by [`crate::Context::run`] on each frame, and should be applied /// This is returned by [`crate::Context::run`] on each frame, and should be applied

View File

@@ -37,6 +37,7 @@ pub struct Button<'a> {
min_size: Vec2, min_size: Vec2,
rounding: Option<Rounding>, rounding: Option<Rounding>,
selected: bool, selected: bool,
image_tint_follows_text_color: bool,
} }
impl<'a> Button<'a> { impl<'a> Button<'a> {
@@ -70,6 +71,7 @@ impl<'a> Button<'a> {
min_size: Vec2::ZERO, min_size: Vec2::ZERO,
rounding: None, rounding: None,
selected: false, selected: false,
image_tint_follows_text_color: false,
} }
} }
@@ -156,6 +158,18 @@ impl<'a> Button<'a> {
self self
} }
/// If true, the tint of the image is multiplied by the widget text color.
///
/// This makes sense for images that are white, that should have the same color as the text color.
/// This will also make the icon color depend on hover state.
///
/// Default: `false`.
#[inline]
pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
self.image_tint_follows_text_color = image_tint_follows_text_color;
self
}
/// Show some text on the right side of the button, in weak color. /// Show some text on the right side of the button, in weak color.
/// ///
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`). /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
@@ -190,6 +204,7 @@ impl Widget for Button<'_> {
min_size, min_size,
rounding, rounding,
selected, selected,
image_tint_follows_text_color,
} = self; } = self;
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
@@ -319,12 +334,16 @@ impl Widget for Button<'_> {
let image_rect = Rect::from_min_size(image_pos, image_size); let image_rect = Rect::from_min_size(image_pos, image_size);
cursor_x += image_size.x; cursor_x += image_size.x;
let tlr = image.load_for_size(ui.ctx(), image_size); let tlr = image.load_for_size(ui.ctx(), image_size);
let mut image_options = image.image_options().clone();
if image_tint_follows_text_color {
image_options.tint = image_options.tint * visuals.text_color();
}
widgets::image::paint_texture_load_result( widgets::image::paint_texture_load_result(
ui, ui,
&tlr, &tlr,
image_rect, image_rect,
image.show_loading_spinner, image.show_loading_spinner,
image.image_options(), &image_options,
); );
response = widgets::image::texture_load_result_response( response = widgets::image::texture_load_result_response(
&image.source(ui.ctx()), &image.source(ui.ctx()),

View File

@@ -452,7 +452,7 @@ impl<'a> Widget for DragValue<'a> {
// in button mode for just one frame. This is important for // in button mode for just one frame. This is important for
// screen readers. // screen readers.
let is_kb_editing = ui.memory_mut(|mem| { let is_kb_editing = ui.memory_mut(|mem| {
mem.interested_in_focus(id); mem.interested_in_focus(id, ui.layer_id());
mem.has_focus(id) mem.has_focus(id)
}); });

View File

@@ -1030,7 +1030,7 @@ impl<'a> Widget for Slider<'a> {
// Logarithmic sliders are allowed to include zero and infinity, // Logarithmic sliders are allowed to include zero and infinity,
// even though mathematically it doesn't make sense. // even though mathematically it doesn't make sense.
use std::f64::INFINITY; const INFINITY: f64 = f64::INFINITY;
/// When the user asks for an infinitely large range (e.g. logarithmic from zero), /// When the user asks for an infinitely large range (e.g. logarithmic from zero),
/// give a scale that this many orders of magnitude in size. /// give a scale that this many orders of magnitude in size.

View File

@@ -1,5 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use emath::Rect;
use epaint::text::{cursor::CCursor, Galley, LayoutJob}; use epaint::text::{cursor::CCursor, Galley, LayoutJob};
use crate::{ use crate::{
@@ -602,6 +603,8 @@ impl<'t> TextEdit<'t> {
if did_interact || response.clicked() { if did_interact || response.clicked() {
ui.memory_mut(|mem| mem.request_focus(response.id)); ui.memory_mut(|mem| mem.request_focus(response.id));
state.last_interaction_time = ui.ctx().input(|i| i.time);
} }
} }
} }
@@ -720,6 +723,16 @@ impl<'t> TextEdit<'t> {
} }
} }
// Allocate additional space if edits were made this frame that changed the size. This is important so that,
// if there's a ScrollArea, it can properly scroll to the cursor.
let extra_size = galley.size() - rect.size();
if extra_size.x > 0.0 || extra_size.y > 0.0 {
ui.allocate_rect(
Rect::from_min_size(outer_rect.max, extra_size),
Sense::hover(),
);
}
painter.galley(galley_pos, galley.clone(), text_color); painter.galley(galley_pos, galley.clone(), text_color);
if has_focus { if has_focus {
@@ -727,16 +740,15 @@ impl<'t> TextEdit<'t> {
let primary_cursor_rect = let primary_cursor_rect =
cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height); cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height);
let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO(emilk): remove this HACK workaround for https://github.com/emilk/egui/issues/1531 if response.changed || selection_changed {
if (response.changed || selection_changed) && !is_fully_visible {
// Scroll to keep primary cursor in view: // Scroll to keep primary cursor in view:
ui.scroll_to_rect(primary_cursor_rect, None); ui.scroll_to_rect(primary_cursor_rect + margin, None);
} }
if text.is_mutable() && interactive { if text.is_mutable() && interactive {
let now = ui.ctx().input(|i| i.time); let now = ui.ctx().input(|i| i.time);
if response.changed || selection_changed { if response.changed || selection_changed {
state.last_edit_time = now; state.last_interaction_time = now;
} }
// Only show (and blink) cursor if the egui viewport has focus. // Only show (and blink) cursor if the egui viewport has focus.
@@ -749,7 +761,7 @@ impl<'t> TextEdit<'t> {
ui, ui,
&painter, &painter,
primary_cursor_rect, primary_cursor_rect,
now - state.last_edit_time, now - state.last_interaction_time,
); );
} }

View File

@@ -53,10 +53,10 @@ pub struct TextEditState {
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub(crate) singleline_offset: f32, pub(crate) singleline_offset: f32,
/// When did the user last press a key? /// When did the user last press a key or click on the `TextEdit`.
/// Used to pause the cursor animation when typing. /// Used to pause the cursor animation when typing.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub(crate) last_edit_time: f64, pub(crate) last_interaction_time: f64,
} }
impl TextEditState { impl TextEditState {

View File

@@ -33,6 +33,7 @@ impl Default for Demos {
Box::<super::highlighting::Highlighting>::default(), Box::<super::highlighting::Highlighting>::default(),
Box::<super::interactive_container::InteractiveContainerDemo>::default(), Box::<super::interactive_container::InteractiveContainerDemo>::default(),
Box::<super::MiscDemoWindow>::default(), Box::<super::MiscDemoWindow>::default(),
Box::<super::modals::Modals>::default(),
Box::<super::multi_touch::MultiTouch>::default(), Box::<super::multi_touch::MultiTouch>::default(),
Box::<super::painting::Painting>::default(), Box::<super::painting::Painting>::default(),
Box::<super::pan_zoom::PanZoom>::default(), Box::<super::pan_zoom::PanZoom>::default(),
@@ -425,7 +426,7 @@ mod tests {
let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options); let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options);
if let Err(err) = result { if let Err(err) = result {
errors.push(err); errors.push(err.to_string());
} }
} }

View File

@@ -17,6 +17,7 @@ pub mod frame_demo;
pub mod highlighting; pub mod highlighting;
pub mod interactive_container; pub mod interactive_container;
pub mod misc_demo_window; pub mod misc_demo_window;
pub mod modals;
pub mod multi_touch; pub mod multi_touch;
pub mod paint_bezier; pub mod paint_bezier;
pub mod painting; pub mod painting;

View File

@@ -0,0 +1,287 @@
use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget, Window};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Modals {
user_modal_open: bool,
save_modal_open: bool,
save_progress: Option<f32>,
role: &'static str,
name: String,
}
impl Default for Modals {
fn default() -> Self {
Self {
user_modal_open: false,
save_modal_open: false,
save_progress: None,
role: Self::ROLES[0],
name: "John Doe".to_owned(),
}
}
}
impl Modals {
const ROLES: [&'static str; 2] = ["user", "admin"];
}
impl crate::Demo for Modals {
fn name(&self) -> &'static str {
"🗖 Modals"
}
fn show(&mut self, ctx: &Context, open: &mut bool) {
use crate::View as _;
Window::new(self.name())
.open(open)
.vscroll(false)
.resizable(false)
.show(ctx, |ui| self.ui(ui));
}
}
impl crate::View for Modals {
fn ui(&mut self, ui: &mut Ui) {
let Self {
user_modal_open,
save_modal_open,
save_progress,
role,
name,
} = self;
ui.horizontal(|ui| {
if ui.button("Open User Modal").clicked() {
*user_modal_open = true;
}
if ui.button("Open Save Modal").clicked() {
*save_modal_open = true;
}
});
ui.label("Click one of the buttons to open a modal.");
ui.label("Modals have a backdrop and prevent interaction with the rest of the UI.");
ui.label(
"You can show modals on top of each other and close the topmost modal with \
escape or by clicking outside the modal.",
);
if *user_modal_open {
let modal = Modal::new(Id::new("Modal A")).show(ui.ctx(), |ui| {
ui.set_width(250.0);
ui.heading("Edit User");
ui.label("Name:");
ui.text_edit_singleline(name);
ComboBox::new("role", "Role")
.selected_text(*role)
.show_ui(ui, |ui| {
for r in Self::ROLES {
ui.selectable_value(role, r, r);
}
});
ui.separator();
egui::Sides::new().show(
ui,
|_ui| {},
|ui| {
if ui.button("Save").clicked() {
*save_modal_open = true;
}
if ui.button("Cancel").clicked() {
*user_modal_open = false;
}
},
);
});
if modal.should_close() {
*user_modal_open = false;
}
}
if *save_modal_open {
let modal = Modal::new(Id::new("Modal B")).show(ui.ctx(), |ui| {
ui.set_width(200.0);
ui.heading("Save? Are you sure?");
ui.add_space(32.0);
egui::Sides::new().show(
ui,
|_ui| {},
|ui| {
if ui.button("Yes Please").clicked() {
*save_progress = Some(0.0);
}
if ui.button("No Thanks").clicked() {
*save_modal_open = false;
}
},
);
});
if modal.should_close() {
*save_modal_open = false;
}
}
if let Some(progress) = *save_progress {
Modal::new(Id::new("Modal C")).show(ui.ctx(), |ui| {
ui.set_width(70.0);
ui.heading("Saving…");
ProgressBar::new(progress).ui(ui);
if progress >= 1.0 {
*save_progress = None;
*save_modal_open = false;
*user_modal_open = false;
} else {
*save_progress = Some(progress + 0.003);
ui.ctx().request_repaint();
}
});
}
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
}
}
#[cfg(test)]
mod tests {
use crate::demo::modals::Modals;
use crate::Demo;
use egui::accesskit::Role;
use egui::Key;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
#[test]
fn clicking_escape_when_popup_open_should_not_close_modal() {
let initial_state = Modals {
user_modal_open: true,
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
harness.get_by_role(Role::ComboBox).click();
harness.run();
assert!(harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(harness.state().user_modal_open);
harness.press_key(Key::Escape);
harness.run();
assert!(!harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(harness.state().user_modal_open);
}
#[test]
fn escape_should_close_top_modal() {
let initial_state = Modals {
user_modal_open: true,
save_modal_open: true,
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
assert!(harness.state().user_modal_open);
assert!(harness.state().save_modal_open);
harness.press_key(Key::Escape);
harness.run();
assert!(harness.state().user_modal_open);
assert!(!harness.state().save_modal_open);
}
#[test]
fn should_match_snapshot() {
let initial_state = Modals {
user_modal_open: true,
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
let mut results = Vec::new();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_1"));
harness.get_by_label("Save").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_2"));
harness.get_by_label("Yes Please").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_3"));
for result in results {
result.unwrap();
}
}
// This tests whether the backdrop actually prevents interaction with lower layers.
#[test]
fn backdrop_should_prevent_focusing_lower_area() {
let initial_state = Modals {
save_modal_open: true,
save_progress: Some(0.0),
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
harness.get_by_label("Yes Please").simulate_click();
harness.run();
// This snapshots should show the progress bar modal on top of the save modal.
harness.wgpu_snapshot("modals_backdrop_should_prevent_focusing_lower_area");
}
}

View File

@@ -73,20 +73,29 @@ impl crate::View for PanZoom {
for (i, (pos, callback)) in [ for (i, (pos, callback)) in [
( (
egui::Pos2::new(0.0, 0.0), egui::Pos2::new(0.0, 0.0),
Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) Box::new(|ui: &mut egui::Ui, _: &mut Self| {
as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>, ui.button("top left").on_hover_text("Normal tooltip")
}) as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>,
), ),
( (
egui::Pos2::new(0.0, 120.0), egui::Pos2::new(0.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), Box::new(|ui: &mut egui::Ui, _| {
ui.button("bottom left").on_hover_text("Normal tooltip")
}),
), ),
( (
egui::Pos2::new(120.0, 120.0), egui::Pos2::new(120.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), Box::new(|ui: &mut egui::Ui, _| {
ui.button("right bottom")
.on_hover_text_at_pointer("Tooltip at pointer")
}),
), ),
( (
egui::Pos2::new(120.0, 0.0), egui::Pos2::new(120.0, 0.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), Box::new(|ui: &mut egui::Ui, _| {
ui.button("right top")
.on_hover_text_at_pointer("Tooltip at pointer")
}),
), ),
( (
egui::Pos2::new(60.0, 60.0), egui::Pos2::new(60.0, 60.0),

View File

@@ -1,5 +1,4 @@
use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui}; use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui};
use std::f64::INFINITY;
/// Showcase sliders /// Showcase sliders
#[derive(PartialEq)] #[derive(PartialEq)]
@@ -77,7 +76,7 @@ impl crate::View for Sliders {
let (type_min, type_max) = if *integer { let (type_min, type_max) = if *integer {
((i32::MIN as f64), (i32::MAX as f64)) ((i32::MIN as f64), (i32::MAX as f64))
} else if *logarithmic { } else if *logarithmic {
(-INFINITY, INFINITY) (-f64::INFINITY, f64::INFINITY)
} else { } else {
(-1e5, 1e5) // linear sliders make little sense with huge numbers (-1e5, 1e5) // linear sliders make little sense with huge numbers
}; };

View File

@@ -13,7 +13,7 @@ pub enum Item<'a> {
// TODO(emilk): add Style here so empty heading still uses up the right amount of space. // TODO(emilk): add Style here so empty heading still uses up the right amount of space.
Newline, Newline,
/// /// Text
Text(Style, &'a str), Text(Style, &'a str),
/// title, url /// title, url

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8d4f004ee11ea68ae0f30657601b6e51403fcc3ca91fa5b8cdcb58585d8d40d oid sha256:01aaa4ef1a167a94fa1e5163550aabe4fa5e9f3a012b26170fe3088a6ca32d94
size 78318 size 81064

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:79ce1dbf7627579d4e10de6494e34d8fd9685506d7b35cb3c9148f90f8c01366 oid sha256:ccfda16ef7cdf94f7fbbd2c0f8df6f6de7904969e2a66337920c32608a6f9f05
size 25144 size 25357

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ pub struct RetainedImage {
} }
impl RetainedImage { impl RetainedImage {
pub fn from_color_image(debug_name: impl Into<String>, image: ColorImage) -> Self { pub fn from_color_image(debug_name: impl Into<String>, image: egui::ColorImage) -> Self {
Self { Self {
debug_name: debug_name.into(), debug_name: debug_name.into(),
size: image.size, size: image.size,
@@ -54,7 +54,7 @@ impl RetainedImage {
) -> Result<Self, String> { ) -> Result<Self, String> {
Ok(Self::from_color_image( Ok(Self::from_color_image(
debug_name, debug_name,
load_image_bytes(image_bytes)?, load_image_bytes(image_bytes).map_err(|err| err.to_string())?,
)) ))
} }
@@ -154,7 +154,7 @@ impl RetainedImage {
self.texture self.texture
.lock() .lock()
.get_or_insert_with(|| { .get_or_insert_with(|| {
let image: &mut ColorImage = &mut self.image.lock(); let image: &mut egui::ColorImage = &mut self.image.lock();
let image = std::mem::take(image); let image = std::mem::take(image);
ctx.load_texture(&self.debug_name, image, self.options) ctx.load_texture(&self.debug_name, image, self.options)
}) })
@@ -190,8 +190,6 @@ impl RetainedImage {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
use egui::ColorImage;
/// Load a (non-svg) image. /// Load a (non-svg) image.
/// ///
/// Requires the "image" feature. You must also opt-in to the image formats you need /// Requires the "image" feature. You must also opt-in to the image formats you need
@@ -200,9 +198,19 @@ use egui::ColorImage;
/// # Errors /// # Errors
/// On invalid image or unsupported image format. /// On invalid image or unsupported image format.
#[cfg(feature = "image")] #[cfg(feature = "image")]
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, String> { pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::load::LoadError> {
crate::profile_function!(); crate::profile_function!();
let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?; let image = image::load_from_memory(image_bytes).map_err(|err| match err {
image::ImageError::Unsupported(err) => match err.kind() {
image::error::UnsupportedErrorKind::Format(format) => {
egui::load::LoadError::FormatNotSupported {
detected_format: Some(format.to_string()),
}
}
_ => egui::load::LoadError::Loading(err.to_string()),
},
err => egui::load::LoadError::Loading(err.to_string()),
})?;
let size = [image.width() as _, image.height() as _]; let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8(); let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples(); let pixels = image_buffer.as_flat_samples();

View File

@@ -7,7 +7,7 @@ use egui::{
use image::ImageFormat; use image::ImageFormat;
use std::{mem::size_of, path::Path, sync::Arc}; use std::{mem::size_of, path::Path, sync::Arc};
type Entry = Result<Arc<ColorImage>, String>; type Entry = Result<Arc<ColorImage>, LoadError>;
#[derive(Default)] #[derive(Default)]
pub struct ImageCrateLoader { pub struct ImageCrateLoader {
@@ -31,9 +31,14 @@ fn is_supported_uri(uri: &str) -> bool {
.any(|format_ext| ext == *format_ext) .any(|format_ext| ext == *format_ext)
} }
fn is_unsupported_mime(mime: &str) -> bool { fn is_supported_mime(mime: &str) -> bool {
// This is the default mime type for binary files, so this might actually be a valid image,
// let's relay on image's format guessing
if mime == "application/octet-stream" {
return true;
}
// Uses only the enabled image crate features // Uses only the enabled image crate features
!ImageFormat::all() ImageFormat::all()
.filter(ImageFormat::reading_enabled) .filter(ImageFormat::reading_enabled)
.map(|fmt| fmt.to_mime_type()) .map(|fmt| fmt.to_mime_type())
.any(|format_mime| mime == format_mime) .any(|format_mime| mime == format_mime)
@@ -46,12 +51,12 @@ impl ImageLoader for ImageCrateLoader {
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult { fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
// three stages of guessing if we support loading the image: // three stages of guessing if we support loading the image:
// 1. URI extension // 1. URI extension (only done for files)
// 2. Mime from `BytesPoll::Ready` // 2. Mime from `BytesPoll::Ready`
// 3. image::guess_format // 3. image::guess_format (used internally by image::load_from_memory)
// (1) // (1)
if !is_supported_uri(uri) { if uri.starts_with("file://") && !is_supported_uri(uri) {
return Err(LoadError::NotSupported); return Err(LoadError::NotSupported);
} }
@@ -59,26 +64,26 @@ impl ImageLoader for ImageCrateLoader {
if let Some(entry) = cache.get(uri).cloned() { if let Some(entry) = cache.get(uri).cloned() {
match entry { match entry {
Ok(image) => Ok(ImagePoll::Ready { image }), Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Loading(err)), Err(err) => Err(err),
} }
} else { } else {
match ctx.try_load_bytes(uri) { match ctx.try_load_bytes(uri) {
Ok(BytesPoll::Ready { bytes, mime, .. }) => { Ok(BytesPoll::Ready { bytes, mime, .. }) => {
// (2 and 3) // (2)
if mime.as_deref().is_some_and(is_unsupported_mime) if let Some(mime) = mime {
|| image::guess_format(&bytes).is_err() if !is_supported_mime(&mime) {
{ return Err(LoadError::FormatNotSupported {
return Err(LoadError::NotSupported); detected_format: Some(mime),
});
}
} }
// (3)
log::trace!("started loading {uri:?}"); log::trace!("started loading {uri:?}");
let result = crate::image::load_image_bytes(&bytes).map(Arc::new); let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
log::trace!("finished loading {uri:?}"); log::trace!("finished loading {uri:?}");
cache.insert(uri.into(), result.clone()); cache.insert(uri.into(), result.clone());
match result { result.map(|image| ImagePoll::Ready { image })
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Loading(err)),
}
} }
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
Err(err) => Err(err), Err(err) => Err(err),
@@ -100,7 +105,7 @@ impl ImageLoader for ImageCrateLoader {
.values() .values()
.map(|result| match result { .map(|result| match result {
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(), Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
Err(err) => err.len(), Err(err) => err.byte_size(),
}) })
.sum() .sum()
} }

View File

@@ -33,9 +33,7 @@ pub fn highlight(
// performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available
// (ui.ctx(), ui.style()) can be used // (ui.ctx(), ui.style()) can be used
impl egui::util::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter {
for Highlighter
{
fn compute( fn compute(
&mut self, &mut self,
(font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str), (font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str),
@@ -44,7 +42,7 @@ pub fn highlight(
} }
} }
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>; type HighlightCache = egui::cache::FrameCache<LayoutJob, Highlighter>;
let font_id = style let font_id = style
.override_font_id .override_font_id

View File

@@ -53,6 +53,9 @@ impl SnapshotOptions {
pub enum SnapshotError { pub enum SnapshotError {
/// Image did not match snapshot /// Image did not match snapshot
Diff { Diff {
/// Name of the test
name: String,
/// Count of pixels that were different /// Count of pixels that were different
diff: i32, diff: i32,
@@ -72,6 +75,9 @@ pub enum SnapshotError {
/// The size of the image did not match the snapshot /// The size of the image did not match the snapshot
SizeMismatch { SizeMismatch {
/// Name of the test
name: String,
/// Expected size /// Expected size
expected: (u32, u32), expected: (u32, u32),
@@ -89,32 +95,43 @@ pub enum SnapshotError {
}, },
} }
const HOW_TO_UPDATE_SCREENSHOTS: &str =
"Run `UPDATE_SNAPSHOTS=1 cargo test` 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 {
match self { match self {
Self::Diff { diff, diff_path } => { Self::Diff {
name,
diff,
diff_path,
} => {
write!( write!(
f, f,
"Image did not match snapshot. Diff: {diff}, {diff_path:?}" "'{name}' Image did not match snapshot. Diff: {diff}, {diff_path:?}. {HOW_TO_UPDATE_SCREENSHOTS}"
) )
} }
Self::OpenSnapshot { path, err } => match err { Self::OpenSnapshot { path, err } => match err {
ImageError::IoError(io) => match io.kind() { ImageError::IoError(io) => match io.kind() {
ErrorKind::NotFound => { ErrorKind::NotFound => {
write!(f, "Missing snapshot: {path:?}") write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
} }
err => { err => {
write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}") write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
} }
}, },
err => { err => {
write!(f, "Error decoding snapshot: {err:?}\nAt: {path:?}") 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 { expected, actual } => { Self::SizeMismatch {
name,
expected,
actual,
} => {
write!( write!(
f, f,
"Image size did not match snapshot. Expected: {expected:?}, Actual: {actual:?}" "'{name}' Image size did not match snapshot. Expected: {expected:?}, Actual: {actual:?}. {HOW_TO_UPDATE_SCREENSHOTS}"
) )
} }
Self::WriteSnapshot { path, err } => { Self::WriteSnapshot { path, err } => {
@@ -194,6 +211,7 @@ pub fn try_image_snapshot_options(
if previous.dimensions() != current.dimensions() { if previous.dimensions() != current.dimensions() {
maybe_update_snapshot(&path, current)?; maybe_update_snapshot(&path, current)?;
return Err(SnapshotError::SizeMismatch { return Err(SnapshotError::SizeMismatch {
name: name.to_owned(),
expected: previous.dimensions(), expected: previous.dimensions(),
actual: current.dimensions(), actual: current.dimensions(),
}); });
@@ -217,13 +235,16 @@ pub fn try_image_snapshot_options(
err, err,
})?; })?;
maybe_update_snapshot(&path, current)?; maybe_update_snapshot(&path, current)?;
return Err(SnapshotError::Diff { diff, diff_path }); Err(SnapshotError::Diff {
name: name.to_owned(),
diff,
diff_path,
})
} else { } else {
// Delete old diff if it exists // Delete old diff if it exists
std::fs::remove_file(diff_path).ok(); std::fs::remove_file(diff_path).ok();
Ok(())
} }
Ok(())
} }
/// Image snapshot test. /// Image snapshot test.

View File

@@ -18,8 +18,8 @@ macro_rules! impl_numeric_float {
($t: ident) => { ($t: ident) => {
impl Numeric for $t { impl Numeric for $t {
const INTEGRAL: bool = false; const INTEGRAL: bool = false;
const MIN: Self = std::$t::MIN; const MIN: Self = $t::MIN;
const MAX: Self = std::$t::MAX; const MAX: Self = $t::MAX;
#[inline(always)] #[inline(always)]
fn to_f64(self) -> f64 { fn to_f64(self) -> f64 {
@@ -44,8 +44,8 @@ macro_rules! impl_numeric_integer {
($t: ident) => { ($t: ident) => {
impl Numeric for $t { impl Numeric for $t {
const INTEGRAL: bool = true; const INTEGRAL: bool = true;
const MIN: Self = std::$t::MIN; const MIN: Self = $t::MIN;
const MAX: Self = std::$t::MAX; const MAX: Self = $t::MAX;
#[inline(always)] #[inline(always)]
fn to_f64(self) -> f64 { fn to_f64(self) -> f64 {

View File

@@ -1,4 +1,3 @@
use std::f32::INFINITY;
use std::fmt; use std::fmt;
use crate::{lerp, pos2, vec2, Div, Mul, Pos2, Rangef, Rot2, Vec2}; use crate::{lerp, pos2, vec2, Div, Mul, Pos2, Rangef, Rot2, Vec2};
@@ -33,8 +32,8 @@ pub struct Rect {
impl Rect { impl Rect {
/// Infinite rectangle that contains every point. /// Infinite rectangle that contains every point.
pub const EVERYTHING: Self = Self { pub const EVERYTHING: Self = Self {
min: pos2(-INFINITY, -INFINITY), min: pos2(-f32::INFINITY, -f32::INFINITY),
max: pos2(INFINITY, INFINITY), max: pos2(f32::INFINITY, f32::INFINITY),
}; };
/// The inverse of [`Self::EVERYTHING`]: stretches from positive infinity to negative infinity. /// The inverse of [`Self::EVERYTHING`]: stretches from positive infinity to negative infinity.
@@ -53,8 +52,8 @@ impl Rect {
/// assert_eq!(rect, Rect::from_min_max(pos2(0.0, 1.0), pos2(2.0, 3.0))) /// assert_eq!(rect, Rect::from_min_max(pos2(0.0, 1.0), pos2(2.0, 3.0)))
/// ``` /// ```
pub const NOTHING: Self = Self { pub const NOTHING: Self = Self {
min: pos2(INFINITY, INFINITY), min: pos2(f32::INFINITY, f32::INFINITY),
max: pos2(-INFINITY, -INFINITY), max: pos2(-f32::INFINITY, -f32::INFINITY),
}; };
/// An invalid [`Rect`] filled with [`f32::NAN`]. /// An invalid [`Rect`] filled with [`f32::NAN`].
@@ -650,6 +649,8 @@ impl Rect {
/// ///
/// A ray that starts inside the rect will return `true`. /// A ray that starts inside the rect will return `true`.
pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool { pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool {
debug_assert!(d.is_normalized(), "expected normalized direction");
let mut tmin = -f32::INFINITY; let mut tmin = -f32::INFINITY;
let mut tmax = f32::INFINITY; let mut tmax = f32::INFINITY;
@@ -671,6 +672,32 @@ impl Rect {
0.0 <= tmax && tmin <= tmax 0.0 <= tmax && tmin <= tmax
} }
/// Where does a ray from the center intersect the rectangle?
///
/// `d` is the direction of the ray and assumed to be normalized.
pub fn intersects_ray_from_center(&self, d: Vec2) -> Pos2 {
debug_assert!(d.is_normalized(), "expected normalized direction");
let mut tmin = f32::NEG_INFINITY;
let mut tmax = f32::INFINITY;
for i in 0..2 {
let inv_d = 1.0 / -d[i];
let mut t0 = (self.min[i] - self.center()[i]) * inv_d;
let mut t1 = (self.max[i] - self.center()[i]) * inv_d;
if inv_d < 0.0 {
std::mem::swap(&mut t0, &mut t1);
}
tmin = tmin.max(t0);
tmax = tmax.min(t1);
}
let t = tmax.min(tmin);
self.center() + t * -d
}
} }
impl fmt::Debug for Rect { impl fmt::Debug for Rect {
@@ -793,4 +820,57 @@ mod tests {
println!("Leftward ray from right:"); println!("Leftward ray from right:");
assert!(rect.intersects_ray(pos2(4.0, 2.0), Vec2::LEFT)); assert!(rect.intersects_ray(pos2(4.0, 2.0), Vec2::LEFT));
} }
#[test]
fn test_ray_from_center_intersection() {
let rect = Rect::from_min_max(pos2(1.0, 1.0), pos2(3.0, 3.0));
assert_eq!(
rect.intersects_ray_from_center(Vec2::RIGHT),
pos2(3.0, 2.0),
"rightward ray"
);
assert_eq!(
rect.intersects_ray_from_center(Vec2::UP),
pos2(2.0, 1.0),
"upward ray"
);
assert_eq!(
rect.intersects_ray_from_center(Vec2::LEFT),
pos2(1.0, 2.0),
"leftward ray"
);
assert_eq!(
rect.intersects_ray_from_center(Vec2::DOWN),
pos2(2.0, 3.0),
"downward ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::LEFT + Vec2::DOWN).normalized()),
pos2(1.0, 3.0),
"bottom-left corner ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::LEFT + Vec2::UP).normalized()),
pos2(1.0, 1.0),
"top-left corner ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::DOWN).normalized()),
pos2(3.0, 3.0),
"bottom-right corner ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::UP).normalized()),
pos2(3.0, 1.0),
"top-right corner ray"
);
}
} }

View File

@@ -138,7 +138,9 @@ fn test_aim() {
assert_eq!(best_in_range_f64(99.999, 100.000), 100.0); assert_eq!(best_in_range_f64(99.999, 100.000), 100.0);
assert_eq!(best_in_range_f64(10.001, 100.001), 100.0); assert_eq!(best_in_range_f64(10.001, 100.001), 100.0);
use std::f64::{INFINITY, NAN, NEG_INFINITY}; const NAN: f64 = f64::NAN;
const INFINITY: f64 = f64::INFINITY;
const NEG_INFINITY: f64 = f64::NEG_INFINITY;
assert!(best_in_range_f64(NAN, NAN).is_nan()); assert!(best_in_range_f64(NAN, NAN).is_nan());
assert_eq!(best_in_range_f64(NAN, 1.2), 1.2); assert_eq!(best_in_range_f64(NAN, 1.2), 1.2);
assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY); assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY);

View File

@@ -176,6 +176,12 @@ impl Vec2 {
} }
} }
/// Checks if `self` has length `1.0` up to a precision of `1e-6`.
#[inline(always)]
pub fn is_normalized(self) -> bool {
(self.length_sq() - 1.0).abs() < 2e-6
}
/// Rotates the vector by 90°, i.e positive X to positive Y /// Rotates the vector by 90°, i.e positive X to positive Y
/// (clockwise in egui coordinates). /// (clockwise in egui coordinates).
#[inline(always)] #[inline(always)]
@@ -497,8 +503,10 @@ impl fmt::Display for Vec2 {
} }
} }
#[test] #[cfg(test)]
fn test_vec2() { mod test {
use super::*;
macro_rules! almost_eq { macro_rules! almost_eq {
($left: expr, $right: expr) => { ($left: expr, $right: expr) => {
let left = $left; let left = $left;
@@ -506,32 +514,58 @@ fn test_vec2() {
assert!((left - right).abs() < 1e-6, "{} != {}", left, right); assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
}; };
} }
use std::f32::consts::TAU;
assert_eq!(Vec2::ZERO.angle(), 0.0); #[test]
assert_eq!(Vec2::angled(0.0).angle(), 0.0); fn test_vec2() {
assert_eq!(Vec2::angled(1.0).angle(), 1.0); use std::f32::consts::TAU;
assert_eq!(Vec2::X.angle(), 0.0);
assert_eq!(Vec2::Y.angle(), 0.25 * TAU);
assert_eq!(Vec2::RIGHT.angle(), 0.0); assert_eq!(Vec2::ZERO.angle(), 0.0);
assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU); assert_eq!(Vec2::angled(0.0).angle(), 0.0);
almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); assert_eq!(Vec2::angled(1.0).angle(), 1.0);
assert_eq!(Vec2::UP.angle(), -0.25 * TAU); assert_eq!(Vec2::X.angle(), 0.0);
assert_eq!(Vec2::Y.angle(), 0.25 * TAU);
let mut assignment = vec2(1.0, 2.0); assert_eq!(Vec2::RIGHT.angle(), 0.0);
assignment += vec2(3.0, 4.0); assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU);
assert_eq!(assignment, vec2(4.0, 6.0)); almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU);
assert_eq!(Vec2::UP.angle(), -0.25 * TAU);
let mut assignment = vec2(4.0, 6.0); let mut assignment = vec2(1.0, 2.0);
assignment -= vec2(1.0, 2.0); assignment += vec2(3.0, 4.0);
assert_eq!(assignment, vec2(3.0, 4.0)); assert_eq!(assignment, vec2(4.0, 6.0));
let mut assignment = vec2(1.0, 2.0); let mut assignment = vec2(4.0, 6.0);
assignment *= 2.0; assignment -= vec2(1.0, 2.0);
assert_eq!(assignment, vec2(2.0, 4.0)); assert_eq!(assignment, vec2(3.0, 4.0));
let mut assignment = vec2(2.0, 4.0); let mut assignment = vec2(1.0, 2.0);
assignment /= 2.0; assignment *= 2.0;
assert_eq!(assignment, vec2(1.0, 2.0)); assert_eq!(assignment, vec2(2.0, 4.0));
let mut assignment = vec2(2.0, 4.0);
assignment /= 2.0;
assert_eq!(assignment, vec2(1.0, 2.0));
}
#[test]
fn test_vec2_normalized() {
fn generate_spiral(n: usize, start: Vec2, end: Vec2) -> impl Iterator<Item = Vec2> {
let angle_step = 2.0 * std::f32::consts::PI / n as f32;
let radius_step = (end.length() - start.length()) / n as f32;
(0..n).map(move |i| {
let angle = i as f32 * angle_step;
let radius = start.length() + i as f32 * radius_step;
let x = radius * angle.cos();
let y = radius * angle.sin();
vec2(x, y)
})
}
for v in generate_spiral(40, Vec2::splat(0.1), Vec2::splat(2.0)) {
let vn = v.normalized();
almost_eq!(vn.length(), 1.0);
assert!(vn.is_normalized());
}
}
} }

View File

@@ -75,7 +75,7 @@ mod mutex_impl {
// Detect if we are recursively taking out a lock on this mutex. // Detect if we are recursively taking out a lock on this mutex.
// use a pointer to the inner data as an id for this lock // use a pointer to the inner data as an id for this lock
let ptr = (&self.0 as *const parking_lot::Mutex<_>).cast::<()>(); let ptr = std::ptr::from_ref::<parking_lot::Mutex<_>>(&self.0).cast::<()>();
// Store it in thread local storage while we have a lock guard taken out // Store it in thread local storage while we have a lock guard taken out
HELD_LOCKS_TLS.with(|held_locks| { HELD_LOCKS_TLS.with(|held_locks| {

View File

@@ -648,10 +648,10 @@ pub struct Glyph {
/// The row/line height of this font. /// The row/line height of this font.
pub font_height: f32, pub font_height: f32,
/// The ascent of the sub-font within the font ("FontImpl"). /// The ascent of the sub-font within the font (`FontImpl`).
pub font_impl_ascent: f32, pub font_impl_ascent: f32,
/// The row/line height of the sub-font within the font ("FontImpl"). /// The row/line height of the sub-font within the font (`FontImpl`).
pub font_impl_height: f32, pub font_impl_height: f32,
/// Position and size of the glyph in the font texture, in texels. /// Position and size of the glyph in the font texture, in texels.

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["tami5 <kkharji@proton.me>"] authors = ["tami5 <kkharji@proton.me>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Varphone Wong <varphone@qq.com>"] authors = ["Varphone Wong <varphone@qq.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -3,7 +3,7 @@ name = "custom_style"
version = "0.1.0" version = "0.1.0"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Maxim Osipenko <maxim1999max@gmail.com>"] authors = ["Maxim Osipenko <maxim1999max@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]
@@ -29,6 +29,4 @@ env_logger = { version = "0.10", default-features = false, features = [
] } ] }
# This is normally enabled by eframe/default, which is not being used here # This is normally enabled by eframe/default, which is not being used here
# because of accesskit, as mentioned above # because of accesskit, as mentioned above
winit = { workspace = true, features = [ winit = { workspace = true, features = ["default"] }
"default"
] }

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Jan Procházka <github.com/jprochazk>"] authors = ["Jan Procházka <github.com/jprochazk>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Jose Palazon <jose@palako.com>"] authors = ["Jose Palazon <jose@palako.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -7,7 +7,7 @@ authors = [
] ]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -45,7 +45,7 @@ impl eframe::App for MyApp {
if ui.button("save to 'top_left.png'").clicked() { if ui.button("save to 'top_left.png'").clicked() {
self.save_to_file = true; self.save_to_file = true;
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(Default::default()));
} }
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
@@ -58,9 +58,13 @@ impl eframe::App for MyApp {
} else { } else {
ctx.set_theme(egui::Theme::Light); ctx.set_theme(egui::Theme::Light);
}; };
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); ctx.send_viewport_cmd(
egui::ViewportCommand::Screenshot(Default::default()),
);
} else if ui.button("take screenshot!").clicked() { } else if ui.button("take screenshot!").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); ctx.send_viewport_cmd(
egui::ViewportCommand::Screenshot(Default::default()),
);
} }
}); });
}); });

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["TicClick <ya@ticclick.ch>"] authors = ["TicClick <ya@ticclick.ch>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -5,6 +5,6 @@
# to the user in the error, instead of "error: invalid channel name '[toolchain]'". # to the user in the error, instead of "error: invalid channel name '[toolchain]'".
[toolchain] [toolchain]
channel = "1.77.0" channel = "1.79.0"
components = ["rustfmt", "clippy"] components = ["rustfmt", "clippy"]
targets = ["wasm32-unknown-unknown"] targets = ["wasm32-unknown-unknown"]

View File

@@ -9,7 +9,7 @@ set -x
# Checks all tests, lints etc. # Checks all tests, lints etc.
# Basically does what the CI does. # Basically does what the CI does.
cargo +1.77.0 install --quiet typos-cli cargo +1.79.0 install --quiet typos-cli
export RUSTFLAGS="-D warnings" export RUSTFLAGS="-D warnings"
export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454

View File

@@ -6,7 +6,7 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Section identical to the root clippy.toml: # Section identical to the root clippy.toml:
msrv = "1.77" msrv = "1.79"
allow-unwrap-in-tests = true allow-unwrap-in-tests = true
@@ -47,6 +47,9 @@ doc-valid-idents = [
# You must also update the same list in the root `clippy.toml`! # You must also update the same list in the root `clippy.toml`!
"AccessKit", "AccessKit",
"WebGL", "WebGL",
"WebGL1",
"WebGL2",
"WebGPU", "WebGPU",
"VirtualBox",
"..", "..",
] ]

View File

@@ -3,14 +3,17 @@ name = "test_egui_extras_compilation"
version = "0.1.0" version = "0.1.0"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]
workspace = true workspace = true
[package.metadata.cargo-machete] [package.metadata.cargo-machete]
ignored = ["eframe", "egui_extras"] # We don't use them, just check that things compile ignored = [
"eframe",
"egui_extras",
] # We don't use them, just check that things compile
[dependencies] [dependencies]
eframe = { workspace = true, features = ["default", "persistence"] } eframe = { workspace = true, features = ["default", "persistence"] }

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Antoine Beyeler <abeyeler@gmail.com>"] authors = ["Antoine Beyeler <abeyeler@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["konkitoman"] authors = ["konkitoman"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.77" rust-version = "1.79"
publish = false publish = false
[lints] [lints]