From 10791cc43da0cb42f913fad7c3980c696cdd5418 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 28 Nov 2024 16:52:05 +0100 Subject: [PATCH 01/19] Add `Modal` and `Memory::set_modal_layer` (#5358) * Closes #686 * Closes #839 * #5370 should be merged before this * [x] I have followed the instructions in the PR template This adds modals to egui. This PR - adds a new `Modal` struct - adds `Memory::set_modal_layer` to limit focus to a layer and above (used by the modal struct, but could also be used by custom modal implementations) - adds `Memory::allows_interaction` to check if a layer is behind a modal layer, deprecating `Layer::allows_interaction` Current problems: - ~When a button is focused before the modal opens, it stays focused and you also can't hit tab to focus the next widget. Seems like focus is "stuck" on that widget until you hit escape. This might be related to https://github.com/emilk/egui/issues/5359~ fixed! Possible future improvements: - The titlebar from `window` should be made into a separate widget and added to the modal - The state whether the modal is open should be stored in egui (optionally), similar to popup and menu. Ideally before this we would refactor popup state to unify popup and menu --------- Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 4 +- crates/egui/src/containers/mod.rs | 2 + crates/egui/src/containers/modal.rs | 160 ++++++++++ crates/egui/src/context.rs | 5 +- crates/egui/src/layers.rs | 1 + crates/egui/src/memory/mod.rs | 106 ++++++- crates/egui/src/widgets/drag_value.rs | 2 +- .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/mod.rs | 1 + crates/egui_demo_lib/src/demo/modals.rs | 287 ++++++++++++++++++ .../tests/snapshots/demos/Modals.png | 3 + .../tests/snapshots/modals_1.png | 3 + .../tests/snapshots/modals_2.png | 3 + .../tests/snapshots/modals_3.png | 3 + ...rop_should_prevent_focusing_lower_area.png | 3 + 15 files changed, 574 insertions(+), 10 deletions(-) create mode 100644 crates/egui/src/containers/modal.rs create mode 100644 crates/egui_demo_lib/src/demo/modals.rs create mode 100644 crates/egui_demo_lib/tests/snapshots/demos/Modals.png create mode 100644 crates/egui_demo_lib/tests/snapshots/modals_1.png create mode 100644 crates/egui_demo_lib/tests/snapshots/modals_2.png create mode 100644 crates/egui_demo_lib/tests/snapshots/modals_3.png create mode 100644 crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png diff --git a/Cargo.lock b/Cargo.lock index ac6b32fc795..0e44fd28ba1 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", @@ -2274,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] 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..25f3fbce7ca --- /dev/null +++ b/crates/egui/src/containers/modal.rs @@ -0,0 +1,160 @@ +use crate::{ + Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, +}; +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) + .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; + let escape_clicked = ctx.input(|i| i.key_pressed(crate::Key::Escape)); + self.backdrop_response.clicked() + || (self.is_top_modal && !self.any_popup_open && escape_clicked) + } +} diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 554ea872261..cffc37c3293 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); } }); 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/memory/mod.rs b/crates/egui/src/memory/mod.rs index 75183bdd15b..e5ca8d42789 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -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; } @@ -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/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_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 7b3f263831b..8729d148f11 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(), 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/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/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 From c86d0e59185d2796de4c72081f9431a7de12436a Mon Sep 17 00:00:00 2001 From: Valentin Date: Sat, 30 Nov 2024 12:56:23 +0100 Subject: [PATCH 02/19] fix accidental change of FallbackEgl to PreferEgl (#5408) I accidentally changed this in a previous commit when I meant to only change the comment above it. https://github.com/emilk/egui/pull/5392#discussion_r1859383653 --- crates/eframe/src/native/glow_integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index db721a64697..2bd80cecbc3 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -943,7 +943,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, From 7e3275ca5caf27b1340b2f18e636041df387908e Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Sun, 1 Dec 2024 18:57:41 +0100 Subject: [PATCH 03/19] Fix cargo machete (#5410) * [x] I have followed the instructions in the PR template cargo machete depends on cargo-platform which seems to bumped it's msrv. Installing via --locked should fix this for now. I think it's fine to do this manually instead of using the cargo action since it's so simple and the action we used basically did the same (without --locked) --- .github/workflows/cargo_machete.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 328422dc62f1396cce550780279efdb341152025 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 1 Dec 2024 18:58:35 +0100 Subject: [PATCH 04/19] Update MSRV to Rust 1.79 (#5421) Mostly to fix `cargo-machete` CI --- .github/workflows/deploy_web_demo.yml | 2 +- .github/workflows/rust.yml | 14 ++++---- Cargo.toml | 16 +++++++-- clippy.toml | 7 ++-- crates/eframe/src/epi.rs | 2 +- .../eframe/src/native/event_loop_context.rs | 2 +- crates/eframe/src/web/web_painter_wgpu.rs | 34 ++----------------- crates/eframe/src/web/web_runner.rs | 2 +- crates/egui-wgpu/src/renderer.rs | 2 +- crates/egui/src/containers/scroll_area.rs | 2 +- crates/egui/src/data/key.rs | 2 +- crates/egui/src/input_state/mod.rs | 6 ++-- crates/egui/src/layout.rs | 2 +- crates/egui/src/lib.rs | 2 +- crates/egui/src/memory/mod.rs | 2 +- crates/egui/src/style.rs | 2 +- crates/egui/src/widgets/slider.rs | 2 +- crates/egui_demo_lib/src/demo/sliders.rs | 3 +- .../src/easy_mark/easy_mark_parser.rs | 2 +- crates/emath/src/numeric.rs | 8 ++--- crates/emath/src/rect.rs | 9 +++-- crates/emath/src/smart_aim.rs | 4 ++- crates/epaint/src/mutex.rs | 2 +- crates/epaint/src/text/text_layout_types.rs | 4 +-- examples/confirm_exit/Cargo.toml | 2 +- examples/custom_3d_glow/Cargo.toml | 2 +- examples/custom_font/Cargo.toml | 2 +- examples/custom_font_style/Cargo.toml | 2 +- examples/custom_keypad/Cargo.toml | 2 +- examples/custom_style/Cargo.toml | 2 +- examples/custom_window_frame/Cargo.toml | 2 +- examples/file_dialog/Cargo.toml | 2 +- examples/hello_world/Cargo.toml | 2 +- examples/hello_world_par/Cargo.toml | 6 ++-- examples/hello_world_simple/Cargo.toml | 2 +- examples/images/Cargo.toml | 2 +- examples/keyboard_events/Cargo.toml | 2 +- examples/multiple_viewports/Cargo.toml | 2 +- examples/puffin_profiler/Cargo.toml | 2 +- examples/screenshot/Cargo.toml | 2 +- examples/serial_windows/Cargo.toml | 2 +- examples/user_attention/Cargo.toml | 2 +- rust-toolchain | 2 +- scripts/check.sh | 2 +- scripts/clippy_wasm/clippy.toml | 5 ++- tests/test_egui_extras_compilation/Cargo.toml | 7 ++-- tests/test_inline_glow_paint/Cargo.toml | 2 +- tests/test_size_pass/Cargo.toml | 2 +- tests/test_ui_stack/Cargo.toml | 2 +- tests/test_viewports/Cargo.toml | 2 +- 50 files changed, 94 insertions(+), 103 deletions(-) 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/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/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/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/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 3b3925c9dcd..8b8bd8ad524 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, 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/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/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..866d20fc64d 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). diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index e5ca8d42789..f49f1342e50 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -736,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 { 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/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_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/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..12770fa3e1c 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`]. 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/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 17826e6afb1..64dd827148c 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -623,10 +623,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/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] From 6833cf56e17d2581163c43eb36dc3ee5d758771b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 2 Dec 2024 09:20:59 +0100 Subject: [PATCH 05/19] Add new `Rect::intersects_ray_from_center` method (#5415) Title. * [x] I have followed the instructions in the PR template --- crates/emath/src/rect.rs | 81 +++++++++++++++++++++++++++++++++++++++ crates/emath/src/vec2.rs | 82 ++++++++++++++++++++++++++++------------ 2 files changed, 139 insertions(+), 24 deletions(-) diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 12770fa3e1c..8b655bd722f 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -649,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; @@ -670,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 { @@ -792,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/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()); + } + } } From 4f7f23ef5eddd839654799d45ba377664131a7d1 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Mon, 2 Dec 2024 03:29:06 -0500 Subject: [PATCH 06/19] Fix cursor clipping in `TextEdit` inside a `ScrollArea` (#3660) * Closes #1531 ### Before Notice how the cursor hides after third enter and when the line is long. https://github.com/user-attachments/assets/8e45736e-d6c7-4dc6-94d0-213188c199ff ### After Cursor is always visible https://github.com/user-attachments/assets/43200683-3524-471b-990a-eb7b49385fa9 - `ScrollArea` now checks if there's a `scroll_target` in `begin`, if there is, it saves it because it's not from its children, then restore it in `end`. - `TextEdit` now allocates additional space if its galley grows during the frame. This is needed so that any surrounding `ScrollArea` can bring the cursor to view, otherwise the cursor lays outside the the `ScrollArea`'s `content_ui`. --- crates/egui/src/containers/scroll_area.rs | 22 +++++++++++++++++++- crates/egui/src/widgets/text_edit/builder.rs | 16 +++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 8b8bd8ad524..6065092cd51 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -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/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 5ce5749b97d..7c37c6a60cf 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::{ @@ -720,6 +721,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,10 +738,9 @@ 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 { From 6a1131f1c90409d7bc766d001b892b22a5e3322b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Dec 2024 09:55:25 +0100 Subject: [PATCH 07/19] Fix docstring backticks --- crates/egui/src/containers/scroll_area.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 6065092cd51..3c14a02e5e1 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -500,8 +500,8 @@ 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. + /// 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, From a9c76ba7a60d96293cf1f7ed57825a6b1c7e983e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Dec 2024 10:08:55 +0100 Subject: [PATCH 08/19] Allow attaching custom user data to a screenshot command (#5416) This lets users trigger a screenshot from anywhere, and then when they get back the results they have some context about what part of their code triggered the screenshot. --- crates/eframe/src/native/glow_integration.rs | 3 +- crates/eframe/src/native/wgpu_integration.rs | 45 ++++++++---- crates/egui-winit/src/lib.rs | 6 +- crates/egui/src/data/input.rs | 4 ++ crates/egui/src/data/mod.rs | 2 + crates/egui/src/data/user_data.rs | 74 ++++++++++++++++++++ crates/egui/src/lib.rs | 2 +- crates/egui/src/viewport.rs | 6 +- examples/screenshot/src/main.rs | 10 ++- 9 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 crates/egui/src/data/user_data.rs diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 2bd80cecbc3..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(), }); } 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/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/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/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/lib.rs b/crates/egui/src/lib.rs index 866d20fc64d..18b99c69a1a 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -471,7 +471,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/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/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()), + ); } }); }); From 8647b56b3142ae4a69fb56cc9ef838c583bcd923 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Dec 2024 10:33:10 +0100 Subject: [PATCH 09/19] Update snapshot for `Code Example` --- crates/egui_demo_lib/tests/snapshots/demos/Code Example.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 3411aba768dc87f74d9713ba76c36a836c72d307 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:46:37 +0100 Subject: [PATCH 10/19] Modals: Add `UiKind::Modal`, and consume escape-key properly (#5414) Small fixes/improvements to `Modal` - Fixes #5413 --- crates/egui/src/containers/modal.rs | 11 ++++++++--- crates/egui/src/ui_stack.rs | 10 ++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 25f3fbce7ca..521b9dedba4 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -1,5 +1,5 @@ use crate::{ - Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, + Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind, }; use emath::{Align2, Vec2}; @@ -32,6 +32,7 @@ impl Modal { /// - 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) @@ -153,8 +154,12 @@ impl ModalResponse { /// - 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; - let escape_clicked = ctx.input(|i| i.key_pressed(crate::Key::Escape)); + + // 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) + || (self.is_top_modal && !self.any_popup_open && escape_clicked()) } } 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 { From c7224aab261fa1808faf31631dcc93ff3a1ea2da Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Dec 2024 13:40:51 +0100 Subject: [PATCH 11/19] Improve error message when kittest fails (#5427) * Closes https://github.com/emilk/egui/issues/5423 New output is actionable ``` failures: ---- demo::demo_app_windows::tests::demos_should_match_snapshot stdout ---- thread 'demo::demo_app_windows::tests::demos_should_match_snapshot' panicked at crates/egui_demo_lib/src/demo/demo_app_windows.rs:433:9: Errors: [ "'demos/Code Example' Image size did not match snapshot. Expected: (402, 574), Actual: (415, 574). Run `UPDATE_SNAPSHOTS=1 cargo test` to update the snapshots.", ] note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: demo::demo_app_windows::tests::demos_should_match_snapshot ``` --- CODEOWNERS | 1 + .../src/demo/demo_app_windows.rs | 2 +- crates/egui_kittest/src/snapshot.rs | 41 ++++++++++++++----- 3 files changed, 33 insertions(+), 11 deletions(-) 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/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 8729d148f11..5b57c675b5a 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -426,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_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. From eac7ba01fa37bad35f71bc303561761952361b7c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 3 Dec 2024 14:28:12 +0100 Subject: [PATCH 12/19] Move `egui::util::cache` to `egui::cache`; add `FramePublisher` (#5426) This moves `egui::util::cache` to `egui::cache` (the old path is deprecated, but still works). It also adds the `FramePublisher` helper, which can be used to publish a value which will be retained for this frame and the next: ``` rs pub type MyPublisher = egui::cache::FramePublisher; // Publish: ctx.memory_mut(|mem| { mem.caches.cache::().set(key, value); }); // Retrieve: let value: Option = ctx.memory_mut(|mem| { mem.caches .cache::() .get(key) .clone() }) ``` --- crates/egui/src/cache/cache_storage.rs | 69 ++++++++++++++++ crates/egui/src/cache/cache_trait.rs | 11 +++ .../{util/cache.rs => cache/frame_cache.rs} | 80 +------------------ crates/egui/src/cache/frame_publisher.rs | 61 ++++++++++++++ crates/egui/src/cache/mod.rs | 21 +++++ crates/egui/src/lib.rs | 1 + crates/egui/src/memory/mod.rs | 4 +- crates/egui/src/util/mod.rs | 5 +- crates/egui_extras/src/syntax_highlighting.rs | 6 +- 9 files changed, 172 insertions(+), 86 deletions(-) create mode 100644 crates/egui/src/cache/cache_storage.rs create mode 100644 crates/egui/src/cache/cache_trait.rs rename crates/egui/src/{util/cache.rs => cache/frame_cache.rs} (51%) create mode 100644 crates/egui/src/cache/frame_publisher.rs create mode 100644 crates/egui/src/cache/mod.rs 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/lib.rs b/crates/egui/src/lib.rs index 18b99c69a1a..6d8c6a3459d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -393,6 +393,7 @@ #![allow(clippy::manual_range_contains)] mod animation_manager; +pub mod cache; pub mod containers; mod context; mod data; diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index f49f1342e50..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 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_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 From cd0f5859b26b783e554be2af0d34cb8ea8e539fb Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Wed, 4 Dec 2024 08:18:49 -0500 Subject: [PATCH 13/19] Make text cursor always appear on click (#5420) * [x] I have followed the instructions in the PR template ### Problem When clicking on a TextEdit sometimes the cursor doesn't appear immediately which makes it feel like the click was not registered for a second. This is because the start time for the blinking animation is only reset on keyboard input, but not on mouse interaction. It's hard to tell on the video but the cursor doesn't show immediately after clicking if the blink timer happens to be off. https://github.com/user-attachments/assets/9f049bd0-0375-4291-b2ef-697777fb854d ### Solution Reset the click timer every time a `TextEdit` is clicked. Additionally, the cursor is now correctly painted on the pixel boundary. IMO we should default to 1px cursor (instead of 2px) but that's not included in this PR. Happy to make that change too. https://github.com/user-attachments/assets/6c489414-f2c4-4dc6-85dd-f8bc457edad0 --- crates/egui/src/text_selection/visuals.rs | 19 +++++++++++++++---- crates/egui/src/widgets/text_edit/builder.rs | 6 ++++-- crates/egui/src/widgets/text_edit/state.rs | 4 ++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index d86f9dc56c2..2a31d1e1270 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -96,8 +96,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)); @@ -121,14 +132,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/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 7c37c6a60cf..012d8c8f0d5 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -603,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); } } } @@ -746,7 +748,7 @@ impl<'t> TextEdit<'t> { 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. @@ -759,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 { From c5ac7d301a90afbaec843ee04fb43d8a0956cc90 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 4 Dec 2024 14:23:05 +0100 Subject: [PATCH 14/19] Fix `on_hover_text_at_pointer` for transformed layers (#5429) --- crates/egui/src/containers/popup.rs | 12 +++++++++--- crates/egui_demo_lib/src/demo/pan_zoom.rs | 19 ++++++++++++++----- .../tests/snapshots/demos/Pan Zoom.png | 4 ++-- 3 files changed, 25 insertions(+), 10 deletions(-) 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_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/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 From 577ee8d22810752540636febac5660a5119c6550 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 4 Dec 2024 15:24:29 +0100 Subject: [PATCH 15/19] Add `Button::image_tint_follows_text_color` (#5430) For when you have a white icon/image that should respond to hover just like the text does. --- crates/egui/src/widgets/button.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 28a578ee694..bfcd656723c 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 the same as the 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 = 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()), From cf513d215cb9b0d021ff44036171480736f06198 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 4 Dec 2024 16:59:42 +0100 Subject: [PATCH 16/19] Update to wgpu 23.0.1 (#5432) Updating wgpu v23.0.0 -> v23.0.1 Updating wgpu-core v23.0.0 -> v23.0.1 Updating wgpu-hal v23.0.0 -> v23.0.1 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e44fd28ba1..a2e858d4b41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2274,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -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]] From f687b27efc590b9584bfa6dd0c2f22edaf8c8cef Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:35:24 +0100 Subject: [PATCH 17/19] Consume escape keystroke when bailing out from a drag operation (#5433) --- crates/egui/src/drag_and_drop.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) 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(); }); From 291b83b7bec77d8a4f2e3a724d70f462fc476dfd Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 5 Dec 2024 07:33:02 +0100 Subject: [PATCH 18/19] Support loading images with weird urls and improve error message (#5431) * Closes #5341 * [x] I have followed the instructions in the PR template --- crates/egui/src/context.rs | 10 ++++- crates/egui/src/load.rs | 28 +++++++++++-- crates/egui_extras/src/image.rs | 22 +++++++---- .../egui_extras/src/loaders/image_loader.rs | 39 +++++++++++-------- 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index cffc37c3293..aa449849e32 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -3455,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/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_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() } From 046034f9020453f1ffe3e96ff26c5404435fcfb5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 5 Dec 2024 13:53:20 +0100 Subject: [PATCH 19/19] Add `Color32::mul` (#5437) Multiply two `Color32` together quickly, in gamma-space --- crates/ecolor/src/color32.rs | 15 +++++++++++++++ crates/egui/src/widgets/button.rs | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) 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/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index bfcd656723c..e4355b49e8d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -158,7 +158,7 @@ impl<'a> Button<'a> { self } - /// If true, the tint of the image is the same as the text color. + /// 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. @@ -336,7 +336,7 @@ impl Widget for Button<'_> { 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 = visuals.text_color(); + image_options.tint = image_options.tint * visuals.text_color(); } widgets::image::paint_texture_load_result( ui,