diff --git a/.github/workflows/cargo_machete.yml b/.github/workflows/cargo_machete.yml index dab6725553c..2b06dfdef36 100644 --- a/.github/workflows/cargo_machete.yml +++ b/.github/workflows/cargo_machete.yml @@ -9,4 +9,4 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Machete - uses: bnjbvr/cargo-machete@main + run: cargo install cargo-machete --locked && cargo machete diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index 7be0b42f358..c5924a3b0f0 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -39,7 +39,7 @@ jobs: with: profile: minimal target: wasm32-unknown-unknown - toolchain: 1.77.0 + toolchain: 1.79.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 16df33e4720..e669fe4c543 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.77.0 + toolchain: 1.79.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.77.0 + toolchain: 1.79.0 targets: wasm32-unknown-unknown - 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: EmbarkStudios/cargo-deny-action@v1 with: - rust-version: "1.77.0" + rust-version: "1.79.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -170,7 +170,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.77.0 + toolchain: 1.79.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -189,7 +189,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.77.0 + toolchain: 1.79.0 targets: aarch64-apple-ios - name: Set up cargo cache @@ -208,7 +208,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.77.0 + toolchain: 1.79.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 @@ -232,7 +232,7 @@ jobs: lfs: true - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.77.0 + toolchain: 1.79.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/CODEOWNERS b/CODEOWNERS index 40b72a3140a..8b71b22c10e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,2 @@ +/crates/egui_kittest @lucasmerlin /crates/egui-wgpu @Wumpf diff --git a/Cargo.lock b/Cargo.lock index ac6b32fc795..a2e858d4b41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2239,7 +2239,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" 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 = [ "accesskit", "accesskit_consumer", @@ -4356,9 +4356,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wgpu" -version = "23.0.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ab52f2d3d18b70d5ab8dd270a1cff3ebe6dbe4a7d13c1cc2557138a9777fdc" +checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" dependencies = [ "arrayvec", "cfg_aliases 0.1.1", @@ -4381,9 +4381,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "23.0.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c68e7b6322a03ee5b83fcd92caeac5c2a932f6457818179f4652ad2a9c065" +checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" dependencies = [ "arrayvec", "bit-vec 0.8.0", @@ -4406,9 +4406,9 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "23.0.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6e7266b869de56c7e3ed72a954899f71d14fec6cc81c102b7530b92947601b" +checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" dependencies = [ "android_system_properties", "arrayvec", @@ -4478,7 +4478,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9fffbc80191..ab53c6cfbb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" -rust-version = "1.77" +rust-version = "1.79" version = "0.29.1" @@ -145,6 +145,7 @@ disallowed_types = "warn" # See clippy.toml doc_link_with_quotes = "warn" doc_markdown = "warn" empty_enum = "warn" +empty_enum_variants_with_brackets = "warn" enum_glob_use = "warn" equatable_if_let = "warn" exit = "warn" @@ -169,6 +170,8 @@ inefficient_to_string = "warn" infinite_loop = "warn" into_iter_without_iter = "warn" invalid_upcast_comparisons = "warn" +iter_filter_is_ok = "warn" +iter_filter_is_some = "warn" iter_not_returning_iterator = "warn" iter_on_empty_collections = "warn" iter_on_single_items = "warn" @@ -185,6 +188,7 @@ macro_use_imports = "warn" manual_assert = "warn" manual_clamp = "warn" manual_instant_elapsed = "warn" +manual_is_variant_and = "warn" manual_let_else = "warn" manual_ok_or = "warn" manual_string_new = "warn" @@ -202,6 +206,7 @@ mismatching_type_param_order = "warn" missing_enforced_import_renames = "warn" missing_errors_doc = "warn" missing_safety_doc = "warn" +mixed_attributes_style = "warn" mut_mut = "warn" mutex_integer = "warn" needless_borrow = "warn" @@ -211,21 +216,25 @@ needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" negative_feature_names = "warn" nonstandard_macro_braces = "warn" +option_as_ref_cloned = "warn" option_option = "warn" path_buf_push_overwrite = "warn" print_stderr = "warn" ptr_as_ptr = "warn" ptr_cast_constness = "warn" +pub_underscore_fields = "warn" pub_without_shorthand = "warn" rc_mutex = "warn" readonly_write_lock = "warn" redundant_type_annotations = "warn" +ref_as_ptr = "warn" ref_option_ref = "warn" ref_patterns = "warn" rest_pat_in_fully_bound_structs = "warn" same_functions_in_if_condition = "warn" semicolon_if_nothing_returned = "warn" single_match_else = "warn" +str_split_at_newline = "warn" str_to_string = "warn" string_add = "warn" string_add_assign = "warn" @@ -261,12 +270,15 @@ zero_sized_map_values = "warn" # TODO(emilk): enable more of these lints: iter_over_hash_type = "allow" -let_underscore_untyped = "allow" missing_assert_message = "allow" should_panic_without_expect = "allow" too_many_lines = "allow" 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 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 diff --git a/clippy.toml b/clippy.toml index 05e436ac419..cd4a25cab4f 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.77" +msrv = "1.79" 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 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", "WebGL", + "WebGL1", + "WebGL2", "WebGPU", + "VirtualBox", "..", ] diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 80e9e0778d3..c38a8d6b9f8 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -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), + ]) + } +} diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index f567507c4da..9f4f6dde8b1 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -439,7 +439,7 @@ pub struct WebOptions { /// Unused by webgl context as of writing. pub depth_buffer: u8, - /// Which version of WebGl context to select + /// Which version of WebGL context to select /// /// Default: [`WebGlContextOption::BestFirst`]. #[cfg(feature = "glow")] diff --git a/crates/eframe/src/native/event_loop_context.rs b/crates/eframe/src/native/event_loop_context.rs index 8f54681a869..f1262f85358 100644 --- a/crates/eframe/src/native/event_loop_context.rs +++ b/crates/eframe/src/native/event_loop_context.rs @@ -14,7 +14,7 @@ impl EventLoopGuard { cell.get().is_none(), "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::(event_loop))); }); Self } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index db721a64697..51c0cadce2a 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -661,13 +661,14 @@ impl<'app> GlowWinitRunning<'app> { { for action in viewport.actions_requested.drain() { match action { - ActionRequested::Screenshot => { + ActionRequested::Screenshot(user_data) => { let screenshot = painter.read_screen_rgba(screen_size_in_pixels); egui_winit .egui_input_mut() .events .push(egui::Event::Screenshot { viewport_id, + user_data, 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. // // 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( egui_ctx, event_loop, diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 997383f85c3..d13bed0bf41 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -643,10 +643,16 @@ impl<'app> WgpuWinitRunning<'app> { let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - let screenshot_requested = viewport - .actions_requested - .take(&ActionRequested::Screenshot) - .is_some(); + let mut screenshot_commands = vec![]; + viewport.actions_requested.retain(|cmd| { + if let ActionRequested::Screenshot(info) = cmd { + screenshot_commands.push(info.clone()); + false + } else { + true + } + }); + let screenshot_requested = !screenshot_commands.is_empty(); let (vsync_secs, screenshot) = painter.paint_and_update_textures( viewport_id, pixels_per_point, @@ -655,19 +661,32 @@ impl<'app> WgpuWinitRunning<'app> { &textures_delta, screenshot_requested, ); - if let Some(screenshot) = screenshot { - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - image: screenshot.into(), - }); + match (screenshot_requested, screenshot) { + (false, None) => {} + (true, Some(screenshot)) => { + let screenshot = Arc::new(screenshot); + for user_data in screenshot_commands { + egui_winit + .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() { match action { - ActionRequested::Screenshot => { + ActionRequested::Screenshot { .. } => { // already handled above } ActionRequested::Cut => { diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 2d5f5a4c3f0..ec487622e38 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -1,41 +1,13 @@ -use raw_window_handle::{ - DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, - RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle, -}; use std::sync::Arc; + use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use crate::WebOptions; use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; -use super::web_painter::WebPainter; - -struct EguiWebWindow(u32); - -#[allow(unsafe_code)] -impl HasWindowHandle for EguiWebWindow { - fn window_handle(&self) -> Result, HandleError> { - // SAFETY: there is no lifetime here. - unsafe { - Ok(WindowHandle::borrow_raw(RawWindowHandle::Web( - WebWindowHandle::new(self.0), - ))) - } - } -} +use crate::WebOptions; -#[allow(unsafe_code)] -impl HasDisplayHandle for EguiWebWindow { - fn display_handle(&self) -> Result, HandleError> { - // SAFETY: there is no lifetime here. - unsafe { - Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Web( - WebDisplayHandle::new(), - ))) - } - } -} +use super::web_painter::WebPainter; pub(crate) struct WebPainterWgpu { canvas: HtmlCanvasElement, diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 46efa09bb33..6cbc371f34c 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -16,7 +16,7 @@ pub struct WebRunner { /// Have we ever panicked? 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`]. runner: Rc>>, diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index a16b93781f6..b6d49d22d4f 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -125,7 +125,7 @@ pub struct ScreenDescriptor { /// Size of the window in physical pixels. pub size_in_pixels: [u32; 2], - /// HiDPI scale factor (pixels per point). + /// High-DPI scale factor (pixels per point). pub pixels_per_point: f32, } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index a78339d01fe..1cb2d502c5d 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1301,7 +1301,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option { - actions_requested.insert(ActionRequested::Screenshot); + ViewportCommand::Screenshot(user_data) => { + actions_requested.insert(ActionRequested::Screenshot(user_data)); } ViewportCommand::RequestCut => { actions_requested.insert(ActionRequested::Cut); diff --git a/crates/egui/src/cache/cache_storage.rs b/crates/egui/src/cache/cache_storage.rs new file mode 100644 index 00000000000..d4c3c9aef15 --- /dev/null +++ b/crates/egui/src/cache/cache_storage.rs @@ -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; +/// +/// # let mut cache_storage = CacheStorage::default(); +/// let mut cache = cache_storage.cache::>(); +/// assert_eq!(cache.get("hello"), 5); +/// ``` +#[derive(Default)] +pub struct CacheStorage { + caches: ahash::HashMap>, +} + +impl CacheStorage { + pub fn cache(&mut self) -> &mut Cache { + self.caches + .entry(std::any::TypeId::of::()) + .or_insert_with(|| Box::::default()) + .as_any_mut() + .downcast_mut::() + .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() + ) + } +} diff --git a/crates/egui/src/cache/cache_trait.rs b/crates/egui/src/cache/cache_trait.rs new file mode 100644 index 00000000000..73cb61f3865 --- /dev/null +++ b/crates/egui/src/cache/cache_trait.rs @@ -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; +} diff --git a/crates/egui/src/util/cache.rs b/crates/egui/src/cache/frame_cache.rs similarity index 51% rename from crates/egui/src/util/cache.rs rename to crates/egui/src/cache/frame_cache.rs index 14ec9a7b6a2..6c74c58dc3b 100644 --- a/crates/egui/src/util/cache.rs +++ b/crates/egui/src/cache/frame_cache.rs @@ -1,9 +1,4 @@ -//! 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. +use super::CacheTrait; /// Something that does an expensive computation that we want to cache /// to save us from recomputing it each frame. @@ -74,17 +69,6 @@ impl FrameCache { } } -#[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 CacheTrait for FrameCache { @@ -100,65 +84,3 @@ impl CacheTrait 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; -/// -/// # let mut cache_storage = CacheStorage::default(); -/// let mut cache = cache_storage.cache::>(); -/// assert_eq!(cache.get("hello"), 5); -/// ``` -#[derive(Default)] -pub struct CacheStorage { - caches: ahash::HashMap>, -} - -impl CacheStorage { - pub fn cache(&mut self) -> &mut FrameCache { - self.caches - .entry(std::any::TypeId::of::()) - .or_insert_with(|| Box::::default()) - .as_any_mut() - .downcast_mut::() - .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() - ) - } -} diff --git a/crates/egui/src/cache/frame_publisher.rs b/crates/egui/src/cache/frame_publisher.rs new file mode 100644 index 00000000000..0c2bc81d6c5 --- /dev/null +++ b/crates/egui/src/cache/frame_publisher.rs @@ -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 { + generation: u32, + cache: ahash::HashMap, +} + +impl Default for FramePublisher { + fn default() -> Self { + Self::new() + } +} + +impl FramePublisher { + 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 CacheTrait for FramePublisher +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 + } +} diff --git a/crates/egui/src/cache/mod.rs b/crates/egui/src/cache/mod.rs new file mode 100644 index 00000000000..68469ef3907 --- /dev/null +++ b/crates/egui/src/cache/mod.rs @@ -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; diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 3dd75a4458e..e68e0def1b0 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod area; pub mod collapsing_header; mod combo_box; pub mod frame; +pub mod modal; pub mod panel; pub mod popup; pub(crate) mod resize; @@ -18,6 +19,7 @@ pub use { collapsing_header::{CollapsingHeader, CollapsingResponse}, combo_box::*, frame::Frame, + modal::{Modal, ModalResponse}, panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs new file mode 100644 index 00000000000..521b9dedba4 --- /dev/null +++ b/crates/egui/src/containers/modal.rs @@ -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, +} + +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(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { + 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 { + /// 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 ModalResponse { + /// 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()) + } +} diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 56e3c44b150..a90e615aca7 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -87,17 +87,22 @@ pub fn show_tooltip_at_pointer( // Add a small exclusion zone around the pointer to avoid tooltips // 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: - 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( ctx, parent_layer, widget_id, allow_placing_below, - &exclusion_rect, + &pointer_rect, Box::new(add_contents), ) }) @@ -155,6 +160,7 @@ fn show_tooltip_at_dyn<'c, R>( widget_rect: &Rect, add_contents: Box R + 'c>, ) -> R { + // Transform layer coords to global coords: let mut widget_rect = *widget_rect; if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { widget_rect = transform * widget_rect; diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 3b3925c9dcd..3c14a02e5e1 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -39,7 +39,7 @@ pub struct State { scroll_start_offset_from_top_left: [Option; 2], /// 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. scroll_stuck_to_end: Vec2b, @@ -499,6 +499,11 @@ struct Prepared { scrolling_enabled: bool, 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; 2], + 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 { id, state, @@ -707,6 +716,7 @@ impl ScrollArea { viewport, scrolling_enabled, stick_to_end, + saved_scroll_target, animated, } } @@ -820,6 +830,7 @@ impl Prepared { viewport: _, scrolling_enabled, stick_to_end, + saved_scroll_target, animated, } = self; @@ -853,7 +864,7 @@ impl Prepared { let (start, end) = (range.min, range.max); let clip_start = clip_rect.min[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 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 = { // At this point this is the available size for the inner rect. let mut inner_size = inner_rect.size(); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 554ea872261..aa449849e32 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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)). #[allow(clippy::too_many_arguments)] 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 self.write(|ctx| { @@ -1172,7 +1173,7 @@ impl Context { viewport.this_pass.widgets.insert(w.layer_id, w); 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); } + let mut format = None; + // Try most recently added loaders first (hence `.rev()`) for loader in image_loaders.iter().rev() { match loader.load(self, uri, size_hint) { Err(load::LoadError::NotSupported) => continue, + Err(load::LoadError::FormatNotSupported { detected_format }) => { + format = format.or(detected_format); + continue; + } 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. diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index a1b9783280e..7987ea61225 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -529,6 +529,10 @@ pub enum Event { /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. Screenshot { viewport_id: crate::ViewportId, + + /// Whatever was passed to [`crate::ViewportCommand::Screenshot`]. + user_data: crate::UserData, + image: std::sync::Arc, }, } diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index c43d1c5685d..a075b025a2f 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -55,7 +55,7 @@ pub enum Key { // `]` CloseBracket, - /// \`, also known as "backquote" or "grave" + /// Also known as "backquote" or "grave" Backtick, /// `-` diff --git a/crates/egui/src/data/mod.rs b/crates/egui/src/data/mod.rs index bfe1e8a327d..f6f267dd06a 100644 --- a/crates/egui/src/data/mod.rs +++ b/crates/egui/src/data/mod.rs @@ -3,5 +3,7 @@ pub mod input; mod key; pub mod output; +mod user_data; pub use key::Key; +pub use user_data::UserData; diff --git a/crates/egui/src/data/user_data.rs b/crates/egui/src/data/user_data.rs new file mode 100644 index 00000000000..20bf5e1a123 --- /dev/null +++ b/crates/egui/src/data/user_data.rs @@ -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>, +} + +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(&self, state: &mut H) { + self.data.as_ref().map(Arc::as_ptr).hash(state); + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for UserData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_none() // can't serialize an `Any` + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for UserData { + fn deserialize(deserializer: D) -> Result + 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(self) -> Result + where + E: serde::de::Error, + { + Ok(UserData::default()) + } + } + + deserializer.deserialize_option(UserDataVisitor) + } +} diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 13da9d314c1..c58a5a48f64 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -23,22 +23,30 @@ pub struct DragAndDrop { impl DragAndDrop { 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 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; + fn begin_pass(ctx: &Context) { + let has_any_payload = Self::has_any_payload(ctx); - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); + 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 { - state.payload = None; + Self::clear_payload(ctx); } + } + } + + fn end_pass(ctx: &Context) { + let mut is_dragging = false; + ctx.data_mut(|data| { + let state = data.get_temp_mut_or_default::(Id::NULL); is_dragging = state.payload.is_some(); }); diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 7f743ee709a..99c166cf443 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -889,9 +889,9 @@ impl Default for PointerState { press_start_time: None, has_moved_too_much_for_a_click: false, started_decidedly_dragging: false, - last_click_time: std::f64::NEG_INFINITY, - last_last_click_time: std::f64::NEG_INFINITY, - last_move_time: std::f64::NEG_INFINITY, + last_click_time: f64::NEG_INFINITY, + last_last_click_time: f64::NEG_INFINITY, + last_move_time: f64::NEG_INFINITY, pointer_events: vec![], input_options: Default::default(), } diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 9105ece2591..b44daa41858 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -95,6 +95,7 @@ impl LayerId { } #[inline(always)] + #[deprecated = "Use `Memory::allows_interaction` instead"] pub fn allow_interaction(&self) -> bool { self.order.allow_interaction() } diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index 32fd0d03ac4..0c9bb494133 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -2,7 +2,7 @@ use crate::{ emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2}, Align, }; -use std::f32::INFINITY; +const INFINITY: f32 = f32::INFINITY; // ---------------------------------------------------------------------------- diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 79784c984a7..6d8c6a3459d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -3,7 +3,7 @@ //! Try the live web demo: . Read more about egui at . //! //! `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) //! which uses [`eframe`](https://docs.rs/eframe). @@ -393,6 +393,7 @@ #![allow(clippy::manual_range_contains)] mod animation_manager; +pub mod cache; pub mod containers; mod context; mod data; @@ -471,7 +472,7 @@ pub use self::{ output::{ self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo, }, - Key, + Key, UserData, }, drag_and_drop::DragAndDrop, epaint::text::TextWrapMode, diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index b6711de3c52..26950eb2add 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -77,16 +77,19 @@ pub enum LoadError { /// Programmer error: There are no image loaders installed. NoImageLoaders, - /// A specific loader does not support this scheme, protocol or image format. + /// A specific loader does not support this scheme or protocol. NotSupported, + /// A specific loader does not support the format of the image. + FormatNotSupported { detected_format: Option }, + /// Programmer error: Failed to find the bytes for this image because /// there was no [`BytesLoader`] supporting the scheme. NoMatchingBytesLoader, /// Programmer error: Failed to parse the bytes as an image because - /// there was no [`ImageLoader`] supporting the scheme. - NoMatchingImageLoader, + /// there was no [`ImageLoader`] supporting the format. + NoMatchingImageLoader { detected_format: Option }, /// Programmer error: no matching [`TextureLoader`]. /// Because of the [`DefaultTextureLoader`], this error should never happen. @@ -96,6 +99,20 @@ pub enum LoadError { 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::(), + } + } +} + impl Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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::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::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), } } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 75183bdd15b..5e94e6c0915 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -54,7 +54,7 @@ pub struct Memory { /// so as not to lock the UI thread. /// /// ``` - /// use egui::util::cache::{ComputerMut, FrameCache}; + /// use egui::cache::{ComputerMut, FrameCache}; /// /// #[derive(Default)] /// struct CharCounter {} @@ -72,7 +72,7 @@ pub struct Memory { /// }); /// ``` #[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 @@ -513,6 +513,12 @@ pub(crate) struct Focus { /// Set when looking for widget with navigational keys like arrows, tab, shift+tab. focus_direction: FocusDirection, + /// The top-most modal layer from the previous frame. + top_modal_layer: Option, + + /// The top-most modal layer from the current frame. + top_modal_layer_current_frame: Option, + /// A cache of widget IDs that are interested in focus with their corresponding rectangles. focus_widgets_cache: IdMap, } @@ -623,6 +629,8 @@ impl Focus { 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 { @@ -676,6 +684,14 @@ impl Focus { 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 { + self.top_modal_layer + } + fn reset_focus(&mut self) { self.focus_direction = FocusDirection::None; } @@ -720,7 +736,7 @@ impl Focus { let current_rect = self.focus_widgets_cache.get(¤t_focused.id)?; - let mut best_score = std::f32::INFINITY; + let mut best_score = f32::INFINITY; let mut best_id = None; for (candidate_id, candidate_rect) in &self.focus_widgets_cache { @@ -802,7 +818,15 @@ impl Memory { /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2) -> Option { - 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. @@ -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. /// This will allow the user to select it with tab and shift-tab. /// 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, /// as in the [`crate::DragValue`] widget, so a widget can be focused /// and rendered correctly in a single frame. + /// + /// Pass in the `layer_id` of the layer that the widget is in. #[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); } + /// 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 { + self.focus()?.top_modal_layer() + } + /// Stop editing the active [`TextEdit`](crate::TextEdit) (if any). #[inline(always)] 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; + /// 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`]. #[derive(Clone, Debug, Default)] @@ -1048,6 +1124,9 @@ pub struct Areas { /// Back-to-front, top is last. order: Vec, + /// Actual order of the layers, pre-calculated each frame. + order_map: OrderMap, + visible_last_frame: ahash::HashSet, visible_current_frame: ahash::HashSet, @@ -1079,12 +1158,28 @@ impl Areas { } /// For each layer, which [`Self::order`] is it in? - pub(crate) fn order_map(&self) -> HashMap { - self.order + pub(crate) fn order_map(&self) -> &OrderMap { + &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() .enumerate() .map(|(i, id)| (*id, i)) - .collect() + .collect(); } 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); } + self.calculate_order_map(); } } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 0f6c9633ebd..14b5aecd6ee 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -288,7 +288,7 @@ pub struct Style { /// If true and scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift 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, } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index b2c2e8d9ea7..14002c4c3f4 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -97,8 +97,19 @@ pub fn paint_text_selection( pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { let stroke = visuals.text_cursor.stroke; - let top = cursor_rect.center_top(); - let bottom = cursor_rect.center_bottom(); + // Ensure the cursor is aligned to the pixel grid for whole number widths. + // 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)); @@ -122,14 +133,14 @@ pub fn paint_text_cursor( ui: &Ui, painter: &Painter, primary_cursor_rect: Rect, - time_since_last_edit: f64, + time_since_last_interaction: f64, ) { if ui.visuals().text_cursor.blink { let on_duration = ui.visuals().text_cursor.on_duration; let off_duration = ui.visuals().text_cursor.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 { // Cursor is visible diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 7aae3f2fbeb..7aa131bec7a 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -24,6 +24,9 @@ pub enum UiKind { /// A bottom [`crate::TopBottomPanel`]. BottomPanel, + /// A modal [`crate::Modal`]. + Modal, + /// A [`crate::Frame`]. Frame, @@ -82,6 +85,7 @@ impl UiKind { Self::Window | Self::Menu + | Self::Modal | Self::Popup | Self::Tooltip | Self::Picker @@ -228,6 +232,12 @@ impl UiStack { 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()`]? #[inline] pub fn is_root_ui(&self) -> bool { diff --git a/crates/egui/src/util/mod.rs b/crates/egui/src/util/mod.rs index 55e93eb0400..de62b961822 100644 --- a/crates/egui/src/util/mod.rs +++ b/crates/egui/src/util/mod.rs @@ -1,6 +1,5 @@ //! Miscellaneous tools used by the rest of egui. -pub mod cache; pub(crate) mod fixed_cache; pub mod id_type_map; pub mod undoer; @@ -9,3 +8,7 @@ pub use id_type_map::IdTypeMap; pub use epaint::emath::History; pub use epaint::util::{hash, hash_with}; + +/// Deprecated alias for [`crate::cache`]. +#[deprecated = "Use egui::cache instead"] +pub use crate::cache; diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 31cbc623e39..d8b26429c77 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -1058,8 +1058,8 @@ pub enum ViewportCommand { /// Take a screenshot. /// - /// The results are returned in `crate::Event::Screenshot`. - Screenshot, + /// The results are returned in [`crate::Event::Screenshot`]. + Screenshot(crate::UserData), /// Request cut of the current selection /// @@ -1100,6 +1100,8 @@ impl ViewportCommand { } } +// ---------------------------------------------------------------------------- + /// Describes a viewport, i.e. a native window. /// /// This is returned by [`crate::Context::run`] on each frame, and should be applied diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 28a578ee694..e4355b49e8d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -37,6 +37,7 @@ pub struct Button<'a> { min_size: Vec2, rounding: Option, selected: bool, + image_tint_follows_text_color: bool, } impl<'a> Button<'a> { @@ -70,6 +71,7 @@ impl<'a> Button<'a> { min_size: Vec2::ZERO, rounding: None, selected: false, + image_tint_follows_text_color: false, } } @@ -156,6 +158,18 @@ impl<'a> Button<'a> { 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. /// /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`). @@ -190,6 +204,7 @@ impl Widget for Button<'_> { min_size, rounding, selected, + image_tint_follows_text_color, } = self; 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); cursor_x += image_size.x; 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( ui, &tlr, image_rect, image.show_loading_spinner, - image.image_options(), + &image_options, ); response = widgets::image::texture_load_result_response( &image.source(ui.ctx()), diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index feec4d07ff5..a5b8c25b2f8 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -452,7 +452,7 @@ impl<'a> Widget for DragValue<'a> { // in button mode for just one frame. This is important for // screen readers. 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) }); diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 3c2bb973f88..7dc3a5cd568 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -1030,7 +1030,7 @@ impl<'a> Widget for Slider<'a> { // Logarithmic sliders are allowed to include zero and infinity, // 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), /// give a scale that this many orders of magnitude in size. diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 5ce5749b97d..012d8c8f0d5 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use emath::Rect; use epaint::text::{cursor::CCursor, Galley, LayoutJob}; use crate::{ @@ -602,6 +603,8 @@ impl<'t> TextEdit<'t> { if did_interact || response.clicked() { 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); if has_focus { @@ -727,16 +740,15 @@ impl<'t> TextEdit<'t> { let primary_cursor_rect = 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) && !is_fully_visible { + if response.changed || selection_changed { // 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 { let now = ui.ctx().input(|i| i.time); 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. @@ -749,7 +761,7 @@ impl<'t> TextEdit<'t> { ui, &painter, primary_cursor_rect, - now - state.last_edit_time, + now - state.last_interaction_time, ); } diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index e73664a1293..cbbb6071484 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -53,10 +53,10 @@ pub struct TextEditState { #[cfg_attr(feature = "serde", serde(skip))] 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. #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) last_edit_time: f64, + pub(crate) last_interaction_time: f64, } impl TextEditState { diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 7b3f263831b..5b57c675b5a 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -33,6 +33,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), @@ -425,7 +426,7 @@ mod tests { let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options); if let Err(err) = result { - errors.push(err); + errors.push(err.to_string()); } } diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 828bdd896a3..8c9034868e4 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -17,6 +17,7 @@ pub mod frame_demo; pub mod highlighting; pub mod interactive_container; pub mod misc_demo_window; +pub mod modals; pub mod multi_touch; pub mod paint_bezier; pub mod painting; diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs new file mode 100644 index 00000000000..989101b4d75 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -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, + + 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"); + } +} diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 6367946faef..f8411a740c0 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -73,20 +73,29 @@ impl crate::View for PanZoom { for (i, (pos, callback)) in [ ( egui::Pos2::new(0.0, 0.0), - Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) - as Box egui::Response>, + Box::new(|ui: &mut egui::Ui, _: &mut Self| { + ui.button("top left").on_hover_text("Normal tooltip") + }) as Box egui::Response>, ), ( 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), - 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), - 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), diff --git a/crates/egui_demo_lib/src/demo/sliders.rs b/crates/egui_demo_lib/src/demo/sliders.rs index d15d9aa3100..ef8bdb0cd11 100644 --- a/crates/egui_demo_lib/src/demo/sliders.rs +++ b/crates/egui_demo_lib/src/demo/sliders.rs @@ -1,5 +1,4 @@ use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui}; -use std::f64::INFINITY; /// Showcase sliders #[derive(PartialEq)] @@ -77,7 +76,7 @@ impl crate::View for Sliders { let (type_min, type_max) = if *integer { ((i32::MIN as f64), (i32::MAX as f64)) } else if *logarithmic { - (-INFINITY, INFINITY) + (-f64::INFINITY, f64::INFINITY) } else { (-1e5, 1e5) // linear sliders make little sense with huge numbers }; diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs index 75d8135dff5..ed3ebe7f90f 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs @@ -13,7 +13,7 @@ pub enum Item<'a> { // TODO(emilk): add Style here so empty heading still uses up the right amount of space. Newline, - /// + /// Text Text(Style, &'a str), /// title, url diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 093b2c6a33b..162fc51a1df 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8d4f004ee11ea68ae0f30657601b6e51403fcc3ca91fa5b8cdcb58585d8d40d -size 78318 +oid sha256:01aaa4ef1a167a94fa1e5163550aabe4fa5e9f3a012b26170fe3088a6ca32d94 +size 81064 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png new file mode 100644 index 00000000000..274b4b57686 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:026723cb5d89b32386a849328c34420ee9e3ae1f97cbf6fa3c4543141123549e +size 32890 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png index 7ba225feae8..384840b7101 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79ce1dbf7627579d4e10de6494e34d8fd9685506d7b35cb3c9148f90f8c01366 -size 25144 +oid sha256:ccfda16ef7cdf94f7fbbd2c0f8df6f6de7904969e2a66337920c32608a6f9f05 +size 25357 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png new file mode 100644 index 00000000000..461bef728bc --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8348ff582e11fdc9baf008b5434f81f8d77b834479cb3765c87d1f4fd695e30f +size 48212 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png new file mode 100644 index 00000000000..0f1273f41df --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23482b77cbd817c66421a630e409ac3d8c5d24de00aa91e476e8d42b607c24b1 +size 48104 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png new file mode 100644 index 00000000000..ba8cca6228b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d94aa33d72c32f6f1aafab92c9753dc07bc5224c701003ac7fe8a01ae8c701a +size 44011 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png new file mode 100644 index 00000000000..14d6fb9cbfc --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1a5d265470c36e64340ccceea4ade464b3c4a1177d60630b02ae8287934748f +size 44026 diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index f6301aec823..1d2f6afa480 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -28,7 +28,7 @@ pub struct RetainedImage { } impl RetainedImage { - pub fn from_color_image(debug_name: impl Into, image: ColorImage) -> Self { + pub fn from_color_image(debug_name: impl Into, image: egui::ColorImage) -> Self { Self { debug_name: debug_name.into(), size: image.size, @@ -54,7 +54,7 @@ impl RetainedImage { ) -> Result { Ok(Self::from_color_image( 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 .lock() .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); ctx.load_texture(&self.debug_name, image, self.options) }) @@ -190,8 +190,6 @@ impl RetainedImage { // ---------------------------------------------------------------------------- -use egui::ColorImage; - /// Load a (non-svg) image. /// /// Requires the "image" feature. You must also opt-in to the image formats you need @@ -200,9 +198,19 @@ use egui::ColorImage; /// # Errors /// On invalid image or unsupported image format. #[cfg(feature = "image")] -pub fn load_image_bytes(image_bytes: &[u8]) -> Result { +pub fn load_image_bytes(image_bytes: &[u8]) -> Result { 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 image_buffer = image.to_rgba8(); let pixels = image_buffer.as_flat_samples(); diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 8c2c497058a..088ef5e4587 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -7,7 +7,7 @@ use egui::{ use image::ImageFormat; use std::{mem::size_of, path::Path, sync::Arc}; -type Entry = Result, String>; +type Entry = Result, LoadError>; #[derive(Default)] pub struct ImageCrateLoader { @@ -31,9 +31,14 @@ fn is_supported_uri(uri: &str) -> bool { .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 - !ImageFormat::all() + ImageFormat::all() .filter(ImageFormat::reading_enabled) .map(|fmt| fmt.to_mime_type()) .any(|format_mime| mime == format_mime) @@ -46,12 +51,12 @@ impl ImageLoader for ImageCrateLoader { fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult { // 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` - // 3. image::guess_format + // 3. image::guess_format (used internally by image::load_from_memory) // (1) - if !is_supported_uri(uri) { + if uri.starts_with("file://") && !is_supported_uri(uri) { return Err(LoadError::NotSupported); } @@ -59,26 +64,26 @@ impl ImageLoader for ImageCrateLoader { if let Some(entry) = cache.get(uri).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Loading(err)), + Err(err) => Err(err), } } else { match ctx.try_load_bytes(uri) { Ok(BytesPoll::Ready { bytes, mime, .. }) => { - // (2 and 3) - if mime.as_deref().is_some_and(is_unsupported_mime) - || image::guess_format(&bytes).is_err() - { - return Err(LoadError::NotSupported); + // (2) + if let Some(mime) = mime { + if !is_supported_mime(&mime) { + return Err(LoadError::FormatNotSupported { + detected_format: Some(mime), + }); + } } + // (3) log::trace!("started loading {uri:?}"); let result = crate::image::load_image_bytes(&bytes).map(Arc::new); log::trace!("finished loading {uri:?}"); cache.insert(uri.into(), result.clone()); - match result { - Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Loading(err)), - } + result.map(|image| ImagePoll::Ready { image }) } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), Err(err) => Err(err), @@ -100,7 +105,7 @@ impl ImageLoader for ImageCrateLoader { .values() .map(|result| match result { Ok(image) => image.pixels.len() * size_of::(), - Err(err) => err.len(), + Err(err) => err.byte_size(), }) .sum() } diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 293fbde007f..9275d345b5b 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -33,9 +33,7 @@ pub fn highlight( // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available // (ui.ctx(), ui.style()) can be used - impl egui::util::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> - for Highlighter - { + impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute( &mut self, (font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str), @@ -44,7 +42,7 @@ pub fn highlight( } } - type HighlightCache = egui::util::cache::FrameCache; + type HighlightCache = egui::cache::FrameCache; let font_id = style .override_font_id diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 40e02027b4b..5be6c275419 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -53,6 +53,9 @@ impl SnapshotOptions { pub enum SnapshotError { /// Image did not match snapshot Diff { + /// Name of the test + name: String, + /// Count of pixels that were different diff: i32, @@ -72,6 +75,9 @@ pub enum SnapshotError { /// The size of the image did not match the snapshot SizeMismatch { + /// Name of the test + name: String, + /// Expected size 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Diff { diff, diff_path } => { + Self::Diff { + name, + diff, + diff_path, + } => { write!( 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 { ImageError::IoError(io) => match io.kind() { ErrorKind::NotFound => { - write!(f, "Missing snapshot: {path:?}") + write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}") } err => { - write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}") + write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}") } }, 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!( 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 } => { @@ -194,6 +211,7 @@ pub fn try_image_snapshot_options( if previous.dimensions() != current.dimensions() { maybe_update_snapshot(&path, current)?; return Err(SnapshotError::SizeMismatch { + name: name.to_owned(), expected: previous.dimensions(), actual: current.dimensions(), }); @@ -217,13 +235,16 @@ pub fn try_image_snapshot_options( err, })?; maybe_update_snapshot(&path, current)?; - return Err(SnapshotError::Diff { diff, diff_path }); + Err(SnapshotError::Diff { + name: name.to_owned(), + diff, + diff_path, + }) } else { // Delete old diff if it exists std::fs::remove_file(diff_path).ok(); + Ok(()) } - - Ok(()) } /// Image snapshot test. diff --git a/crates/emath/src/numeric.rs b/crates/emath/src/numeric.rs index 03d00077129..9a7814b23d2 100644 --- a/crates/emath/src/numeric.rs +++ b/crates/emath/src/numeric.rs @@ -18,8 +18,8 @@ macro_rules! impl_numeric_float { ($t: ident) => { impl Numeric for $t { const INTEGRAL: bool = false; - const MIN: Self = std::$t::MIN; - const MAX: Self = std::$t::MAX; + const MIN: Self = $t::MIN; + const MAX: Self = $t::MAX; #[inline(always)] fn to_f64(self) -> f64 { @@ -44,8 +44,8 @@ macro_rules! impl_numeric_integer { ($t: ident) => { impl Numeric for $t { const INTEGRAL: bool = true; - const MIN: Self = std::$t::MIN; - const MAX: Self = std::$t::MAX; + const MIN: Self = $t::MIN; + const MAX: Self = $t::MAX; #[inline(always)] fn to_f64(self) -> f64 { diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 6c0677ad55e..8b655bd722f 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -1,4 +1,3 @@ -use std::f32::INFINITY; use std::fmt; use crate::{lerp, pos2, vec2, Div, Mul, Pos2, Rangef, Rot2, Vec2}; @@ -33,8 +32,8 @@ pub struct Rect { impl Rect { /// Infinite rectangle that contains every point. pub const EVERYTHING: Self = Self { - min: pos2(-INFINITY, -INFINITY), - max: pos2(INFINITY, INFINITY), + min: pos2(-f32::INFINITY, -f32::INFINITY), + max: pos2(f32::INFINITY, f32::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))) /// ``` pub const NOTHING: Self = Self { - min: pos2(INFINITY, INFINITY), - max: pos2(-INFINITY, -INFINITY), + min: pos2(f32::INFINITY, f32::INFINITY), + max: pos2(-f32::INFINITY, -f32::INFINITY), }; /// An invalid [`Rect`] filled with [`f32::NAN`]. @@ -650,6 +649,8 @@ impl Rect { /// /// A ray that starts inside the rect will return `true`. 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 tmax = f32::INFINITY; @@ -671,6 +672,32 @@ impl Rect { 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 { @@ -793,4 +820,57 @@ mod tests { println!("Leftward ray from right:"); 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" + ); + } } diff --git a/crates/emath/src/smart_aim.rs b/crates/emath/src/smart_aim.rs index 88b807cf80b..72094706995 100644 --- a/crates/emath/src/smart_aim.rs +++ b/crates/emath/src/smart_aim.rs @@ -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(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_eq!(best_in_range_f64(NAN, 1.2), 1.2); assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY); diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 03e0715c20a..9a173348b03 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -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 /// (clockwise in egui coordinates). #[inline(always)] @@ -497,8 +503,10 @@ impl fmt::Display for Vec2 { } } -#[test] -fn test_vec2() { +#[cfg(test)] +mod test { + use super::*; + macro_rules! almost_eq { ($left: expr, $right: expr) => { let left = $left; @@ -506,32 +514,58 @@ fn test_vec2() { assert!((left - right).abs() < 1e-6, "{} != {}", left, right); }; } - use std::f32::consts::TAU; - assert_eq!(Vec2::ZERO.angle(), 0.0); - assert_eq!(Vec2::angled(0.0).angle(), 0.0); - assert_eq!(Vec2::angled(1.0).angle(), 1.0); - assert_eq!(Vec2::X.angle(), 0.0); - assert_eq!(Vec2::Y.angle(), 0.25 * TAU); + #[test] + fn test_vec2() { + use std::f32::consts::TAU; + + assert_eq!(Vec2::ZERO.angle(), 0.0); + assert_eq!(Vec2::angled(0.0).angle(), 0.0); + assert_eq!(Vec2::angled(1.0).angle(), 1.0); + 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::DOWN.angle(), 0.25 * TAU); + almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); + assert_eq!(Vec2::UP.angle(), -0.25 * TAU); - assert_eq!(Vec2::RIGHT.angle(), 0.0); - assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU); - almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); - assert_eq!(Vec2::UP.angle(), -0.25 * TAU); + let mut assignment = vec2(1.0, 2.0); + assignment += vec2(3.0, 4.0); + assert_eq!(assignment, vec2(4.0, 6.0)); - let mut assignment = vec2(1.0, 2.0); - assignment += vec2(3.0, 4.0); - assert_eq!(assignment, vec2(4.0, 6.0)); + let mut assignment = vec2(4.0, 6.0); + assignment -= vec2(1.0, 2.0); + assert_eq!(assignment, vec2(3.0, 4.0)); - let mut assignment = vec2(4.0, 6.0); - assignment -= vec2(1.0, 2.0); - assert_eq!(assignment, vec2(3.0, 4.0)); + let mut assignment = vec2(1.0, 2.0); + assignment *= 2.0; + assert_eq!(assignment, vec2(2.0, 4.0)); - let mut assignment = vec2(1.0, 2.0); - assignment *= 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 { + 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) + }) + } - let mut assignment = vec2(2.0, 4.0); - assignment /= 2.0; - assert_eq!(assignment, vec2(1.0, 2.0)); + 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()); + } + } } diff --git a/crates/epaint/src/mutex.rs b/crates/epaint/src/mutex.rs index 157701c2be0..bd984a1d0ec 100644 --- a/crates/epaint/src/mutex.rs +++ b/crates/epaint/src/mutex.rs @@ -75,7 +75,7 @@ mod mutex_impl { // 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 - let ptr = (&self.0 as *const parking_lot::Mutex<_>).cast::<()>(); + let ptr = std::ptr::from_ref::>(&self.0).cast::<()>(); // Store it in thread local storage while we have a lock guard taken out HELD_LOCKS_TLS.with(|held_locks| { diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index f90210ca136..70d040f7501 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -648,10 +648,10 @@ pub struct Glyph { /// The row/line height of this font. 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, - /// 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, /// Position and size of the glyph in the font texture, in texels. diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index 4f31cc6d7cc..d4a21060b76 100644 --- a/examples/confirm_exit/Cargo.toml +++ b/examples/confirm_exit/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index 7bbea3b07de..d1dcc056949 100644 --- a/examples/custom_3d_glow/Cargo.toml +++ b/examples/custom_3d_glow/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index d7cecf07312..ee769cc62d5 100644 --- a/examples/custom_font/Cargo.toml +++ b/examples/custom_font/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index 54d037d15ed..f25676e87a1 100644 --- a/examples/custom_font_style/Cargo.toml +++ b/examples/custom_font_style/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["tami5 "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml index 088e7a3fc41..dc3c62dddb8 100644 --- a/examples/custom_keypad/Cargo.toml +++ b/examples/custom_keypad/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Varphone Wong "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index cd4f4063bee..6299e1aee35 100644 --- a/examples/custom_style/Cargo.toml +++ b/examples/custom_style/Cargo.toml @@ -3,7 +3,7 @@ name = "custom_style" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index 5e4941e3df8..848189084ad 100644 --- a/examples/custom_window_frame/Cargo.toml +++ b/examples/custom_window_frame/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index 7adb8e1b8a9..dc58e0ba2e7 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index e5acf4d89bb..6e7dd8d00f1 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index 07cdd4858c1..d486e35791d 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Maxim Osipenko "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [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 # because of accesskit, as mentioned above -winit = { workspace = true, features = [ - "default" -] } +winit = { workspace = true, features = ["default"] } diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index 169504409f7..0d77c65e316 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index c564f4c1590..4759e2d128e 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jan Procházka "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index c2cd90ce575..e587764bacf 100644 --- a/examples/keyboard_events/Cargo.toml +++ b/examples/keyboard_events/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jose Palazon "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index 089d0840142..9910aed5a2d 100644 --- a/examples/multiple_viewports/Cargo.toml +++ b/examples/multiple_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index 8f200bd628d..d2cbce19052 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index cc4733594dd..2e74482a64e 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 88b57f20e7a..1dd0bbf5076 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -45,7 +45,7 @@ impl eframe::App for MyApp { if ui.button("save to 'top_left.png'").clicked() { 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| { @@ -58,9 +58,13 @@ impl eframe::App for MyApp { } else { 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() { - ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); + ctx.send_viewport_cmd( + egui::ViewportCommand::Screenshot(Default::default()), + ); } }); }); diff --git a/examples/serial_windows/Cargo.toml b/examples/serial_windows/Cargo.toml index 069d7184481..c377524c69c 100644 --- a/examples/serial_windows/Cargo.toml +++ b/examples/serial_windows/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index a9fd5aa0373..3fbc75e260e 100644 --- a/examples/user_attention/Cargo.toml +++ b/examples/user_attention/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["TicClick "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/rust-toolchain b/rust-toolchain index ed934c15761..9fdafb7a67a 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.77.0" +channel = "1.79.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/check.sh b/scripts/check.sh index 5f316d57a69..3129f2ac114 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,7 @@ set -x # Checks all tests, lints etc. # 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 RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index f0e91004a81..0f7fc92dcc7 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Section identical to the root clippy.toml: -msrv = "1.77" +msrv = "1.79" allow-unwrap-in-tests = true @@ -47,6 +47,9 @@ doc-valid-idents = [ # You must also update the same list in the root `clippy.toml`! "AccessKit", "WebGL", + "WebGL1", + "WebGL2", "WebGPU", + "VirtualBox", "..", ] diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index bccea8a3216..1a310566f2a 100644 --- a/tests/test_egui_extras_compilation/Cargo.toml +++ b/tests/test_egui_extras_compilation/Cargo.toml @@ -3,14 +3,17 @@ name = "test_egui_extras_compilation" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] workspace = true [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] eframe = { workspace = true, features = ["default", "persistence"] } diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index 975f777d72b..5ded3cc356b 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/tests/test_size_pass/Cargo.toml b/tests/test_size_pass/Cargo.toml index 21d645cef5e..d6ee661e6b6 100644 --- a/tests/test_size_pass/Cargo.toml +++ b/tests/test_size_pass/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/tests/test_ui_stack/Cargo.toml b/tests/test_ui_stack/Cargo.toml index e87845bc043..df2e2bf15c2 100644 --- a/tests/test_ui_stack/Cargo.toml +++ b/tests/test_ui_stack/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Antoine Beyeler "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints] diff --git a/tests/test_viewports/Cargo.toml b/tests/test_viewports/Cargo.toml index 6edac0bd5e1..cb962411558 100644 --- a/tests/test_viewports/Cargo.toml +++ b/tests/test_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["konkitoman"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.77" +rust-version = "1.79" publish = false [lints]