From a33ae64785f507e749969dd40f8d2382978bb867 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 20 Feb 2024 13:35:19 +0100 Subject: [PATCH 01/27] Fix: assign a different id to each table cell, avoiding id clashes (#4076) Each cell in a table now has a `Ui` with a unique `Id` based on the row and column. This avoids Id-clashes, e.g. when putting a `CollapsingHeader` in a table cell. --- crates/egui_extras/src/layout.rs | 10 +++++++--- crates/egui_extras/src/strip.rs | 8 +++++++- crates/egui_extras/src/table.rs | 8 +++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 0876d9db0..b970d392e 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -1,4 +1,4 @@ -use egui::{Pos2, Rect, Response, Sense, Ui}; +use egui::{Id, Pos2, Rect, Response, Sense, Ui}; #[derive(Clone, Copy)] pub(crate) enum CellSize { @@ -113,6 +113,7 @@ impl<'l> StripLayout<'l> { flags: StripLayoutFlags, width: CellSize, height: CellSize, + child_ui_id_source: Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> (Rect, Response) { let max_rect = self.cell_rect(&width, &height); @@ -145,7 +146,7 @@ impl<'l> StripLayout<'l> { ); } - let used_rect = self.cell(flags, max_rect, add_cell_contents); + let used_rect = self.cell(flags, max_rect, child_ui_id_source, add_cell_contents); self.set_pos(max_rect); @@ -186,9 +187,12 @@ impl<'l> StripLayout<'l> { &mut self, flags: StripLayoutFlags, rect: Rect, + child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> Rect { - let mut child_ui = self.ui.child_ui(rect, self.cell_layout); + let mut child_ui = + self.ui + .child_ui_with_id_source(rect, self.cell_layout, child_ui_id_source); if flags.clip { let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); diff --git a/crates/egui_extras/src/strip.rs b/crates/egui_extras/src/strip.rs index 67903d1cb..9087f673b 100644 --- a/crates/egui_extras/src/strip.rs +++ b/crates/egui_extras/src/strip.rs @@ -194,7 +194,13 @@ impl<'a, 'b> Strip<'a, 'b> { clip: self.clip, ..Default::default() }; - self.layout.add(flags, width, height, add_contents); + self.layout.add( + flags, + width, + height, + egui::Id::new(self.size_index), + add_contents, + ); } /// Add an empty cell. diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 008041c29..5e9cf80fd 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -1166,7 +1166,13 @@ impl<'a, 'b> TableRow<'a, 'b> { selected: self.selected, }; - let (used_rect, response) = self.layout.add(flags, width, height, add_cell_contents); + let (used_rect, response) = self.layout.add( + flags, + width, + height, + egui::Id::new((self.row_index, col_index)), + add_cell_contents, + ); if let Some(max_w) = self.max_used_widths.get_mut(col_index) { *max_w = max_w.max(used_rect.width()); From 9096abdeec62fe50c867082c4b2785b77375836f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 20 Feb 2024 14:39:26 +0100 Subject: [PATCH 02/27] Support interacting with the background of a `Ui` (#4074) Add `Ui::interact_bg` which interacts with the ui _behind_ any of its children. --- crates/egui/src/context.rs | 145 +++++---------------------------- crates/egui/src/hit_test.rs | 3 +- crates/egui/src/interaction.rs | 11 +-- crates/egui/src/lib.rs | 4 +- crates/egui/src/response.rs | 2 +- crates/egui/src/ui.rs | 43 +++++++++- crates/egui/src/widget_rect.rs | 125 ++++++++++++++++++++++++++++ 7 files changed, 195 insertions(+), 138 deletions(-) create mode 100644 crates/egui/src/widget_rect.rs diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 22741b237..996f7a7ec 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -198,100 +198,6 @@ impl ContextImpl { // ---------------------------------------------------------------------------- -/// Used to store each widget's [Id], [Rect] and [Sense] each frame. -/// Used to check for overlaps between widgets when handling events. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WidgetRect { - /// The globally unique widget id. - /// - /// For interactive widgets, this better be globally unique. - /// If not there will be weird bugs, - /// and also big red warning test on the screen in debug builds - /// (see [`Options::warn_on_id_clash`]). - /// - /// You can ensure globally unique ids using [`Ui::push_id`]. - pub id: Id, - - /// What layer the widget is on. - pub layer_id: LayerId, - - /// The full widget rectangle. - pub rect: Rect, - - /// Where the widget is. - /// - /// This is after clipping with the parent ui clip rect. - pub interact_rect: Rect, - - /// How the widget responds to interaction. - pub sense: Sense, - - /// Is the widget enabled? - pub enabled: bool, -} - -/// Stores the positions of all widgets generated during a single egui update/frame. -/// -/// Actually, only those that are on screen. -#[derive(Default, Clone, PartialEq, Eq)] -pub struct WidgetRects { - /// All widgets, in painting order. - pub by_layer: HashMap>, - - /// All widgets - pub by_id: IdMap, -} - -impl WidgetRects { - /// Clear the contents while retaining allocated memory. - pub fn clear(&mut self) { - let Self { by_layer, by_id } = self; - - for rects in by_layer.values_mut() { - rects.clear(); - } - - by_id.clear(); - } - - /// Insert the given widget rect in the given layer. - pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { - if !widget_rect.interact_rect.is_positive() { - return; - } - - let Self { by_layer, by_id } = self; - - let layer_widgets = by_layer.entry(layer_id).or_default(); - - match by_id.entry(widget_rect.id) { - std::collections::hash_map::Entry::Vacant(entry) => { - // A new widget - entry.insert(widget_rect); - layer_widgets.push(widget_rect); - } - std::collections::hash_map::Entry::Occupied(mut entry) => { - // e.g. calling `response.interact(…)` to add more interaction. - let existing = entry.get_mut(); - existing.rect = existing.rect.union(widget_rect.rect); - existing.interact_rect = existing.interact_rect.union(widget_rect.interact_rect); - existing.sense |= widget_rect.sense; - existing.enabled |= widget_rect.enabled; - - // Find the existing widget in this layer and update it: - for previous in layer_widgets.iter_mut().rev() { - if previous.id == widget_rect.id { - *previous = *existing; - break; - } - } - } - } - } -} - -// ---------------------------------------------------------------------------- - /// State stored per viewport #[derive(Default)] struct ViewportState { @@ -546,12 +452,7 @@ impl ContextImpl { .map(|(i, id)| (*id, i)) .collect(); - let mut layers: Vec = viewport - .widgets_prev_frame - .by_layer - .keys() - .copied() - .collect(); + let mut layers: Vec = viewport.widgets_prev_frame.layer_ids().collect(); layers.sort_by(|a, b| { if a.order == b.order { @@ -1124,23 +1025,19 @@ impl Context { w.sense.drag = false; } - if w.interact_rect.is_positive() { - // Remember this widget - self.write(|ctx| { - let viewport = ctx.viewport(); + // Remember this widget + self.write(|ctx| { + let viewport = ctx.viewport(); - // We add all widgets here, even non-interactive ones, - // because we need this list not only for checking for blocking widgets, - // but also to know when we have reached the widget we are checking for cover. - viewport.widgets_this_frame.insert(w.layer_id, w); + // We add all widgets here, even non-interactive ones, + // because we need this list not only for checking for blocking widgets, + // but also to know when we have reached the widget we are checking for cover. + viewport.widgets_this_frame.insert(w.layer_id, w); - if w.sense.focusable { - ctx.memory.interested_in_focus(w.id); - } - }); - } else { - // Don't remember invisible widgets - } + if w.sense.focusable { + ctx.memory.interested_in_focus(w.id); + } + }); if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { // Not interested or allowed input: @@ -1175,9 +1072,8 @@ impl Context { let viewport = ctx.viewport(); viewport .widgets_this_frame - .by_id - .get(&id) - .or_else(|| viewport.widgets_prev_frame.by_id.get(&id)) + .get(id) + .or_else(|| viewport.widgets_prev_frame.get(id)) .copied() }) .map(|widget_rect| self.get_response(widget_rect)) @@ -1916,13 +1812,16 @@ impl Context { #[cfg(debug_assertions)] fn debug_painting(&self) { let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { - let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); - painter.debug_rect(widget.interact_rect, color, text); + let rect = widget.interact_rect; + if rect.is_positive() { + let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); + painter.debug_rect(rect, color, text); + } }; let paint_widget_id = |id: Id, text: &str, color: Color32| { if let Some(widget) = - self.write(|ctx| ctx.viewport().widgets_this_frame.by_id.get(&id).cloned()) + self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).cloned()) { paint_widget(&widget, text, color); } @@ -1931,8 +1830,8 @@ impl Context { if self.style().debug.show_interactive_widgets { // Show all interactive widgets: let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); - for (layer_id, rects) in rects.by_layer { - let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING); + for (layer_id, rects) in rects.layers() { + let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING); for rect in rects { if rect.sense.interactive() { let (color, text) = if rect.sense.click && rect.sense.drag { diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index fe8b085a3..2b778e9d7 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -56,8 +56,7 @@ pub fn hit_test( let mut close: Vec = layer_order .iter() .filter(|layer| layer.order.allow_interaction()) - .filter_map(|layer_id| widgets.by_layer.get(layer_id)) - .flatten() + .flat_map(|&layer_id| widgets.get_layer(layer_id)) .filter(|&w| { let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index f1a3e2a32..2f1925c75 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -107,13 +107,13 @@ pub(crate) fn interact( crate::profile_function!(); if let Some(id) = interaction.potential_click_id { - if !widgets.by_id.contains_key(&id) { + if !widgets.contains(id) { // The widget we were interested in clicking is gone. interaction.potential_click_id = None; } } if let Some(id) = interaction.potential_drag_id { - if !widgets.by_id.contains_key(&id) { + if !widgets.contains(id) { // The widget we were interested in dragging is gone. // This is fine! This could be drag-and-drop, // and the widget being dragged is now "in the air" and thus @@ -145,7 +145,7 @@ pub(crate) fn interact( if click.is_some() { if let Some(widget) = interaction .potential_click_id - .and_then(|id| widgets.by_id.get(&id)) + .and_then(|id| widgets.get(id)) { clicked = Some(widget.id); } @@ -160,10 +160,7 @@ pub(crate) fn interact( if dragged.is_none() { // Check if we started dragging something new: - if let Some(widget) = interaction - .potential_drag_id - .and_then(|id| widgets.by_id.get(&id)) - { + if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) { let is_dragged = if widget.sense.click && widget.sense.drag { // This widget is sensitive to both clicks and drags. // When the mouse first is pressed, it could be either, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c715b859e..9c065811d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -403,6 +403,7 @@ pub mod text_selection; mod ui; pub mod util; pub mod viewport; +mod widget_rect; pub mod widget_text; pub mod widgets; @@ -443,7 +444,7 @@ pub mod text { pub use { containers::*, - context::{Context, RepaintCause, RequestRepaintInfo, WidgetRect, WidgetRects}, + context::{Context, RepaintCause, RequestRepaintInfo}, data::{ input::*, output::{ @@ -466,6 +467,7 @@ pub use { text::{Galley, TextFormat}, ui::Ui, viewport::*, + widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, widgets::*, }; diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 6e791fc57..82349993e 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -658,7 +658,7 @@ impl Response { id: self.id, rect: self.rect, interact_rect: self.interact_rect, - sense, + sense: self.sense | sense, enabled: self.enabled, }) } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 182c5782f..b5f861897 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -75,7 +75,7 @@ impl Ui { /// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. pub fn new(ctx: Context, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self { let style = ctx.style(); - Ui { + let ui = Ui { id, next_auto_id_source: id.with("auto").value(), painter: Painter::new(ctx, layer_id, clip_rect), @@ -83,7 +83,20 @@ impl Ui { placer: Placer::new(max_rect, Layout::default()), enabled: true, menu_state: None, - } + }; + + // Register in the widget stack early, to ensure we are behind all widgets we contain: + let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + ui.ctx().create_widget(WidgetRect { + id: ui.id, + layer_id: ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense: Sense::hover(), + enabled: ui.enabled, + }); + + ui } /// Create a new [`Ui`] at a specific region. @@ -101,7 +114,7 @@ impl Ui { crate::egui_assert!(!max_rect.any_nan()); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); - Ui { + let child_ui = Ui { id: self.id.with(id_source), next_auto_id_source, painter: self.painter.clone(), @@ -109,7 +122,20 @@ impl Ui { placer: Placer::new(max_rect, layout), enabled: self.enabled, menu_state: self.menu_state.clone(), - } + }; + + // Register in the widget stack early, to ensure we are behind all widgets we contain: + let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + child_ui.ctx().create_widget(WidgetRect { + id: child_ui.id, + layer_id: child_ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense: Sense::hover(), + enabled: child_ui.enabled, + }); + + child_ui } // ------------------------------------------------- @@ -668,6 +694,15 @@ impl Ui { self.interact(rect, id, sense) } + /// Interact with the background of this [`Ui`], + /// i.e. behind all the widgets. + /// + /// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`]. + pub fn interact_bg(&self, sense: Sense) -> Response { + // This will update the WidgetRect that was first created in `Ui::new`. + self.interact(self.min_rect(), self.id, sense) + } + /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? /// /// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance, diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs new file mode 100644 index 000000000..1ef8c28bd --- /dev/null +++ b/crates/egui/src/widget_rect.rs @@ -0,0 +1,125 @@ +use ahash::HashMap; + +use crate::*; + +/// Used to store each widget's [Id], [Rect] and [Sense] each frame. +/// +/// Used to check which widget gets input when a user clicks somewhere. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WidgetRect { + /// The globally unique widget id. + /// + /// For interactive widgets, this better be globally unique. + /// If not there will be weird bugs, + /// and also big red warning test on the screen in debug builds + /// (see [`Options::warn_on_id_clash`]). + /// + /// You can ensure globally unique ids using [`Ui::push_id`]. + pub id: Id, + + /// What layer the widget is on. + pub layer_id: LayerId, + + /// The full widget rectangle. + pub rect: Rect, + + /// Where the widget is. + /// + /// This is after clipping with the parent ui clip rect. + pub interact_rect: Rect, + + /// How the widget responds to interaction. + pub sense: Sense, + + /// Is the widget enabled? + pub enabled: bool, +} + +/// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame. +/// +/// All [`Ui`]s have a [`WidgetRects`], but whether or not their rects are correct +/// depends on if [`Ui::interact_bg`] was ever called. +#[derive(Default, Clone, PartialEq, Eq)] +pub struct WidgetRects { + /// All widgets, in painting order. + by_layer: HashMap>, + + /// All widgets, by id, and their order in their respective layer + by_id: IdMap<(usize, WidgetRect)>, +} + +impl WidgetRects { + /// All known layers with widgets. + pub fn layer_ids(&self) -> impl ExactSizeIterator + '_ { + self.by_layer.keys().copied() + } + + pub fn layers(&self) -> impl Iterator + '_ { + self.by_layer + .iter() + .map(|(layer_id, rects)| (layer_id, &rects[..])) + } + + #[inline] + pub fn get(&self, id: Id) -> Option<&WidgetRect> { + self.by_id.get(&id).map(|(_, w)| w) + } + + #[inline] + pub fn contains(&self, id: Id) -> bool { + self.by_id.contains_key(&id) + } + + /// All widgets in this layer, sorted back-to-front. + #[inline] + pub fn get_layer(&self, layer_id: LayerId) -> impl Iterator + '_ { + self.by_layer.get(&layer_id).into_iter().flatten() + } + + /// Clear the contents while retaining allocated memory. + pub fn clear(&mut self) { + let Self { by_layer, by_id } = self; + + for rects in by_layer.values_mut() { + rects.clear(); + } + + by_id.clear(); + } + + /// Insert the given widget rect in the given layer. + pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { + let Self { by_layer, by_id } = self; + + let layer_widgets = by_layer.entry(layer_id).or_default(); + + match by_id.entry(widget_rect.id) { + std::collections::hash_map::Entry::Vacant(entry) => { + // A new widget + let idx_in_layer = layer_widgets.len(); + entry.insert((idx_in_layer, widget_rect)); + layer_widgets.push(widget_rect); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + // This is a known widget, but we might need to update it! + // e.g. calling `response.interact(…)` to add more interaction. + let (idx_in_layer, existing) = entry.get_mut(); + + // Update it: + existing.rect = widget_rect.rect; // last wins + existing.interact_rect = widget_rect.interact_rect; // last wins + existing.sense |= widget_rect.sense; + existing.enabled |= widget_rect.enabled; + + egui_assert!( + existing.layer_id == widget_rect.layer_id, + "Widget changed layer_id during the frame" + ); + + if existing.layer_id == widget_rect.layer_id { + layer_widgets[*idx_in_layer] = *existing; + } + } + } + } +} From 4fc0c49a6be17dec3cb438e8935b65ed351730e9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 20 Feb 2024 14:50:13 +0100 Subject: [PATCH 03/27] Fix interaction with widgets inside selectable rows of `Table` (#4077) * Closes https://github.com/emilk/egui/issues/4047 --- crates/egui_extras/src/layout.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index b970d392e..a8ae5998d 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -146,7 +146,9 @@ impl<'l> StripLayout<'l> { ); } - let used_rect = self.cell(flags, max_rect, child_ui_id_source, add_cell_contents); + let child_ui = self.cell(flags, max_rect, child_ui_id_source, add_cell_contents); + + let used_rect = child_ui.min_rect(); self.set_pos(max_rect); @@ -156,7 +158,9 @@ impl<'l> StripLayout<'l> { max_rect.union(used_rect) }; - let response = self.ui.allocate_rect(allocation_rect, self.sense); + self.ui.advance_cursor_after_rect(allocation_rect); + + let response = child_ui.interact(max_rect, child_ui.id(), self.sense); (used_rect, response) } @@ -183,13 +187,14 @@ impl<'l> StripLayout<'l> { self.ui.allocate_rect(rect, Sense::hover()); } + /// Return the Ui to which the contents where added fn cell( &mut self, flags: StripLayoutFlags, rect: Rect, child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), - ) -> Rect { + ) -> Ui { let mut child_ui = self.ui .child_ui_with_id_source(rect, self.cell_layout, child_ui_id_source); @@ -208,7 +213,7 @@ impl<'l> StripLayout<'l> { add_cell_contents(&mut child_ui); - child_ui.min_rect() + child_ui } /// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size From ca8eeb8621592b8a74cc60f6890b0274165bb535 Mon Sep 17 00:00:00 2001 From: wangxiaochuTHU <86735730+wangxiaochuTHU@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:29:23 +0800 Subject: [PATCH 04/27] Add `ColorImage::from_gray_iter` (#3536) Add an alternative method for creating a [`ColorImage`] that accepts `Iterator` as the argument. It can be useful when `&[u8]` is not available but the iterator is. --------- Co-authored-by: Emil Ernerfeldt --- crates/epaint/src/image.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index fa3f1e5e0..454969d75 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -120,6 +120,16 @@ impl ColorImage { Self { size, pixels } } + /// Alternative method to `from_gray`. + /// Create a [`ColorImage`] from iterator over flat opaque gray data. + /// + /// Panics if `size[0] * size[1] != gray_iter.len()`. + pub fn from_gray_iter(size: [usize; 2], gray_iter: impl Iterator) -> Self { + let pixels: Vec<_> = gray_iter.map(Color32::from_gray).collect(); + assert_eq!(size[0] * size[1], pixels.len()); + Self { size, pixels } + } + /// A view of the underlying data as `&[u8]` #[cfg(feature = "bytemuck")] pub fn as_raw(&self) -> &[u8] { From 74891cac2f45f844a41dee17f184f701d1f33ca6 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:26:47 +0100 Subject: [PATCH 05/27] egui_plot: fix panic when the base step size is set to 0 (#4078) This can happen e.g. when the user forces some axis range to (0.0, 0.0) like in rerun-io/rerun#5239 --- crates/egui_plot/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 692399e8b..4238c7b32 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1391,6 +1391,11 @@ pub struct GridMark { pub fn log_grid_spacer(log_base: i64) -> GridSpacer { let log_base = log_base as f64; let step_sizes = move |input: GridInput| -> Vec { + // handle degenerate cases + if input.base_step_size.abs() < f64::EPSILON { + return Vec::new(); + } + // The distance between two of the thinnest grid lines is "rounded" up // to the next-bigger power of base let smallest_visible_unit = next_power(input.base_step_size, log_base); @@ -1693,7 +1698,7 @@ impl PreparedPlot { /// assert_eq!(next_power(0.2, 10.0), 1); /// ``` fn next_power(value: f64, base: f64) -> f64 { - assert_ne!(value, 0.0); // can be negative (typical for Y axis) + debug_assert_ne!(value, 0.0); // can be negative (typical for Y axis) base.powi(value.abs().log(base).ceil() as i32) } @@ -1708,7 +1713,7 @@ fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec { /// Fill in all values between [min, max] which are a multiple of `step_size` fn fill_marks_between(out: &mut Vec, step_size: f64, (min, max): (f64, f64)) { - assert!(max > min); + debug_assert!(max > min); let first = (min / step_size).ceil() as i64; let last = (max / step_size).ceil() as i64; From b8048572e8cc47ef9410b3516456da2a320fcdd2 Mon Sep 17 00:00:00 2001 From: Giantblargg Date: Tue, 20 Feb 2024 10:00:39 -0700 Subject: [PATCH 06/27] Add API for raw mouse motion (#4063) Raw mouse movement is unaccelerated and unclamped by screen boundaries, and does not relate to any position on the screen. It is useful in certain situations such as draggable values and 3D cameras, where screen position does not matter. https://github.com/emilk/egui/assets/1700581/1400e6a6-0573-41b9-99a1-a9cd305aa1a3 Added `Event::MouseMoved` for integrations to supply raw mouse movement. Added `Response:drag_motion` to get the raw mouse movement, but will fall back to delta in case the integration does not supply it. Nothing should be breaking, but third-party integrations that can send `Event::MouseMoved` should be updated to do so. Based on #1614 but updated to the current version, and with better fallback behaviour. * Closes #1611 * Supersedes #1614 --- crates/eframe/src/native/glow_integration.rs | 27 +++++++++++++++++++ crates/eframe/src/native/wgpu_integration.rs | 27 +++++++++++++++++++ crates/egui-winit/src/lib.rs | 7 +++++ crates/egui/src/data/input.rs | 6 +++++ crates/egui/src/input_state.rs | 20 ++++++++++++++ crates/egui/src/response.rs | 14 ++++++++++ .../egui_demo_app/src/apps/custom3d_glow.rs | 2 +- .../egui_demo_app/src/apps/custom3d_wgpu.rs | 2 +- examples/custom_3d_glow/src/main.rs | 2 +- 9 files changed, 104 insertions(+), 3 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index be30d853c..0cb7ec331 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -446,6 +446,33 @@ impl WinitApp for GlowWinitApp { } } + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut glutin = running.glutin.borrow_mut(); + if let Some(viewport) = glutin + .focused_viewport + .and_then(|viewport| glutin.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( accesskit_winit::ActionRequestEvent { request, window_id }, diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index e8fe39f90..b3451be9c 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -456,6 +456,33 @@ impl WinitApp for WgpuWinitApp { } } + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut shared = running.shared.borrow_mut(); + if let Some(viewport) = shared + .focused_viewport + .and_then(|viewport| shared.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( accesskit_winit::ActionRequestEvent { request, window_id }, diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 15b6663a7..ca1150822 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -475,6 +475,13 @@ impl State { } } + pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { + self.egui_input.events.push(egui::Event::MouseMoved(Vec2 { + x: delta.0 as f32, + y: delta.1 as f32, + })); + } + /// Call this when there is a new [`accesskit::ActionRequest`]. /// /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`]. diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 21fa269db..f61b9312f 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -396,6 +396,12 @@ pub enum Event { /// The mouse or touch moved to a new place. PointerMoved(Pos2), + /// The mouse moved, the units are unspecified. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// `PointerMoved` and `MouseMoved` can be sent at the same time. + /// This event is optional. If the integration can not determine unfiltered motion it should not send this event. + MouseMoved(Vec2), + /// A mouse button was pressed or released (or a touch started or stopped). PointerButton { /// Where is the pointer? diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 0de78a437..5446a799f 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -617,6 +617,11 @@ pub struct PointerState { /// How much the pointer moved compared to last frame, in points. delta: Vec2, + /// How much the mouse moved since the last frame, in unspecified units. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// May be unavailable on some integrations. + motion: Option, + /// Current velocity of pointer. velocity: Vec2, @@ -664,6 +669,7 @@ impl Default for PointerState { latest_pos: None, interact_pos: None, delta: Vec2::ZERO, + motion: None, velocity: Vec2::ZERO, pos_history: History::new(0..1000, 0.1), down: Default::default(), @@ -690,6 +696,9 @@ impl PointerState { let old_pos = self.latest_pos; self.interact_pos = self.latest_pos; + if self.motion.is_some() { + self.motion = Some(Vec2::ZERO); + } for event in &new.events { match event { @@ -775,6 +784,7 @@ impl PointerState { self.latest_pos = None; // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. } + Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta, _ => {} } } @@ -819,6 +829,14 @@ impl PointerState { self.delta } + /// How much the mouse moved since the last frame, in unspecified units. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// May be unavailable on some integrations. + #[inline(always)] + pub fn motion(&self) -> Option { + self.motion + } + /// Current velocity of pointer. #[inline(always)] pub fn velocity(&self) -> Vec2 { @@ -1139,6 +1157,7 @@ impl PointerState { latest_pos, interact_pos, delta, + motion, velocity, pos_history: _, down, @@ -1155,6 +1174,7 @@ impl PointerState { ui.label(format!("latest_pos: {latest_pos:?}")); ui.label(format!("interact_pos: {interact_pos:?}")); ui.label(format!("delta: {delta:?}")); + ui.label(format!("motion: {motion:?}")); ui.label(format!( "velocity: [{:3.0} {:3.0}] points/sec", velocity.x, velocity.y diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 82349993e..e63def16c 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -356,6 +356,20 @@ impl Response { } } + /// If dragged, how far did the mouse move? + /// This will use raw mouse movement if provided by the integration, otherwise will fall back to [`Response::drag_delta`] + /// Raw mouse movement is unaccelerated and unclamped by screen boundaries, and does not relate to any position on the screen. + /// This may be useful in certain situations such as draggable values and 3D cameras, where screen position does not matter. + #[inline] + pub fn drag_motion(&self) -> Vec2 { + if self.dragged() { + self.ctx + .input(|i| i.pointer.motion().unwrap_or(i.pointer.delta())) + } else { + Vec2::ZERO + } + } + /// If the user started dragging this widget this frame, store the payload for drag-and-drop. #[doc(alias = "drag and drop")] pub fn dnd_set_drag_payload(&self, payload: Payload) { diff --git a/crates/egui_demo_app/src/apps/custom3d_glow.rs b/crates/egui_demo_app/src/apps/custom3d_glow.rs index 3175cf4fa..7f488e767 100644 --- a/crates/egui_demo_app/src/apps/custom3d_glow.rs +++ b/crates/egui_demo_app/src/apps/custom3d_glow.rs @@ -55,7 +55,7 @@ impl Custom3d { let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); - self.angle += response.drag_delta().x * 0.01; + self.angle += response.drag_motion().x * 0.01; // Clone locals so we can move them into the paint callback: let angle = self.angle; diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index 6b748cb1d..1676a0ba7 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -173,7 +173,7 @@ impl Custom3d { let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); - self.angle += response.drag_delta().x * 0.01; + self.angle += response.drag_motion().x * 0.01; ui.painter().add(egui_wgpu::Callback::new_paint_callback( rect, CustomTriangleCallback { angle: self.angle }, diff --git a/examples/custom_3d_glow/src/main.rs b/examples/custom_3d_glow/src/main.rs index 937973978..a1f6fa269 100644 --- a/examples/custom_3d_glow/src/main.rs +++ b/examples/custom_3d_glow/src/main.rs @@ -69,7 +69,7 @@ impl MyApp { let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); - self.angle += response.drag_delta().x * 0.01; + self.angle += response.drag_motion().x * 0.01; // Clone locals so we can move them into the paint callback: let angle = self.angle; From cdb7d153dcf05a614df5621ff738149eebcca16b Mon Sep 17 00:00:00 2001 From: Mike Tsao Date: Wed, 21 Feb 2024 07:20:14 -0800 Subject: [PATCH 07/27] `ui.dnd_drop_zone()` now returns `InnerResponse`. (#4079) * Closes ```bash $ ./scripts/check.sh [...] + echo 'All checks passed.' ``` --- crates/egui/src/ui.rs | 10 +++++----- crates/egui_demo_lib/src/demo/drag_and_drop.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index b5f861897..b82160119 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2223,11 +2223,11 @@ impl Ui { /// /// The given frame is used for its margins, but it color is ignored. #[doc(alias = "drag and drop")] - pub fn dnd_drop_zone( + pub fn dnd_drop_zone( &mut self, frame: Frame, - add_contents: impl FnOnce(&mut Ui), - ) -> (Response, Option>) + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> (InnerResponse, Option>) where Payload: Any + Send + Sync, { @@ -2236,7 +2236,7 @@ impl Ui { DragAndDrop::has_payload_of_type::(self.ctx()); let mut frame = frame.begin(self); - add_contents(&mut frame.content_ui); + let inner = add_contents(&mut frame.content_ui); let response = frame.allocate_space(self); // NOTE: we use `response.contains_pointer` here instead of `hovered`, because @@ -2266,7 +2266,7 @@ impl Ui { let payload = response.dnd_release_payload::(); - (response, payload) + (InnerResponse { inner, response }, payload) } /// Close the menu we are in (including submenus), if any. diff --git a/crates/egui_demo_lib/src/demo/drag_and_drop.rs b/crates/egui_demo_lib/src/demo/drag_and_drop.rs index 29bda3350..883095953 100644 --- a/crates/egui_demo_lib/src/demo/drag_and_drop.rs +++ b/crates/egui_demo_lib/src/demo/drag_and_drop.rs @@ -60,7 +60,7 @@ impl super::View for DragAndDropDemo { let frame = Frame::default().inner_margin(4.0); - let (_, dropped_payload) = ui.dnd_drop_zone::(frame, |ui| { + let (_, dropped_payload) = ui.dnd_drop_zone::(frame, |ui| { ui.set_min_size(vec2(64.0, 100.0)); for (row_idx, item) in column.iter().enumerate() { let item_id = Id::new(("my_drag_and_drop_demo", col_idx, row_idx)); From 23e8312fc004912537ef4e342a64ef845b396b78 Mon Sep 17 00:00:00 2001 From: 0Qwel <149522615+0Qwel@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:20:26 +0100 Subject: [PATCH 08/27] Convenience const fn for Margin, Rounding and Shadow (#4080) I often write constants at the top of my widget files, as a "config". I kept writing stuff like that : ```rust const DEFAULT_INNER_MARGIN: Margin = Margin { left: 17., right: 17., top: 7., bottom: 7. }; ``` So I prefixed constructors for `Margin`, `Rounding` and `Shadow` const. No code was changed. I also added a `Shadow::new()` for similar reasons. --- crates/egui/src/style.rs | 8 ++++---- crates/epaint/src/shadow.rs | 12 ++++++++---- crates/epaint/src/shape.rs | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index e42d1996c..6b77e8a14 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -622,7 +622,7 @@ impl Margin { }; #[inline] - pub fn same(margin: f32) -> Self { + pub const fn same(margin: f32) -> Self { Self { left: margin, right: margin, @@ -633,7 +633,7 @@ impl Margin { /// Margins with the same size on opposing sides #[inline] - pub fn symmetric(x: f32, y: f32) -> Self { + pub const fn symmetric(x: f32, y: f32) -> Self { Self { left: x, right: x, @@ -649,12 +649,12 @@ impl Margin { } #[inline] - pub fn left_top(&self) -> Vec2 { + pub const fn left_top(&self) -> Vec2 { vec2(self.left, self.top) } #[inline] - pub fn right_bottom(&self) -> Vec2 { + pub const fn right_bottom(&self) -> Vec2 { vec2(self.right, self.bottom) } diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index ad13037ae..fb6a9a4ab 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -19,8 +19,12 @@ impl Shadow { color: Color32::TRANSPARENT, }; + pub const fn new(extrusion: f32, color: Color32) -> Self { + Self { extrusion, color } + } + /// Tooltips, menus, …, for dark mode. - pub fn small_dark() -> Self { + pub const fn small_dark() -> Self { Self { extrusion: 16.0, color: Color32::from_black_alpha(96), @@ -28,7 +32,7 @@ impl Shadow { } /// Tooltips, menus, …, for light mode. - pub fn small_light() -> Self { + pub const fn small_light() -> Self { Self { extrusion: 16.0, color: Color32::from_black_alpha(20), @@ -36,7 +40,7 @@ impl Shadow { } /// Used for egui windows in dark mode. - pub fn big_dark() -> Self { + pub const fn big_dark() -> Self { Self { extrusion: 32.0, color: Color32::from_black_alpha(96), @@ -44,7 +48,7 @@ impl Shadow { } /// Used for egui windows in light mode. - pub fn big_light() -> Self { + pub const fn big_light() -> Self { Self { extrusion: 32.0, color: Color32::from_black_alpha(16), diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index ffc7b174b..033d4dfc5 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -701,7 +701,7 @@ impl Rounding { }; #[inline] - pub fn same(radius: f32) -> Self { + pub const fn same(radius: f32) -> Self { Self { nw: radius, ne: radius, From 5cf99c6308e36b5f36861accea6835f190924cce Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 21 Feb 2024 16:25:28 +0100 Subject: [PATCH 09/27] Add some profile scopes to handle_platform_output --- crates/egui-winit/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index ca1150822..ac29a0b24 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -815,12 +815,14 @@ impl State { let allow_ime = ime.is_some(); if self.allow_ime != allow_ime { self.allow_ime = allow_ime; + crate::profile_scope!("set_ime_allowed"); window.set_ime_allowed(allow_ime); } if let Some(ime) = ime { let rect = ime.rect; let pixels_per_point = pixels_per_point(&self.egui_ctx, window); + crate::profile_scope!("set_ime_cursor_area"); window.set_ime_cursor_area( winit::dpi::PhysicalPosition { x: pixels_per_point * rect.min.x, @@ -836,6 +838,7 @@ impl State { #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_ref() { if let Some(update) = accesskit_update { + crate::profile_scope!("accesskit"); accesskit.update_if_active(|| update); } } From e8af6f38fcebcc889963e86c4a3ccebe97db1908 Mon Sep 17 00:00:00 2001 From: Georg Weisert Date: Mon, 26 Feb 2024 13:33:43 +0100 Subject: [PATCH 10/27] Serde feature: Add serde derives to input related structs (#4100) We plan to store input data for creating automated tests, hence the need for more serde derives on input related structs. --------- Co-authored-by: Georg Weisert --- crates/egui/src/input_state.rs | 4 ++++ crates/egui/src/input_state/touch_state.rs | 5 +++++ crates/emath/src/history.rs | 1 + 3 files changed, 10 insertions(+) diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 5446a799f..1b1417d14 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -22,6 +22,7 @@ const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings /// You can check if `egui` is using the inputs using /// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`]. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct InputState { /// The raw input we got this frame from the backend. pub raw: RawInput, @@ -546,6 +547,7 @@ impl InputState { /// A pointer (mouse or touch) click. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct Click { pub pos: Pos2, @@ -567,6 +569,7 @@ impl Click { } #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) enum PointerEvent { Moved(Pos2), Pressed { @@ -595,6 +598,7 @@ impl PointerEvent { /// Mouse or touch state. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PointerState { /// Latest known time time: f64, diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index a948ab16f..2ee7a91de 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -64,6 +64,7 @@ pub struct MultiTouchInfo { /// The current state (for a specific touch device) of touch events and gestures. #[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct TouchState { /// Technical identifier of the touch device. This is used to identify relevant touch events /// for this [`TouchState`] instance. @@ -83,6 +84,7 @@ pub(crate) struct TouchState { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct GestureState { start_time: f64, start_pointer_pos: Pos2, @@ -93,6 +95,7 @@ struct GestureState { /// Gesture data that can change over time #[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct DynGestureState { /// used for proportional zooming avg_distance: f32, @@ -110,6 +113,7 @@ struct DynGestureState { /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as /// long as the finger/pen touches the surface. #[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct ActiveTouch { /// Current position of this touch, in device coordinates (not necessarily screen position) pos: Pos2, @@ -302,6 +306,7 @@ impl Debug for TouchState { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] enum PinchType { Horizontal, Vertical, diff --git a/crates/emath/src/history.rs b/crates/emath/src/history.rs index bacd9625f..d85a27a39 100644 --- a/crates/emath/src/history.rs +++ b/crates/emath/src/history.rs @@ -16,6 +16,7 @@ use std::collections::VecDeque; /// or for smoothed velocity (e.g. mouse pointer speed). /// All times are in seconds. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct History { /// In elements, i.e. of `values.len()`. /// The length is initially zero, but once past `min_len` will not shrink below it. From 86d7f296ae7a2c34778aeec533d246e8280c8e96 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 29 Feb 2024 14:18:06 +0100 Subject: [PATCH 11/27] Give each menu `Area` an id distinct from the id of what was clicked (#4114) * Closes https://github.com/emilk/egui/issues/4113 Previously the `Id` of the menu `Area` was using the same id as the thing that was clicked (i.e. the button opening menu), which lead to id clashes --- crates/egui/src/menu.rs | 32 ++++++++++++++++---------------- crates/egui/src/widget_rect.rs | 10 +++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 846b57383..ad9d84b94 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -39,13 +39,14 @@ impl BarState { } /// Show a menu at pointer if primary-clicked response. + /// /// Should be called from [`Context`] on a [`Response`] pub fn bar_menu( &mut self, response: &Response, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option> { - MenuRoot::stationary_click_interaction(response, &mut self.open_menu, response.id); + MenuRoot::stationary_click_interaction(response, &mut self.open_menu); self.open_menu.show(response, add_contents) } @@ -134,7 +135,7 @@ pub(crate) fn submenu_button( /// wrapper for the contents of every menu. pub(crate) fn menu_ui<'c, R>( ctx: &Context, - menu_id: impl Into, + menu_id: Id, menu_state_arc: &Arc>, add_contents: impl FnOnce(&mut Ui) -> R + 'c, ) -> InnerResponse { @@ -144,7 +145,7 @@ pub(crate) fn menu_ui<'c, R>( menu_state.rect.min }; - let area = Area::new(menu_id) + let area = Area::new(menu_id.with("__menu")) .order(Order::Foreground) .fixed_pos(pos) .constrain_to(ctx.screen_rect()) @@ -222,7 +223,7 @@ pub(crate) fn context_menu( let menu_id = Id::new(CONTEXT_MENU_ID_STR); let mut bar_state = BarState::load(&response.ctx, menu_id); - MenuRoot::context_click_interaction(response, &mut bar_state, response.id); + MenuRoot::context_click_interaction(response, &mut bar_state); let inner_response = bar_state.show(response, add_contents); bar_state.store(&response.ctx, menu_id); @@ -237,6 +238,7 @@ pub(crate) struct MenuRootManager { impl MenuRootManager { /// Show a menu at pointer if right-clicked response. + /// /// Should be called from [`Context`] on a [`Response`] pub fn show( &mut self, @@ -308,11 +310,9 @@ impl MenuRoot { /// Interaction with a stationary menu, i.e. fixed in another Ui. /// /// Responds to primary clicks. - fn stationary_interaction( - response: &Response, - root: &mut MenuRootManager, - id: Id, - ) -> MenuResponse { + fn stationary_interaction(response: &Response, root: &mut MenuRootManager) -> MenuResponse { + let id = response.id; + if (response.clicked() && root.is_menu_open(id)) || response.ctx.input(|i| i.key_pressed(Key::Escape)) { @@ -357,8 +357,8 @@ impl MenuRoot { MenuResponse::Stay } - /// Interaction with a context menu (secondary clicks). - fn context_interaction(response: &Response, root: &mut Option, id: Id) -> MenuResponse { + /// Interaction with a context menu (secondary click). + fn context_interaction(response: &Response, root: &mut Option) -> MenuResponse { let response = response.interact(Sense::click()); response.ctx.input(|input| { let pointer = &input.pointer; @@ -371,7 +371,7 @@ impl MenuRoot { } if !in_old_menu { if response.hovered() && response.secondary_clicked() { - return MenuResponse::Create(pos, id); + return MenuResponse::Create(pos, response.id); } else if (response.hovered() && pointer.primary_down()) || destroy { return MenuResponse::Close; } @@ -392,14 +392,14 @@ impl MenuRoot { } /// Respond to secondary (right) clicks. - pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) { - let menu_response = Self::context_interaction(response, root, id); + pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager) { + let menu_response = Self::context_interaction(response, root); Self::handle_menu_response(root, menu_response); } // Responds to primary clicks. - pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) { - let menu_response = Self::stationary_interaction(response, root, id); + pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager) { + let menu_response = Self::stationary_interaction(response, root); Self::handle_menu_response(root, menu_response); } } diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index 1ef8c28bd..acf3dd95d 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -105,17 +105,17 @@ impl WidgetRects { // e.g. calling `response.interact(…)` to add more interaction. let (idx_in_layer, existing) = entry.get_mut(); + egui_assert!( + existing.layer_id == widget_rect.layer_id, + "Widget changed layer_id during the frame" + ); + // Update it: existing.rect = widget_rect.rect; // last wins existing.interact_rect = widget_rect.interact_rect; // last wins existing.sense |= widget_rect.sense; existing.enabled |= widget_rect.enabled; - egui_assert!( - existing.layer_id == widget_rect.layer_id, - "Widget changed layer_id during the frame" - ); - if existing.layer_id == widget_rect.layer_id { layer_widgets[*idx_in_layer] = *existing; } From e29022efc4783fe06842a46371d5bd88e3f13bdd Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 29 Feb 2024 15:34:16 +0100 Subject: [PATCH 12/27] `Area::new` now takes an `Id` by argument (#4115) This makes it more explicit that you are responsible for assigning a globally unique `Id`. --- crates/egui/src/containers/area.rs | 10 +++++++--- crates/egui_demo_lib/src/demo/pan_zoom.rs | 14 ++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index e93217e58..7a9111f64 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -52,7 +52,7 @@ impl State { /// /// ``` /// # egui::__run_test_ctx(|ctx| { -/// egui::Area::new("my_area") +/// egui::Area::new(egui::Id::new("my_area")) /// .fixed_pos(egui::pos2(32.0, 32.0)) /// .show(ctx, |ui| { /// ui.label("Floating text!"); @@ -79,9 +79,10 @@ pub struct Area { } impl Area { - pub fn new(id: impl Into) -> Self { + /// The `id` must be globally unique. + pub fn new(id: Id) -> Self { Self { - id: id.into(), + id, movable: true, interactable: true, constrain: false, @@ -96,6 +97,9 @@ impl Area { } } + /// Let's you change the `id` that you assigned in [`Self::new`]. + /// + /// The `id` must be globally unique. #[inline] pub fn id(mut self, id: Id) -> Self { self.id = id; diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 6aa42179c..08829d6d5 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -69,30 +69,25 @@ impl super::View for PanZoom { } } - for (id, pos, callback) in [ + for (i, (pos, callback)) in [ ( - "a", egui::Pos2::new(0.0, 0.0), Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) as Box egui::Response>, ), ( - "b", egui::Pos2::new(0.0, 120.0), Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), ), ( - "c", egui::Pos2::new(120.0, 120.0), Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), ), ( - "d", egui::Pos2::new(120.0, 0.0), Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), ), ( - "e", egui::Pos2::new(60.0, 60.0), Box::new(|ui, state| { use egui::epaint::*; @@ -110,8 +105,11 @@ impl super::View for PanZoom { ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value")) }), ), - ] { - let id = egui::Area::new(id) + ] + .into_iter() + .enumerate() + { + let id = egui::Area::new(id.with(("subarea", i))) .default_pos(pos) // Need to cover up the pan_zoom demo window, // but may also cover over other windows. From 18eeb01f5784ced56c8085138c95e2680e3d3c2e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 4 Mar 2024 20:00:13 +0100 Subject: [PATCH 13/27] Quickly animate scroll when calling `ui.scroll_to_cursor` etc (#4119) Uses ease-in-ease-out interpolation, with a time between 0.1s and 0.3s, depending on the distance needed to scroll. ![smooth-scroll-to-target](https://github.com/emilk/egui/assets/1148717/c5c8556d-476b-4597-842b-aa0e5927fbb9) --- crates/egui/src/containers/scroll_area.rs | 95 ++++++++++++++++++---- crates/egui/src/input_state.rs | 13 ++- crates/egui/src/response.rs | 1 + crates/egui/src/ui.rs | 2 + crates/egui_demo_lib/src/demo/scrolling.rs | 6 +- crates/emath/src/lib.rs | 46 +++++++++++ 6 files changed, 141 insertions(+), 22 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index d18cd8028..27128f671 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -2,6 +2,13 @@ use crate::*; +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +struct ScrollTarget { + animation_time_span: (f64, f64), + target_offset: f32, +} + #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -9,6 +16,9 @@ pub struct State { /// Positive offset means scrolling down/right pub offset: Vec2, + /// If set, quickly but smoothly scroll to this target offset. + offset_target: [Option; 2], + /// Were the scroll bars visible last frame? show_scroll: Vec2b, @@ -35,6 +45,7 @@ impl Default for State { fn default() -> Self { Self { offset: Vec2::ZERO, + offset_target: Default::default(), show_scroll: Vec2b::FALSE, content_is_too_large: Vec2b::FALSE, scroll_bar_interaction: Vec2b::FALSE, @@ -559,25 +570,56 @@ impl ScrollArea { state.vel[d] = input.pointer.velocity()[d]; }); state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } else { state.vel[d] = 0.0; } } } else { - // Kinetic scrolling - let stop_speed = 20.0; // Pixels per second. - let friction_coeff = 1000.0; // Pixels per second squared. - let dt = ui.input(|i| i.unstable_dt); + for d in 0..2 { + let dt = ui.input(|i| i.stable_dt).at_most(0.1); - let friction = friction_coeff * dt; - if friction > state.vel.length() || state.vel.length() < stop_speed { - state.vel = Vec2::ZERO; - } else { - state.vel -= friction * state.vel.normalized(); - // Offset has an inverted coordinate system compared to - // the velocity, so we subtract it instead of adding it - state.offset -= state.vel * dt; - ctx.request_repaint(); + if let Some(scroll_target) = state.offset_target[d] { + state.vel[d] = 0.0; + + if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 { + // Arrived + state.offset[d] = scroll_target.target_offset; + state.offset_target[d] = None; + } else { + // Move towards target + let t = emath::interpolation_factor( + scroll_target.animation_time_span, + ui.input(|i| i.time), + dt, + emath::ease_in_ease_out, + ); + if t < 1.0 { + state.offset[d] = + emath::lerp(state.offset[d]..=scroll_target.target_offset, t); + ctx.request_repaint(); + } else { + // Arrived + state.offset[d] = scroll_target.target_offset; + state.offset_target[d] = None; + } + } + } else { + // Kinetic scrolling + let stop_speed = 20.0; // Pixels per second. + let friction_coeff = 1000.0; // Pixels per second squared. + + let friction = friction_coeff * dt; + if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed { + state.vel[d] = 0.0; + } else { + state.vel[d] -= friction * state.vel[d].signum(); + // Offset has an inverted coordinate system compared to + // the velocity, so we subtract it instead of adding it + state.offset[d] -= state.vel[d] * dt; + ctx.request_repaint(); + } + } } } } @@ -716,11 +758,11 @@ impl Prepared { let scroll_target = content_ui .ctx() .frame_state_mut(|state| state.scroll_target[d].take()); - if let Some((scroll, align)) = scroll_target { + if let Some((target_range, align)) = scroll_target { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); let visible_range = min..=min + clip_rect.size()[d]; - let (start, end) = (scroll.min, scroll.max); + let (start, end) = (target_range.min, target_range.max); let clip_start = clip_rect.min[d]; let clip_end = clip_rect.max[d]; let mut spacing = ui.spacing().item_spacing[d]; @@ -729,7 +771,7 @@ impl Prepared { let center_factor = align.to_factor(); let offset = - lerp(scroll, center_factor) - lerp(visible_range, center_factor); + lerp(target_range, center_factor) - lerp(visible_range, center_factor); // Depending on the alignment we need to add or subtract the spacing spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); @@ -745,7 +787,24 @@ impl Prepared { }; if delta != 0.0 { - state.offset[d] += delta; + let target_offset = state.offset[d] + delta; + + if let Some(animation) = &mut state.offset_target[d] { + // For instance: the user is continuously calling `ui.scroll_to_cursor`, + // so we don't want to reset the animation, but perhaps update the target: + animation.target_offset = target_offset; + } else { + // The further we scroll, the more time we take. + // TODO(emilk): let users configure this in `Style`. + let now = ui.input(|i| i.time); + let points_per_second = 1000.0; + let animation_duration = + (delta.abs() / points_per_second).clamp(0.1, 0.3); + state.offset_target[d] = Some(ScrollTarget { + animation_time_span: (now, now + animation_duration as f64), + target_offset, + }); + } ui.ctx().request_repaint(); } } @@ -808,6 +867,7 @@ impl Prepared { }); state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } } } @@ -952,6 +1012,7 @@ impl Prepared { // some manual action taken, scroll not stuck state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } else { state.scroll_start_offset_from_top_left[d] = None; } diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 1b1417d14..ed29282e9 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -223,7 +223,7 @@ impl InputState { let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta; - let smooth_scroll_delta; + let mut smooth_scroll_delta = Vec2::ZERO; { // Mouse wheels often go very large steps. @@ -233,8 +233,15 @@ impl InputState { let dt = stable_dt.at_most(0.1); let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO: parameterize - smooth_scroll_delta = t * unprocessed_scroll_delta; - unprocessed_scroll_delta -= smooth_scroll_delta; + for d in 0..2 { + if unprocessed_scroll_delta[d].abs() < 1.0 { + smooth_scroll_delta[d] = unprocessed_scroll_delta[d]; + unprocessed_scroll_delta[d] = 0.0; + } else { + smooth_scroll_delta[d] = t * unprocessed_scroll_delta[d]; + unprocessed_scroll_delta[d] -= smooth_scroll_delta[d]; + } + } } let mut modifiers = new.modifiers; diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index e63def16c..2be552420 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -679,6 +679,7 @@ impl Response { /// Adjust the scroll position until this UI becomes visible. /// + /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc. /// If `align` is `None`, it'll scroll enough to bring the UI into view. /// /// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`]. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index b82160119..4e28ece40 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1002,6 +1002,7 @@ impl Ui { /// Adjust the scroll position of any parent [`ScrollArea`] so that the given [`Rect`] becomes visible. /// + /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc. /// If `align` is `None`, it'll scroll enough to bring the cursor into view. /// /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_cursor`]. [`Ui::scroll_with_delta`].. @@ -1028,6 +1029,7 @@ impl Ui { /// Adjust the scroll position of any parent [`ScrollArea`] so that the cursor (where the next widget goes) becomes visible. /// + /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc. /// If `align` is not provided, it'll scroll enough to bring the cursor into view. /// /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`]. diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 30855f77f..0530f2fee 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -252,6 +252,8 @@ impl super::View for ScrollTo { fn ui(&mut self, ui: &mut Ui) { ui.label("This shows how you can scroll to a specific item or pixel offset"); + let num_items = 500; + let mut track_item = false; let mut go_to_scroll_offset = false; let mut scroll_top = false; @@ -260,7 +262,7 @@ impl super::View for ScrollTo { ui.horizontal(|ui| { ui.label("Scroll to a specific item index:"); track_item |= ui - .add(Slider::new(&mut self.track_item, 1..=50).text("Track Item")) + .add(Slider::new(&mut self.track_item, 1..=num_items).text("Track Item")) .dragged(); }); @@ -304,7 +306,7 @@ impl super::View for ScrollTo { ui.scroll_to_cursor(Some(Align::TOP)); } ui.vertical(|ui| { - for item in 1..=50 { + for item in 1..=num_items { if track_item && item == self.track_item { let response = ui.colored_label(Color32::YELLOW, format!("This is item {item}")); diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index d77091667..911fb52a0 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -383,6 +383,52 @@ pub fn exponential_smooth_factor( 1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds) } +/// If you have a value animating over time, +/// how much towards its target do you need to move it this frame? +/// +/// You only need to store the start time and target value in order to animate using this function. +/// +/// ``` rs +/// struct Animation { +/// current_value: f32, +/// +/// animation_time_span: (f64, f64), +/// target_value: f32, +/// } +/// +/// impl Animation { +/// fn update(&mut self, now: f64, dt: f32) { +/// let t = interpolation_factor(self.animation_time_span, now, dt, ease_in_ease_out); +/// self.current_value = emath::lerp(self.current_value..=self.target_value, t); +/// } +/// } +/// ``` +pub fn interpolation_factor( + (start_time, end_time): (f64, f64), + current_time: f64, + dt: f32, + easing: impl Fn(f32) -> f32, +) -> f32 { + let animation_duration = (end_time - start_time) as f32; + let prev_time = current_time - dt as f64; + let prev_t = easing((prev_time - start_time) as f32 / animation_duration); + let end_t = easing((current_time - start_time) as f32 / animation_duration); + if end_t < 1.0 { + (end_t - prev_t) / (1.0 - prev_t) + } else { + 1.0 + } +} + +/// Ease in, ease out. +/// +/// `f(0) = 0, f'(0) = 0, f(1) = 1, f'(1) = 0`. +#[inline] +pub fn ease_in_ease_out(t: f32) -> f32 { + let t = t.clamp(0.0, 1.0); + (3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0) +} + // ---------------------------------------------------------------------------- /// An assert that is only active when `emath` is compiled with the `extra_asserts` feature From ca1f453c47e01dda08e70802ded21d1c760584c9 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Mon, 4 Mar 2024 13:20:03 -0600 Subject: [PATCH 14/27] add webgpu feature by default for wgpu feature of eframe (#4124) Fix #4123. Looks like it was caused by #3875. --- crates/eframe/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 092e332c7..da1aea6ba 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -239,4 +239,8 @@ web-sys = { version = "0.3.58", features = [ # optional web: egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit -wgpu = { workspace = true, optional = true } +wgpu = { workspace = true, optional = true, features = [ + # Let's enable some backends so that users can use `eframe` out-of-the-box + # without having to explicitly opt-in to backends + "webgpu", +] } From a1d5145c16aba4d0b11668d496735d07520d0339 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:30:20 +0100 Subject: [PATCH 15/27] Hide hover UI when showing the context menu (#4138) This PR hides the hover UI for a given widget whenever a corresponding context menu is opened. Fixes: - https://github.com/rerun-io/rerun/issues/5310 --- crates/egui/src/menu.rs | 7 +++++++ crates/egui/src/response.rs | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index ad9d84b94..67689ca9d 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -230,6 +230,13 @@ pub(crate) fn context_menu( inner_response } +/// Returns `true` if the context menu is opened for this widget. +pub(crate) fn context_menu_opened(response: &Response) -> bool { + let menu_id = Id::new(CONTEXT_MENU_ID_STR); + let bar_state = BarState::load(&response.ctx, menu_id); + bar_state.is_menu_open(response.id) +} + /// Stores the state for the context menu. #[derive(Clone, Default)] pub(crate) struct MenuRootManager { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 2be552420..70c82ac31 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -534,6 +534,10 @@ impl Response { return true; } + if self.context_menu_opened() { + return false; + } + if self.enabled { if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) { return false; @@ -849,6 +853,13 @@ impl Response { menu::context_menu(self, add_contents) } + /// Returns whether a context menu is currently open for this widget. + /// + /// See [`Self::context_menu`]. + pub fn context_menu_opened(&self) -> bool { + menu::context_menu_opened(self) + } + /// Draw a debug rectangle over the response displaying the response's id and whether it is /// enabled and/or hovered. /// From 76411b5d74aa980395fd8c24e072fa300879d539 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Fri, 8 Mar 2024 09:54:21 +0100 Subject: [PATCH 16/27] Add accessibility to `ProgressBar` and `Spinner` (#4139) - Introduces `WidgetType::ProgressIndicator` and maps it to the corresponding AccessKit role. - A `Spinner` is now exposed as a widget indicating a progress for which a completion state is not known. - On the other hand, a `ProgressBar` reports a completion state and can possibly be labeled. Note that a label is not used if not explicitly asked by the user, as it would be redundant information. Assistive technologies prefer the numerical value so they can, for instance, emit beeps of which the frequency rise as the completion state increase. I had to call `floor` on the progression as it seems all the ATs I tested would round the value, hence reporting something different than what is displayed on the label. --- crates/egui/src/data/output.rs | 1 + crates/egui/src/lib.rs | 2 ++ crates/egui/src/response.rs | 1 + crates/egui/src/widgets/progress_bar.rs | 11 +++++++++++ crates/egui/src/widgets/spinner.rs | 3 ++- 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 9960d5159..d1db5fc2a 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -633,6 +633,7 @@ impl WidgetInfo { WidgetType::ColorButton => "color button", WidgetType::ImageButton => "image button", WidgetType::CollapsingHeader => "collapsing header", + WidgetType::ProgressIndicator => "progress indicator", WidgetType::Label | WidgetType::Other => "", }; diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 9c065811d..693280453 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -643,6 +643,8 @@ pub enum WidgetType { CollapsingHeader, + ProgressIndicator, + /// If you cannot fit any of the above slots. /// /// If this is something you think should be added, file an issue. diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 70c82ac31..a34ed8fa7 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -783,6 +783,7 @@ impl Response { WidgetType::Slider => Role::Slider, WidgetType::DragValue => Role::SpinButton, WidgetType::ColorButton => Role::ColorWell, + WidgetType::ProgressIndicator => Role::ProgressIndicator, WidgetType::Other => Role::Unknown, }); if let Some(label) = info.label { diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index cd00bd740..99c768bf9 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -113,6 +113,17 @@ impl Widget for ProgressBar { let (outer_rect, response) = ui.allocate_exact_size(vec2(desired_width, height), Sense::hover()); + response.widget_info(|| { + let mut info = if let Some(ProgressBarText::Custom(text)) = &text { + WidgetInfo::labeled(WidgetType::ProgressIndicator, text.text()) + } else { + WidgetInfo::new(WidgetType::ProgressIndicator) + }; + info.value = Some((progress as f64 * 100.0).floor()); + + info + }); + if ui.is_rect_visible(response.rect) { if animate { ui.ctx().request_repaint(); diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index 0327bfab1..f18842844 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -1,6 +1,6 @@ use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke}; -use crate::{Response, Sense, Ui, Widget}; +use crate::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType}; /// A spinner widget used to indicate loading. /// @@ -66,6 +66,7 @@ impl Widget for Spinner { .size .unwrap_or_else(|| ui.style().spacing.interact_size.y); let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); + response.widget_info(|| WidgetInfo::new(WidgetType::ProgressIndicator)); self.paint_at(ui, rect); response From 385daeb354fe10dc0bf968c760b9be7437684d49 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Fri, 8 Mar 2024 18:00:28 +0900 Subject: [PATCH 17/27] Fix IME bug where currently typed characters got copied * Closes #3730 Fix IME: Currently typed characters get copied when switching TextEdit fields --- crates/egui/src/widgets/text_edit/builder.rs | 11 ++++++++--- crates/egui/src/widgets/text_edit/state.rs | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 22a5623f0..12077b932 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -940,6 +940,7 @@ fn events( if !text_mark.is_empty() { text.insert_text_at(&mut ccursor, text_mark, char_limit); } + state.ime_cursor_range = cursor_range; Some(CCursorRange::two(start_cursor, ccursor)) } else { None @@ -947,12 +948,16 @@ fn events( } Event::CompositionEnd(prediction) => { - // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, so do not check `state.has_ime = true` in the following statement. + // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, + // so do not check `state.has_ime = true` in the following statement. if prediction != "\n" && prediction != "\r" { state.has_ime = false; - let mut ccursor = text.delete_selected(&cursor_range); - if !prediction.is_empty() { + let mut ccursor; + if !prediction.is_empty() && cursor_range == state.ime_cursor_range { + ccursor = text.delete_selected(&cursor_range); text.insert_text_at(&mut ccursor, prediction, char_limit); + } else { + ccursor = cursor_range.primary.ccursor; } Some(CCursorRange::one(ccursor)) } else { diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 819688289..8901fd56f 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -44,6 +44,10 @@ pub struct TextEditState { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) has_ime: bool, + // cursor range for IME candidate. + #[cfg_attr(feature = "serde", serde(skip))] + pub(crate) ime_cursor_range: CursorRange, + // Visual offset when editing singleline text bigger than the width. #[cfg_attr(feature = "serde", serde(skip))] pub(crate) singleline_offset: f32, From 4d776fd84e647632aa81600ab06644402db12270 Mon Sep 17 00:00:00 2001 From: Varphone Wong Date: Fri, 8 Mar 2024 17:07:31 +0800 Subject: [PATCH 18/27] `epaint`: Added `Shape::{scale,translate}` wrappers (#4090) The `Shape::translate` method has been replaced with `Shape::transform`, which introduces breaking changes that could negatively impact existing users. This patch adds a `Shape::translate` wrapper to prevent these breaking changes. --- crates/epaint/src/shape.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 033d4dfc5..89048ac48 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -355,6 +355,22 @@ impl Shape { } } + /// Scale the shape by `factor`, in-place. + /// + /// A wrapper around [`Self::transform`]. + #[inline(always)] + pub fn scale(&mut self, factor: f32) { + self.transform(TSTransform::from_scaling(factor)); + } + + /// Move the shape by `delta`, in-place. + /// + /// A wrapper around [`Self::transform`]. + #[inline(always)] + pub fn translate(&mut self, delta: Vec2) { + self.transform(TSTransform::from_translation(delta)); + } + /// Move the shape by this many points, in-place. /// /// If using a [`PaintCallback`], note that only the rect is scaled as opposed From 1f414c059d9e873fec0010932bf6d79f5e5116a9 Mon Sep 17 00:00:00 2001 From: Varphone Wong Date: Fri, 8 Mar 2024 17:24:34 +0800 Subject: [PATCH 19/27] `egui_extras`: Fixed handling of `file://` protocol for images (#4107) * Remove the leading slash from the path if the target OS is Windows. This is because Windows paths are not supposed to start with a slash. For example, `file:///C:/path/to/file` is a valid URI, but `/C:/path/to/file` is not a valid path. * Use the input URI consistently as the cache key. Currently, the cache key is inconsistently set as either the path or the URI, while the forget key is always the URI. This inconsistency should be resolved. --------- Co-authored-by: Emil Ernerfeldt --- crates/egui_extras/src/loaders/file_loader.rs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 10bfec539..afa7d0eef 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -25,6 +25,19 @@ impl FileLoader { const PROTOCOL: &str = "file://"; +/// Remove the leading slash from the path if the target OS is Windows. +/// +/// This is because Windows paths are not supposed to start with a slash. +/// For example, `file:///C:/path/to/file` is a valid URI, but `/C:/path/to/file` is not a valid path. +#[inline] +fn trim_extra_slash(s: &str) -> &str { + if cfg!(target_os = "windows") { + s.trim_start_matches('/') + } else { + s + } +} + impl BytesLoader for FileLoader { fn id(&self) -> &str { Self::ID @@ -32,12 +45,12 @@ impl BytesLoader for FileLoader { fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult { // File loader only supports the `file` protocol. - let Some(path) = uri.strip_prefix(PROTOCOL) else { + let Some(path) = uri.strip_prefix(PROTOCOL).map(trim_extra_slash) else { return Err(LoadError::NotSupported); }; let mut cache = self.cache.lock(); - if let Some(entry) = cache.get(path).cloned() { + if let Some(entry) = cache.get(uri).cloned() { // `path` has either begun loading, is loaded, or has failed to load. match entry { Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready { @@ -54,7 +67,7 @@ impl BytesLoader for FileLoader { // Set the file to `pending` until we finish loading it. let path = path.to_owned(); - cache.insert(path.clone(), Poll::Pending); + cache.insert(uri.to_owned(), Poll::Pending); drop(cache); // Spawn a thread to read the file, so that we don't block the render for too long. @@ -63,7 +76,7 @@ impl BytesLoader for FileLoader { .spawn({ let ctx = ctx.clone(); let cache = self.cache.clone(); - let _uri = uri.to_owned(); + let uri = uri.to_owned(); move || { let result = match std::fs::read(&path) { Ok(bytes) => { @@ -82,10 +95,10 @@ impl BytesLoader for FileLoader { } Err(err) => Err(err.to_string()), }; - let prev = cache.lock().insert(path, Poll::Ready(result)); + let prev = cache.lock().insert(uri.clone(), Poll::Ready(result)); assert!(matches!(prev, Some(Poll::Pending))); ctx.request_repaint(); - log::trace!("finished loading {_uri:?}"); + log::trace!("finished loading {uri:?}"); } }) .expect("failed to spawn thread"); From a93c6cd5d25ef966dca805073eb6f2e33f3075a2 Mon Sep 17 00:00:00 2001 From: Varphone Wong Date: Fri, 8 Mar 2024 17:32:23 +0800 Subject: [PATCH 20/27] `egui`: Fixed the incorrect display of the Window frame with a wide border or large rounding (#4032) Currently, the Window frame is displayed incorrectly when using a wide border or large rounding. * Closes #3806 * Closes #4024 * Closes #4025 * Screencast of egui demo app (emilk:master) [window-frame-bug.webm](https://github.com/emilk/egui/assets/1274171/391f67fa-ae6f-445a-8c64-1bb575770127) * Screencast of egui demo app (varphone:hotfix/window-custom-frame) [window-frame-fixed.webm](https://github.com/emilk/egui/assets/1274171/1953124e-9f7a-4c2d-9024-5d2eece6b87c) --- crates/egui/src/containers/resize.rs | 9 +- crates/egui/src/containers/window.rs | 76 +++++++++++----- crates/egui/src/painter.rs | 6 ++ crates/egui/src/style.rs | 110 ++++++++++++++++++++++++ crates/epaint/src/shape.rs | 124 +++++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 25 deletions(-) diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 22286368b..ef0d045de 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -369,19 +369,22 @@ use epaint::Stroke; pub fn paint_resize_corner(ui: &Ui, response: &Response) { let stroke = ui.style().interact(response).fg_stroke; - paint_resize_corner_with_style(ui, &response.rect, stroke, Align2::RIGHT_BOTTOM); + paint_resize_corner_with_style(ui, &response.rect, stroke.color, Align2::RIGHT_BOTTOM); } pub fn paint_resize_corner_with_style( ui: &Ui, rect: &Rect, - stroke: impl Into, + color: impl Into, corner: Align2, ) { let painter = ui.painter(); let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect)); let mut w = 2.0; - let stroke = stroke.into(); + let stroke = Stroke { + width: 1.0, // Set width to 1.0 to prevent overlapping + color: color.into(), + }; while w <= rect.width() && w <= rect.height() { painter.line_segment( diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 8cf5475eb..eaea0f5ac 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -392,7 +392,12 @@ impl<'open> Window<'open> { let header_color = frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill); - let window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); + let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); + // Keep the original inner margin for later use + let window_margin = window_frame.inner_margin; + let border_padding = window_frame.stroke.width / 2.0; + // Add border padding to the inner margin to prevent it from covering the contents + window_frame.inner_margin += border_padding; let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -420,9 +425,10 @@ impl<'open> Window<'open> { // Calculate roughly how much larger the window size is compared to the inner rect let (title_bar_height, title_content_spacing) = if with_title_bar { let style = ctx.style(); - let window_margin = window_frame.inner_margin; let spacing = window_margin.top + window_margin.bottom; let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing; + window_frame.rounding.ne = window_frame.rounding.ne.clamp(0.0, height / 2.0); + window_frame.rounding.nw = window_frame.rounding.nw.clamp(0.0, height / 2.0); (height, spacing) } else { (0.0, 0.0) @@ -495,21 +501,33 @@ impl<'open> Window<'open> { .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); let outer_rect = frame.end(&mut area_content_ui).rect; - paint_resize_corner(&area_content_ui, &possible, outer_rect, frame_stroke); + paint_resize_corner( + &area_content_ui, + &possible, + outer_rect, + frame_stroke, + window_frame.rounding, + ); // END FRAME -------------------------------- if let Some(title_bar) = title_bar { - if on_top && area_content_ui.visuals().window_highlight_topmost { - let rect = Rect::from_min_size( - outer_rect.min, - Vec2 { - x: outer_rect.size().x, - y: title_bar_height, - }, - ); + let mut title_rect = Rect::from_min_size( + outer_rect.min + vec2(border_padding, border_padding), + Vec2 { + x: outer_rect.size().x - border_padding * 2.0, + y: title_bar_height, + }, + ); + title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect); + + if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = window_frame.rounding; + + // Eliminate the rounding gap between the title bar and the window frame + round -= border_padding; + if !is_collapsed { round.se = 0.0; round.sw = 0.0; @@ -517,18 +535,18 @@ impl<'open> Window<'open> { area_content_ui.painter().set( *where_to_put_header_background, - RectShape::filled(rect, round, header_color), + RectShape::filled(title_rect, round, header_color), ); }; // Fix title bar separator line position if let Some(response) = &mut content_response { - response.rect.min.y = outer_rect.min.y + title_bar_height; + response.rect.min.y = outer_rect.min.y + title_bar_height + border_padding; } title_bar.ui( &mut area_content_ui, - outer_rect, + title_rect, &content_response, open, &mut collapsing, @@ -558,23 +576,34 @@ fn paint_resize_corner( possible: &PossibleInteractions, outer_rect: Rect, stroke: impl Into, + rounding: impl Into, ) { - let corner = if possible.resize_right && possible.resize_bottom { - Align2::RIGHT_BOTTOM + let stroke = stroke.into(); + let rounding = rounding.into(); + let (corner, radius) = if possible.resize_right && possible.resize_bottom { + (Align2::RIGHT_BOTTOM, rounding.se) } else if possible.resize_left && possible.resize_bottom { - Align2::LEFT_BOTTOM + (Align2::LEFT_BOTTOM, rounding.sw) } else if possible.resize_left && possible.resize_top { - Align2::LEFT_TOP + (Align2::LEFT_TOP, rounding.nw) } else if possible.resize_right && possible.resize_top { - Align2::RIGHT_TOP + (Align2::RIGHT_TOP, rounding.ne) } else { return; }; + // Adjust the corner offset to accommodate the stroke width and window rounding + let offset = if radius <= 2.0 && stroke.width < 2.0 { + 2.0 + } else { + // The corner offset is calculated to make the corner appear to be in the correct position + (2.0_f32.sqrt() * (1.0 + radius + stroke.width / 2.0) - radius) + * 45.0_f32.to_radians().cos() + }; let corner_size = Vec2::splat(ui.visuals().resize_corner_size); let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); - let corner_rect = corner_rect.translate(-2.0 * corner.to_sign()); // move away from corner - crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke, corner); + let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner + crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner); } // ---------------------------------------------------------------------------- @@ -1036,7 +1065,10 @@ impl TitleBar { let y = content_response.rect.top(); // let y = lerp(self.rect.bottom()..=content_response.rect.top(), 0.5); let stroke = ui.visuals().widgets.noninteractive.bg_stroke; - ui.painter().hline(outer_rect.x_range(), y, stroke); + // Workaround: To prevent border infringement, + // the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels + let x_range = outer_rect.x_range().shrink(0.1); + ui.painter().hline(x_range, y, stroke); } // Don't cover the close- and collapse buttons: diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 8689ec815..788319dc9 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -152,6 +152,12 @@ impl Painter { pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { self.ctx().round_pos_to_pixels(pos) } + + /// Useful for pixel-perfect rendering. + #[inline] + pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect { + self.ctx().round_rect_to_pixels(rect) + } } /// ## Low level diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 6b77e8a14..a5b19e14d 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -702,6 +702,116 @@ impl std::ops::Add for Margin { } } +impl std::ops::Add for Margin { + type Output = Self; + + #[inline] + fn add(self, v: f32) -> Self { + Self { + left: self.left + v, + right: self.right + v, + top: self.top + v, + bottom: self.bottom + v, + } + } +} + +impl std::ops::AddAssign for Margin { + #[inline] + fn add_assign(&mut self, v: f32) { + self.left += v; + self.right += v; + self.top += v; + self.bottom += v; + } +} + +impl std::ops::Div for Margin { + type Output = Self; + + #[inline] + fn div(self, v: f32) -> Self { + Self { + left: self.left / v, + right: self.right / v, + top: self.top / v, + bottom: self.bottom / v, + } + } +} + +impl std::ops::DivAssign for Margin { + #[inline] + fn div_assign(&mut self, v: f32) { + self.left /= v; + self.right /= v; + self.top /= v; + self.bottom /= v; + } +} + +impl std::ops::Mul for Margin { + type Output = Self; + + #[inline] + fn mul(self, v: f32) -> Self { + Self { + left: self.left * v, + right: self.right * v, + top: self.top * v, + bottom: self.bottom * v, + } + } +} + +impl std::ops::MulAssign for Margin { + #[inline] + fn mul_assign(&mut self, v: f32) { + self.left *= v; + self.right *= v; + self.top *= v; + self.bottom *= v; + } +} + +impl std::ops::Sub for Margin { + type Output = Self; + + #[inline] + fn sub(self, other: Self) -> Self { + Self { + left: self.left - other.left, + right: self.right - other.right, + top: self.top - other.top, + bottom: self.bottom - other.bottom, + } + } +} + +impl std::ops::Sub for Margin { + type Output = Self; + + #[inline] + fn sub(self, v: f32) -> Self { + Self { + left: self.left - v, + right: self.right - v, + top: self.top - v, + bottom: self.bottom - v, + } + } +} + +impl std::ops::SubAssign for Margin { + #[inline] + fn sub_assign(&mut self, v: f32) { + self.left -= v; + self.right -= v; + self.top -= v; + self.bottom -= v; + } +} + // ---------------------------------------------------------------------------- /// How and when interaction happens. diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 89048ac48..0aeec5525 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -755,6 +755,130 @@ impl Rounding { } } +impl std::ops::Add for Rounding { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self { + nw: self.nw + rhs.nw, + ne: self.ne + rhs.ne, + sw: self.sw + rhs.sw, + se: self.se + rhs.se, + } + } +} + +impl std::ops::AddAssign for Rounding { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = Self { + nw: self.nw + rhs.nw, + ne: self.ne + rhs.ne, + sw: self.sw + rhs.sw, + se: self.se + rhs.se, + }; + } +} + +impl std::ops::AddAssign for Rounding { + #[inline] + fn add_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw + rhs, + ne: self.ne + rhs, + sw: self.sw + rhs, + se: self.se + rhs, + }; + } +} + +impl std::ops::Sub for Rounding { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self { + nw: self.nw - rhs.nw, + ne: self.ne - rhs.ne, + sw: self.sw - rhs.sw, + se: self.se - rhs.se, + } + } +} + +impl std::ops::SubAssign for Rounding { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = Self { + nw: self.nw - rhs.nw, + ne: self.ne - rhs.ne, + sw: self.sw - rhs.sw, + se: self.se - rhs.se, + }; + } +} + +impl std::ops::SubAssign for Rounding { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw - rhs, + ne: self.ne - rhs, + sw: self.sw - rhs, + se: self.se - rhs, + }; + } +} + +impl std::ops::Div for Rounding { + type Output = Self; + #[inline] + fn div(self, rhs: f32) -> Self { + Self { + nw: self.nw / rhs, + ne: self.ne / rhs, + sw: self.sw / rhs, + se: self.se / rhs, + } + } +} + +impl std::ops::DivAssign for Rounding { + #[inline] + fn div_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw / rhs, + ne: self.ne / rhs, + sw: self.sw / rhs, + se: self.se / rhs, + }; + } +} + +impl std::ops::Mul for Rounding { + type Output = Self; + #[inline] + fn mul(self, rhs: f32) -> Self { + Self { + nw: self.nw * rhs, + ne: self.ne * rhs, + sw: self.sw * rhs, + se: self.se * rhs, + } + } +} + +impl std::ops::MulAssign for Rounding { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw * rhs, + ne: self.ne * rhs, + sw: self.sw * rhs, + se: self.se * rhs, + }; + } +} + // ---------------------------------------------------------------------------- /// How to paint some text on screen. From 00a399b2f7eae6bcbe968bdaa7bb77689043bba2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 11 Mar 2024 09:29:48 +0100 Subject: [PATCH 21/27] A `Window` can now be resizable in only one direction (#4155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For instance: `Window::new(…).resizable([true, false])` is a window that is only resizable in the horizontal direction. This PR also removes a hack added in https://github.com/emilk/egui/pull/3039 which is no longer needed since https://github.com/emilk/egui/pull/4026 --- crates/egui/src/containers/area.rs | 17 ------- crates/egui/src/containers/resize.rs | 38 +++++++++------- crates/egui/src/containers/window.rs | 44 ++++++++++++------- crates/egui/src/context.rs | 5 +-- crates/egui/src/memory.rs | 11 +---- crates/egui_demo_lib/src/demo/about.rs | 1 + crates/egui_demo_lib/src/demo/code_example.rs | 1 + .../egui_demo_lib/src/demo/widget_gallery.rs | 2 +- crates/emath/src/vec2.rs | 12 +++++ crates/emath/src/vec2b.rs | 24 ++++++++++ 10 files changed, 91 insertions(+), 64 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 7a9111f64..3c6b711be 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -20,10 +20,6 @@ pub(crate) struct State { /// If false, clicks goes straight through to what is behind us. /// Good for tooltips etc. pub interactable: bool, - - /// When `true`, this `Area` belongs to a resizable window, so it needs to - /// receive mouse input which occurs a short distance beyond its bounding rect. - pub edges_padded_for_resize: bool, } impl State { @@ -75,7 +71,6 @@ pub struct Area { pivot: Align2, anchor: Option<(Align2, Vec2)>, new_pos: Option, - edges_padded_for_resize: bool, } impl Area { @@ -93,7 +88,6 @@ impl Area { new_pos: None, pivot: Align2::LEFT_TOP, anchor: None, - edges_padded_for_resize: false, } } @@ -227,14 +221,6 @@ impl Area { Align2::LEFT_TOP } } - - /// When `true`, this `Area` belongs to a resizable window, so it needs to - /// receive mouse input which occurs a short distance beyond its bounding rect. - #[inline] - pub(crate) fn edges_padded_for_resize(mut self, edges_padded_for_resize: bool) -> Self { - self.edges_padded_for_resize = edges_padded_for_resize; - self - } } pub(crate) struct Prepared { @@ -279,7 +265,6 @@ impl Area { anchor, constrain, constrain_rect, - edges_padded_for_resize, } = self; let layer_id = LayerId::new(order, id); @@ -300,11 +285,9 @@ impl Area { pivot, size: Vec2::ZERO, interactable, - edges_padded_for_resize, }); state.pivot_pos = new_pos.unwrap_or(state.pivot_pos); state.interactable = interactable; - state.edges_padded_for_resize = edges_padded_for_resize; if let Some((anchor, offset)) = anchor { let screen = ctx.available_rect(); diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index ef0d045de..e2654ca5a 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -34,7 +34,7 @@ pub struct Resize { id_source: Option, /// If false, we are no enabled - resizable: bool, + resizable: Vec2b, pub(crate) min_size: Vec2, pub(crate) max_size: Vec2, @@ -49,7 +49,7 @@ impl Default for Resize { Self { id: None, id_source: None, - resizable: true, + resizable: Vec2b::TRUE, min_size: Vec2::splat(16.0), max_size: Vec2::splat(f32::INFINITY), default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area. @@ -152,12 +152,13 @@ impl Resize { /// /// Default is `true`. #[inline] - pub fn resizable(mut self, resizable: bool) -> Self { - self.resizable = resizable; + pub fn resizable(mut self, resizable: impl Into) -> Self { + self.resizable = resizable.into(); self } - pub fn is_resizable(&self) -> bool { + #[inline] + pub fn is_resizable(&self) -> Vec2b { self.resizable } @@ -175,7 +176,7 @@ impl Resize { self.default_size = size; self.min_size = size; self.max_size = size; - self.resizable = false; + self.resizable = Vec2b::FALSE; self } @@ -226,7 +227,7 @@ impl Resize { let mut user_requested_size = state.requested_size.take(); - let corner_id = self.resizable.then(|| id.with("__resize_corner")); + let corner_id = self.resizable.any().then(|| id.with("__resize_corner")); if let Some(corner_id) = corner_id { if let Some(corner_response) = ui.ctx().read_response(corner_id) { @@ -299,18 +300,21 @@ impl Resize { // ------------------------------ - let size = if self.with_stroke || self.resizable { - // We show how large we are, - // so we must follow the contents: + let mut size = state.last_content_size; + for d in 0..2 { + if self.with_stroke || self.resizable[d] { + // We show how large we are, + // so we must follow the contents: - state.desired_size = state.desired_size.max(state.last_content_size); + state.desired_size[d] = state.desired_size[d].max(state.last_content_size[d]); - // We are as large as we look - state.desired_size - } else { - // Probably a window. - state.last_content_size - }; + // We are as large as we look + size[d] = state.desired_size[d]; + } else { + // Probably a window. + size[d] = state.last_content_size[d]; + } + } ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size)); // ------------------------------ diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index eaea0f5ac..698033cf9 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -48,9 +48,7 @@ impl<'open> Window<'open> { /// If you need a changing title, you must call `window.id(…)` with a fixed id. pub fn new(title: impl Into) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading); - let area = Area::new(Id::new(title.text())) - .constrain(true) - .edges_padded_for_resize(true); + let area = Area::new(Id::new(title.text())).constrain(true); Self { title, open: None, @@ -119,9 +117,6 @@ impl<'open> Window<'open> { #[inline] pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self { self.resize = mutate(self.resize); - self.area = self - .area - .edges_padded_for_resize(self.resize.is_resizable()); self } @@ -278,7 +273,6 @@ impl<'open> Window<'open> { #[inline] pub fn fixed_size(mut self, size: impl Into) -> Self { self.resize = self.resize.fixed_size(size); - self.area = self.area.edges_padded_for_resize(false); self } @@ -296,11 +290,15 @@ impl<'open> Window<'open> { /// /// Note that even if you set this to `false` the window may still auto-resize. /// + /// You can set the window to only be resizable in one direction by using + /// e.g. `[true, false]` as the argument, + /// making the window only resizable in the x-direction. + /// /// Default is `true`. #[inline] - pub fn resizable(mut self, resizable: bool) -> Self { + pub fn resizable(mut self, resizable: impl Into) -> Self { + let resizable = resizable.into(); self.resize = self.resize.resizable(resizable); - self.area = self.area.edges_padded_for_resize(resizable); self } @@ -326,7 +324,6 @@ impl<'open> Window<'open> { pub fn auto_sized(mut self) -> Self { self.resize = self.resize.auto_sized(); self.scroll = ScrollArea::neither(); - self.area = self.area.edges_padded_for_resize(false); self } @@ -589,7 +586,20 @@ fn paint_resize_corner( } else if possible.resize_right && possible.resize_top { (Align2::RIGHT_TOP, rounding.ne) } else { - return; + // We're not in two directions, but it is still nice to tell the user + // we're resizable by painting the resize corner in the expected place + // (i.e. for windows only resizable in one direction): + if possible.resize_right || possible.resize_bottom { + (Align2::RIGHT_BOTTOM, rounding.se) + } else if possible.resize_left || possible.resize_bottom { + (Align2::LEFT_BOTTOM, rounding.sw) + } else if possible.resize_left || possible.resize_top { + (Align2::LEFT_TOP, rounding.nw) + } else if possible.resize_right || possible.resize_top { + (Align2::RIGHT_TOP, rounding.ne) + } else { + return; + } }; // Adjust the corner offset to accommodate the stroke width and window rounding @@ -621,13 +631,15 @@ struct PossibleInteractions { impl PossibleInteractions { fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self { let movable = area.is_enabled() && area.is_movable(); - let resizable = area.is_enabled() && resize.is_resizable() && !is_collapsed; + let resizable = resize + .is_resizable() + .and(area.is_enabled() && !is_collapsed); let pivot = area.get_pivot(); Self { - resize_left: resizable && (movable || pivot.x() != Align::LEFT), - resize_right: resizable && (movable || pivot.x() != Align::RIGHT), - resize_top: resizable && (movable || pivot.y() != Align::TOP), - resize_bottom: resizable && (movable || pivot.y() != Align::BOTTOM), + resize_left: resizable.x && (movable || pivot.x() != Align::LEFT), + resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT), + resize_top: resizable.y && (movable || pivot.y() != Align::TOP), + resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM), } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 996f7a7ec..8e09414a6 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -496,7 +496,6 @@ impl ContextImpl { pivot: Align2::LEFT_TOP, size: screen_rect.size(), interactable: true, - edges_padded_for_resize: false, }, ); @@ -2331,9 +2330,7 @@ impl Context { /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2) -> Option { - self.memory(|mem| { - mem.layer_id_at(pos, mem.options.style.interaction.resize_grab_radius_side) - }) + self.memory(|mem| mem.layer_id_at(pos)) } /// Moves the given area to the top in its [`Order`]. diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index fcfe0accb..fb18258eb 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -646,9 +646,8 @@ impl Memory { } /// Top-most layer at the given position. - pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option { - self.areas() - .layer_id_at(pos, resize_interact_radius_side, &self.layer_transforms) + pub fn layer_id_at(&self, pos: Pos2) -> Option { + self.areas().layer_id_at(pos, &self.layer_transforms) } /// An iterator over all layers. Back-to-front. Top is last. @@ -921,7 +920,6 @@ impl Areas { pub fn layer_id_at( &self, pos: Pos2, - resize_interact_radius_side: f32, layer_transforms: &HashMap, ) -> Option { for layer in self.order.iter().rev() { @@ -929,11 +927,6 @@ impl Areas { if let Some(state) = self.areas.get(&layer.id) { let mut rect = state.rect(); if state.interactable { - if state.edges_padded_for_resize { - // Allow us to resize by dragging just outside the window: - rect = rect.expand(resize_interact_radius_side); - } - if let Some(transform) = layer_transforms.get(layer) { rect = *transform * rect; } diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 56e5c9020..f3143e3c7 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -13,6 +13,7 @@ impl super::Demo for About { .default_width(320.0) .default_height(480.0) .open(open) + .resizable([true, false]) .show(ctx, |ui| { use super::View as _; self.ui(ui); diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index b4adf8fb5..6786e55a1 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -75,6 +75,7 @@ impl super::Demo for CodeExample { .default_size([800.0, 400.0]) .vscroll(false) .hscroll(true) + .resizable([true, false]) .show(ctx, |ui| self.ui(ui)); } } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index ac55a4d01..d68af17db 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -50,7 +50,7 @@ impl super::Demo for WidgetGallery { fn show(&mut self, ctx: &egui::Context, open: &mut bool) { egui::Window::new(self.name()) .open(open) - .resizable(true) + .resizable([true, false]) .default_width(280.0) .show(ctx, |ui| { use super::View as _; diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 2c5ceeedf..0cab00fe6 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -1,5 +1,7 @@ use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use crate::Vec2b; + /// A vector has a direction and length. /// A [`Vec2`] is often used to represent a size. /// @@ -86,6 +88,16 @@ impl From<&Vec2> for (f32, f32) { } } +impl From for Vec2 { + #[inline(always)] + fn from(v: Vec2b) -> Self { + Self { + x: v.x as i32 as f32, + y: v.y as i32 as f32, + } + } +} + // ---------------------------------------------------------------------------- // Mint compatibility and convenience conversions diff --git a/crates/emath/src/vec2b.rs b/crates/emath/src/vec2b.rs index dc5eb0ea5..f241de64e 100644 --- a/crates/emath/src/vec2b.rs +++ b/crates/emath/src/vec2b.rs @@ -19,6 +19,30 @@ impl Vec2b { pub fn any(&self) -> bool { self.x || self.y } + + /// Are both `x` and `y` true? + #[inline] + pub fn all(&self) -> bool { + self.x && self.y + } + + #[inline] + pub fn and(&self, other: impl Into) -> Self { + let other = other.into(); + Self { + x: self.x && other.x, + y: self.y && other.y, + } + } + + #[inline] + pub fn or(&self, other: impl Into) -> Self { + let other = other.into(); + Self { + x: self.x || other.x, + y: self.y || other.y, + } + } } impl From for Vec2b { From 827fdefd8343f0e92f203d7dfafa34cb5cf91cc7 Mon Sep 17 00:00:00 2001 From: Varphone Wong Date: Tue, 12 Mar 2024 18:06:08 +0800 Subject: [PATCH 22/27] eframe: Added `App::raw_input_hook` allows for the manipulation or filtering of raw input events (#4008) # What's New * eframe: Added `App::raw_input_hook` allows for the manipulation or filtering of raw input events A filter applied to raw input before [`Self::update`] This allows for the manipulation or filtering of input events before they are processed by egui. This can be used to exclude specific keyboard shortcuts, mouse events, etc. Additionally, it can be used to add custom keyboard or mouse events generated by a virtual keyboard. * examples: Added an example to demonstrates how to implement a custom virtual keyboard. [eframe-custom-keypad.webm](https://github.com/emilk/egui/assets/1274171/a9dc8e34-2c35-4172-b7ef-41010b794fb8) --- Cargo.lock | 9 + crates/eframe/src/epi.rs | 18 ++ crates/eframe/src/native/epi_integration.rs | 2 + examples/custom_keypad/Cargo.toml | 23 ++ examples/custom_keypad/README.md | 7 + examples/custom_keypad/screenshot.png | Bin 0 -> 39832 bytes examples/custom_keypad/src/keypad.rs | 255 ++++++++++++++++++++ examples/custom_keypad/src/main.rs | 68 ++++++ 8 files changed, 382 insertions(+) create mode 100644 examples/custom_keypad/Cargo.toml create mode 100644 examples/custom_keypad/README.md create mode 100644 examples/custom_keypad/screenshot.png create mode 100644 examples/custom_keypad/src/keypad.rs create mode 100644 examples/custom_keypad/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 92948c458..ba8d5a17a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1072,6 +1072,15 @@ dependencies = [ "env_logger", ] +[[package]] +name = "custom_keypad" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_extras", + "env_logger", +] + [[package]] name = "custom_plot_manipulation" version = "0.1.0" diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index e26bece3a..16a8a6891 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -196,6 +196,24 @@ pub trait App { fn persist_egui_memory(&self) -> bool { true } + + /// A hook for manipulating or filtering raw input before it is processed by [`Self::update`]. + /// + /// This function provides a way to modify or filter input events before they are processed by egui. + /// + /// It can be used to prevent specific keyboard shortcuts or mouse events from being processed by egui. + /// + /// Additionally, it can be used to inject custom keyboard or mouse events into the input stream, which can be useful for implementing features like a virtual keyboard. + /// + /// # Arguments + /// + /// * `_ctx` - The context of the egui, which provides access to the current state of the egui. + /// * `_raw_input` - The raw input events that are about to be processed. This can be modified to change the input that egui processes. + /// + /// # Note + /// + /// This function does not return a value. Any changes to the input should be made directly to `_raw_input`. + fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {} } /// Selects the level of hardware graphics acceleration. diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index ee54b5212..f27d01120 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -274,6 +274,8 @@ impl EpiIntegration { let close_requested = raw_input.viewport().close_requested(); + app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml new file mode 100644 index 000000000..1557b35c8 --- /dev/null +++ b/examples/custom_keypad/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "custom_keypad" +version = "0.1.0" +authors = ["Varphone Wong "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.72" +publish = false + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/examples/custom_keypad/README.md b/examples/custom_keypad/README.md new file mode 100644 index 000000000..9e5cdf7e8 --- /dev/null +++ b/examples/custom_keypad/README.md @@ -0,0 +1,7 @@ +Example showing how to implements a custom keypad. + +```sh +cargo run -p custom_keypad +``` + +![](screenshot.png) diff --git a/examples/custom_keypad/screenshot.png b/examples/custom_keypad/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..632459e51aa92fc9e642e0e99e3c3bd4319b4a37 GIT binary patch literal 39832 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

kyz%XJU00yocP@f90*mC$X(2}KC< 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 literal 0 HcmV?d00001 diff --git a/examples/custom_keypad/src/keypad.rs b/examples/custom_keypad/src/keypad.rs new file mode 100644 index 000000000..81800f47d --- /dev/null +++ b/examples/custom_keypad/src/keypad.rs @@ -0,0 +1,255 @@ +use eframe::egui::{self, pos2, vec2, Button, Ui, Vec2}; + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum Transition { + #[default] + None, + CloseOnNextFrame, + CloseImmediately, +} + +#[derive(Clone, Debug)] +struct State { + open: bool, + closable: bool, + close_on_next_frame: bool, + start_pos: egui::Pos2, + focus: Option, + events: Option>, +} + +impl State { + fn new() -> Self { + Self { + open: false, + closable: false, + close_on_next_frame: false, + start_pos: pos2(100.0, 100.0), + focus: None, + events: None, + } + } + + fn queue_char(&mut self, c: char) { + let events = self.events.get_or_insert(vec![]); + if let Some(key) = egui::Key::from_name(&c.to_string()) { + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } + events.push(egui::Event::Text(c.to_string())); + } + + fn queue_key(&mut self, key: egui::Key) { + let events = self.events.get_or_insert(vec![]); + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +/// A simple keypad widget. +pub struct Keypad { + id: egui::Id, +} + +impl Keypad { + pub fn new() -> Self { + Self { + id: egui::Id::new("keypad"), + } + } + + pub fn bump_events(&self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + let events = ctx.memory_mut(|m| { + m.data + .get_temp_mut_or_default::(self.id) + .events + .take() + }); + if let Some(mut events) = events { + events.append(&mut raw_input.events); + raw_input.events = events; + } + } + + fn buttons(ui: &mut Ui, state: &mut State) -> Transition { + let mut trans = Transition::None; + ui.vertical(|ui| { + let window_margin = ui.spacing().window_margin; + let size_1x1 = vec2(32.0, 26.0); + let _size_1x2 = vec2(32.0, 52.0 + window_margin.top); + let _size_2x1 = vec2(64.0 + window_margin.left, 26.0); + + ui.spacing_mut().item_spacing = Vec2::splat(window_margin.left); + + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("1")).clicked() { + state.queue_char('1'); + } + if ui.add_sized(size_1x1, Button::new("2")).clicked() { + state.queue_char('2'); + } + if ui.add_sized(size_1x1, Button::new("3")).clicked() { + state.queue_char('3'); + } + if ui.add_sized(size_1x1, Button::new("⏮")).clicked() { + state.queue_key(egui::Key::Home); + } + if ui.add_sized(size_1x1, Button::new("🔙")).clicked() { + state.queue_key(egui::Key::Backspace); + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("4")).clicked() { + state.queue_char('4'); + } + if ui.add_sized(size_1x1, Button::new("5")).clicked() { + state.queue_char('5'); + } + if ui.add_sized(size_1x1, Button::new("6")).clicked() { + state.queue_char('6'); + } + if ui.add_sized(size_1x1, Button::new("⏭")).clicked() { + state.queue_key(egui::Key::End); + } + if ui.add_sized(size_1x1, Button::new("⎆")).clicked() { + state.queue_key(egui::Key::Enter); + trans = Transition::CloseOnNextFrame; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("7")).clicked() { + state.queue_char('7'); + } + if ui.add_sized(size_1x1, Button::new("8")).clicked() { + state.queue_char('8'); + } + if ui.add_sized(size_1x1, Button::new("9")).clicked() { + state.queue_char('9'); + } + if ui.add_sized(size_1x1, Button::new("⏶")).clicked() { + state.queue_key(egui::Key::ArrowUp); + } + if ui.add_sized(size_1x1, Button::new("⌨")).clicked() { + trans = Transition::CloseImmediately; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("0")).clicked() { + state.queue_char('0'); + } + if ui.add_sized(size_1x1, Button::new(".")).clicked() { + state.queue_char('.'); + } + if ui.add_sized(size_1x1, Button::new("⏴")).clicked() { + state.queue_key(egui::Key::ArrowLeft); + } + if ui.add_sized(size_1x1, Button::new("⏷")).clicked() { + state.queue_key(egui::Key::ArrowDown); + } + if ui.add_sized(size_1x1, Button::new("⏵")).clicked() { + state.queue_key(egui::Key::ArrowRight); + } + }); + }); + + trans + } + + pub fn show(&self, ctx: &egui::Context) { + let (focus, mut state) = ctx.memory(|m| { + ( + m.focus(), + m.data.get_temp::(self.id).unwrap_or_default(), + ) + }); + + let mut is_first_show = false; + if ctx.wants_keyboard_input() && state.focus != focus { + let y = ctx.style().spacing.interact_size.y * 1.25; + state.open = true; + state.start_pos = ctx.input(|i| { + i.pointer + .hover_pos() + .map_or(pos2(100.0, 100.0), |p| p + vec2(0.0, y)) + }); + state.focus = focus; + is_first_show = true; + } + + if state.close_on_next_frame { + state.open = false; + state.close_on_next_frame = false; + state.focus = None; + } + + let mut open = state.open; + + let win = egui::Window::new("⌨ Keypad"); + let win = if is_first_show { + win.current_pos(state.start_pos) + } else { + win.default_pos(state.start_pos) + }; + let resp = win + .movable(true) + .resizable(false) + .open(&mut open) + .show(ctx, |ui| Self::buttons(ui, &mut state)); + + state.open = open; + + if let Some(resp) = resp { + match resp.inner { + Some(Transition::CloseOnNextFrame) => { + state.close_on_next_frame = true; + } + Some(Transition::CloseImmediately) => { + state.open = false; + state.focus = None; + } + _ => {} + } + if !state.closable && resp.response.hovered() { + state.closable = true; + } + if state.closable && resp.response.clicked_elsewhere() { + state.open = false; + state.closable = false; + state.focus = None; + } + if is_first_show { + ctx.move_to_top(resp.response.layer_id); + } + } + + if let (true, Some(focus)) = (state.open, state.focus) { + ctx.memory_mut(|m| { + m.request_focus(focus); + }); + } + + ctx.memory_mut(|m| m.data.insert_temp(self.id, state)); + } +} + +impl Default for Keypad { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/custom_keypad/src/main.rs b/examples/custom_keypad/src/main.rs new file mode 100644 index 000000000..5cb26240c --- /dev/null +++ b/examples/custom_keypad/src/main.rs @@ -0,0 +1,68 @@ +// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +use eframe::egui; + +mod keypad; +use keypad::Keypad; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), + ..Default::default() + }; + eframe::run_native( + "Custom Keypad App", + options, + Box::new(|cc| { + // Use the dark theme + cc.egui_ctx.set_visuals(egui::Visuals::dark()); + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Box::::default() + }), + ) +} + +struct MyApp { + name: String, + age: u32, + keypad: Keypad, +} + +impl MyApp {} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + keypad: Keypad::new(), + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::Window::new("Custom Keypad") + .default_pos([100.0, 100.0]) + .title_bar(true) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + }); + + self.keypad.show(ctx); + } + + fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + self.keypad.bump_events(ctx, raw_input); + } +} From d0a6bbf2b880ae00dd3db1185591ba9c0ed51ed6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 12 Mar 2024 11:14:12 +0100 Subject: [PATCH 23/27] Fix style of disabled widgets (#4163) * Broke in https://github.com/emilk/egui/pull/4026/files The `Response::sense` for `enabled: false` widgets was wrong, leading to the wrong widget style. --- crates/egui/src/context.rs | 17 ++++++++--------- crates/egui/src/hit_test.rs | 10 ++++++++++ crates/egui/src/interaction.rs | 24 +++++++++++++----------- crates/egui/src/response.rs | 7 +++++++ crates/egui/src/widget_rect.rs | 7 +++++++ 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 8e09414a6..232cca385 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1018,12 +1018,7 @@ impl Context { /// /// If the widget already exists, its state (sense, Rect, etc) will be updated. #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response { - if !w.enabled { - w.sense.click = false; - w.sense.drag = false; - } - + pub(crate) fn create_widget(&self, w: WidgetRect) -> Response { // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1130,7 +1125,8 @@ impl Context { let input = &viewport.input; let memory = &mut ctx.memory; - if sense.click + if enabled + && sense.click && memory.has_focus(id) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { @@ -1139,7 +1135,10 @@ impl Context { } #[cfg(feature = "accesskit")] - if sense.click && input.has_accesskit_action_request(id, accesskit::Action::Default) { + if enabled + && sense.click + && input.has_accesskit_action_request(id, accesskit::Action::Default) + { res.clicked[PointerButton::Primary as usize] = true; } @@ -1159,7 +1158,7 @@ impl Context { for pointer_event in &input.pointer.pointer_events { if let PointerEvent::Released { click, button } = pointer_event { - if sense.click && clicked { + if enabled && sense.click && clicked { if let Some(click) = click { res.clicked[*button as usize] = true; res.double_clicked[*button as usize] = click.is_double(); diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 2b778e9d7..62a762aa5 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -78,6 +78,16 @@ pub fn hit_test( let top_layer = closest_hit.layer_id; close.retain(|w| w.layer_id == top_layer); + // If the widget is disabled, treat it as if it isn't sensing anything. + // This simplifies the code in `hit_test_on_close` so it doesn't have to check + // the `enabled` flag everywhere: + for w in &mut close { + if !w.enabled { + w.sense.click = false; + w.sense.drag = false; + } + } + let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos); let hits = hit_test_on_close(&close, pos_in_layer); diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 2f1925c75..20d548870 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -161,18 +161,20 @@ pub(crate) fn interact( if dragged.is_none() { // Check if we started dragging something new: if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) { - let is_dragged = if widget.sense.click && widget.sense.drag { - // This widget is sensitive to both clicks and drags. - // When the mouse first is pressed, it could be either, - // so we postpone the decision until we know. - input.pointer.is_decidedly_dragging() - } else { - // This widget is just sensitive to drags, so we can mark it as dragged right away: - widget.sense.drag - }; + if widget.enabled { + let is_dragged = if widget.sense.click && widget.sense.drag { + // This widget is sensitive to both clicks and drags. + // When the mouse first is pressed, it could be either, + // so we postpone the decision until we know. + input.pointer.is_decidedly_dragging() + } else { + // This widget is just sensitive to drags, so we can mark it as dragged right away: + widget.sense.drag + }; - if is_dragged { - dragged = Some(widget.id); + if is_dragged { + dragged = Some(widget.id); + } } } } diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index a34ed8fa7..ddd683c4c 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -42,6 +42,13 @@ pub struct Response { pub interact_rect: Rect, /// The senses (click and/or drag) that the widget was interested in (if any). + /// + /// Note: if [`Self::enabled`] is `false`, then + /// the widget _effectively_ doesn't sense anything, + /// but can still have the same `Sense`. + /// This is because the sense informs the styling of the widget, + /// but we don't want to change the style when a widget is disabled + /// (that is handled by the `Painter` directly). pub sense: Sense, /// Was the widget enabled? diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index acf3dd95d..ab95447e1 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -29,6 +29,13 @@ pub struct WidgetRect { pub interact_rect: Rect, /// How the widget responds to interaction. + /// + /// Note: if [`Self::enabled`] is `false`, then + /// the widget _effectively_ doesn't sense anything, + /// but can still have the same `Sense`. + /// This is because the sense informs the styling of the widget, + /// but we don't want to change the style when a widget is disabled + /// (that is handled by the `Painter` directly). pub sense: Sense, /// Is the widget enabled? From 4a330de53d2a79a65e3920f80701c7b8634a3ced Mon Sep 17 00:00:00 2001 From: CrazyCraftix <43807375+CrazyCraftix@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:14:24 +0100 Subject: [PATCH 24/27] Fix scaling of rounded corners for rect shape (#4152) (#4161) When scaling an `egui::Shape` of variant `Rect` using the new `transform` function, corner rounding isn't taken into account. The fix is to multiply the rounding by the scaling factor. * Closes --- crates/epaint/src/shape.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 0aeec5525..fefae6c93 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -403,6 +403,7 @@ impl Shape { Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; rect_shape.stroke.width *= transform.scaling; + rect_shape.rounding *= transform.scaling; } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; From efc0a6385c7a70f802697e4a29f2dfdc5367a61f Mon Sep 17 00:00:00 2001 From: ming08108 Date: Tue, 12 Mar 2024 05:14:40 -0500 Subject: [PATCH 25/27] Update kb modifiers from web mouse events (#4156) Update modifier state from web mouse events. This allows modifiers to be correctly updated when the window is not in focus but the mouse is still moving over the window. --- CONTRIBUTING.md | 4 ++-- crates/eframe/src/web/events.rs | 12 +++++++++--- crates/eframe/src/web/input.rs | 18 +++++++++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b33189280..206829e7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ Please keep pull requests small and focused. The smaller it is, the more likely Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs! -It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpituity, so we have a high bar on what get merged! +It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpetuity, so we have a high bar on what get merged! When reviewing, we look for: * The PR title and description should be helpful @@ -123,7 +123,7 @@ with `Vec2::X` increasing to the right and `Vec2::Y` increasing downwards. `egui` uses logical _points_ as its coordinate system. Those related to physical _pixels_ by the `pixels_per_point` scale factor. -For example, a high-dpi screeen can have `pixels_per_point = 2.0`, +For example, a high-dpi screen can have `pixels_per_point = 2.0`, meaning there are two physical screen pixels for each logical point. Angles are in radians, and are measured clockwise from the X-axis, which has angle=0. diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 13bea2560..71fe04e0c 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -87,7 +87,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa return; } - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; let key = event.key(); @@ -158,7 +158,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa &document, "keyup", |event: web_sys::KeyboardEvent, runner| { - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; if let Some(key) = translate_key(&event.key()) { runner.input.raw.events.push(egui::Event::Key { @@ -301,6 +301,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu &canvas, "mousedown", |event: web_sys::MouseEvent, runner: &mut AppRunner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { let pos = pos_from_mouse_event(runner.canvas_id(), &event); let modifiers = runner.input.raw.modifiers; @@ -327,6 +329,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu &canvas, "mousemove", |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; let pos = pos_from_mouse_event(runner.canvas_id(), &event); runner.input.raw.events.push(egui::Event::PointerMoved(pos)); runner.needs_repaint.repaint_asap(); @@ -336,6 +340,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu )?; runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { let pos = pos_from_mouse_event(runner.canvas_id(), &event); let modifiers = runner.input.raw.modifiers; @@ -474,7 +480,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. // This if-statement is equivalent to how `Modifiers.command` is determined in - // `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`]. + // `modifiers_from_kb_event()`, but we cannot directly use that fn for a [`WheelEvent`]. if event.ctrl_key() || event.meta_key() { let factor = (delta.y / 200.0).exp(); runner.input.raw.events.push(egui::Event::Zoom(factor)); diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index 96cad32e2..223693f42 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -115,7 +115,23 @@ pub fn translate_key(key: &str) -> Option { egui::Key::from_name(key) } -pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { +pub fn modifiers_from_kb_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { + egui::Modifiers { + alt: event.alt_key(), + ctrl: event.ctrl_key(), + shift: event.shift_key(), + + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + mac_cmd: event.meta_key(), + + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + command: event.ctrl_key() || event.meta_key(), + } +} + +pub fn modifiers_from_mouse_event(event: &web_sys::MouseEvent) -> egui::Modifiers { egui::Modifiers { alt: event.alt_key(), ctrl: event.ctrl_key(), From c87bcc4bccd76938f3f7aff9a1f10903d032bf2e Mon Sep 17 00:00:00 2001 From: Vincent Alsteen Date: Tue, 12 Mar 2024 11:15:13 +0100 Subject: [PATCH 26/27] Remove unnecessary allocation in `RepaintCause::new` (#4146) Hi! I'm using egui for the UI of a VST3/Clap plugin, and this kind of environment is rather picky on performance. I use [assert_no_alloc](https://crates.io/crates/assert_no_alloc) to make sure the audio thread is never allocating. The audio thread may request a repaint of the GUI tho, and this is where a saw that it may allocate when tracing the repaint reason. Turns out it's not necessary, `Location::caller` is `'static`, so using a `&'static str` instead of a `String` in `RepaintCause::file` will just work, so this PR just does that. --- crates/egui/src/context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 232cca385..c1dcd5b5b 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -256,7 +256,7 @@ struct ViewportState { #[derive(Clone, Debug)] pub struct RepaintCause { /// What file had the call that requested the repaint? - pub file: String, + pub file: &'static str, /// What line number of the the call that requested the repaint? pub line: u32, @@ -269,7 +269,7 @@ impl RepaintCause { pub fn new() -> Self { let caller = Location::caller(); Self { - file: caller.file().to_owned(), + file: caller.file(), line: caller.line(), } } From f0190320333c8a8f5f5520833d754ada1ce0f529 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 12 Mar 2024 19:21:33 +0900 Subject: [PATCH 27/27] TextEdit: fix crash when hitting SHIFT + TAB around non-ASCII text (#3984) * Closes #3846 * Closes #3878 Dear emilk. Leaving aside other function, I think this is all you need to fix to patch the panic that occurs when Shift + TAB. Thank you, emilk. --- crates/egui/src/widgets/text_edit/text_buffer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index f28878a1d..e70ce695a 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -89,10 +89,12 @@ pub trait TextBuffer { fn decrease_indentation(&mut self, ccursor: &mut CCursor) { let line_start = find_line_start(self.as_str(), *ccursor); - let remove_len = if self.as_str()[line_start.index..].starts_with('\t') { + let remove_len = if self.as_str().chars().nth(line_start.index) == Some('\t') { Some(1) - } else if self.as_str()[line_start.index..] + } else if self + .as_str() .chars() + .skip(line_start.index) .take(TAB_SIZE) .all(|c| c == ' ') {