diff --git a/CHANGELOG.md b/CHANGELOG.md index e36a149d..4c646ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Add `Ui::hyperlink_to(label, url)`. * Sliders can now have a value prefix and suffix (e.g. the suffix `"°"` works like a unit). * `Context::set_pixels_per_point` to control the scale of the UI. +* Add `Response::changed()` to query if e.g. a slider was dragged, text was entered or a checkbox was clicked. * Add support for all integers in `DragValue` and `Slider` (except 128-bit). ### Changed 🔧 diff --git a/egui/src/context.rs b/egui/src/context.rs index 25d950e3..c986c559 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -198,6 +198,7 @@ impl CtxRef { interact_pointer_pos: None, has_kb_focus, lost_kb_focus, + changed: false, // must be set by the widget itself }; if !enabled || sense == Sense::hover() || !layer_id.allow_interaction() { diff --git a/egui/src/response.rs b/egui/src/response.rs index 2d88d205..f1965eeb 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -8,8 +8,11 @@ use crate::{CtxRef, Id, LayerId, Sense, Ui}; /// The result of adding a widget to a [`Ui`]. /// -/// This lets you know whether or not a widget has been clicked this frame. +/// A `Response` lets you know whether or not a widget is being hovered, clicked or dragged. /// It also lets you easily show a tooltip on hover. +/// +/// Whenever something gets added to a `Ui`, a `Response` object is returned. +/// [`ui.add`] returns a `Response`, as does [`ui.button`], and all similar shortcuts. #[derive(Clone)] pub struct Response { // CONTEXT: @@ -62,6 +65,11 @@ pub struct Response { /// The widget had keyboard focus and lost it. pub(crate) lost_kb_focus: bool, + + /// What the underlying data changed? + /// e.g. the slider was dragged, text was entered in a `TextEdit` etc. + /// Always `false` for something like a `Button`. + pub(crate) changed: bool, } impl std::fmt::Debug for Response { @@ -82,6 +90,7 @@ impl std::fmt::Debug for Response { interact_pointer_pos, has_kb_focus, lost_kb_focus, + changed, } = self; f.debug_struct("Response") .field("layer_id", layer_id) @@ -98,6 +107,7 @@ impl std::fmt::Debug for Response { .field("interact_pointer_pos", interact_pointer_pos) .field("has_kb_focus", has_kb_focus) .field("lost_kb_focus", lost_kb_focus) + .field("changed", changed) .finish() } } @@ -192,6 +202,24 @@ impl Response { self.is_pointer_button_down_on } + /// What the underlying data changed? + /// + /// e.g. the slider was dragged, text was entered in a `TextEdit` etc. + /// Always `false` for something like a `Button`. + /// Can sometimes be `true` even though the data didn't changed + /// (e.g. if the user entered a character and erased it the same frame). + pub fn changed(&self) -> bool { + self.changed + } + + /// Report the data shown by this widget changed. + /// + /// This must be called by widgets that represent some mutable data, + /// e.g. checkboxes, sliders etc. + pub fn mark_changed(&mut self) { + self.changed = true; + } + /// Show this UI if the item was hovered (i.e. a tooltip). /// If you call this multiple times the tooltips will stack underneath the previous ones. pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { @@ -315,6 +343,7 @@ impl Response { interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), has_kb_focus: self.has_kb_focus || other.has_kb_focus, lost_kb_focus: self.lost_kb_focus || other.lost_kb_focus, + changed: self.changed || other.changed, } } } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 487b34dc..3227bf95 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -897,9 +897,10 @@ impl Ui { selected_value: Value, text: impl Into, ) -> Response { - let response = self.radio(*current_value == selected_value, text); + let mut response = self.radio(*current_value == selected_value, text); if response.clicked() { *current_value = selected_value; + response.mark_changed(); } response } @@ -920,9 +921,10 @@ impl Ui { selected_value: Value, text: impl Into, ) -> Response { - let response = self.selectable_label(*current_value == selected_value, text); + let mut response = self.selectable_label(*current_value == selected_value, text); if response.clicked() { *current_value = selected_value; + response.mark_changed(); } response } @@ -938,11 +940,12 @@ impl Ui { #![allow(clippy::float_cmp)] let mut degrees = radians.to_degrees(); - let response = self.add(DragValue::f32(&mut degrees).speed(1.0).suffix("°")); + let mut response = self.add(DragValue::f32(&mut degrees).speed(1.0).suffix("°")); // only touch `*radians` if we actually changed the degree value if degrees != radians.to_degrees() { *radians = degrees.to_radians(); + response.changed = true; } response @@ -957,13 +960,14 @@ impl Ui { use std::f32::consts::TAU; let mut taus = *radians / TAU; - let response = self + let mut response = self .add(DragValue::f32(&mut taus).speed(0.01).suffix("τ")) .on_hover_text("1τ = one turn, 0.5τ = half a turn, etc. 0.25τ = 90°"); // only touch `*radians` if we actually changed the value if taus != *radians / TAU { *radians = taus * TAU; + response.changed = true; } response diff --git a/egui/src/widgets/button.rs b/egui/src/widgets/button.rs index 60c96ed0..2bfb3ae2 100644 --- a/egui/src/widgets/button.rs +++ b/egui/src/widgets/button.rs @@ -227,9 +227,10 @@ impl<'a> Widget for Checkbox<'a> { let mut desired_size = total_extra + galley.size; desired_size = desired_size.at_least(spacing.interact_size); desired_size.y = desired_size.y.max(icon_width); - let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); + let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); if response.clicked() { *checked = !*checked; + response.mark_changed(); } // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful diff --git a/egui/src/widgets/color_picker.rs b/egui/src/widgets/color_picker.rs index 2f1b8eb6..a134e489 100644 --- a/egui/src/widgets/color_picker.rs +++ b/egui/src/widgets/color_picker.rs @@ -307,15 +307,22 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) { }); } -fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) { +/// return true on change +fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool { let mut hsvag = HsvaGamma::from(*hsva); color_picker_hsvag_2d(ui, &mut hsvag, alpha); - *hsva = Hsva::from(hsvag); + let new_hasva = Hsva::from(hsvag); + if *hsva == new_hasva { + false + } else { + *hsva = new_hasva; + true + } } pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response { let pupup_id = ui.auto_id_with("popup"); - let button_response = color_button(ui, (*hsva).into()).on_hover_text("Click to edit color"); + let mut button_response = color_button(ui, (*hsva).into()).on_hover_text("Click to edit color"); if button_response.clicked() { ui.memory().toggle_popup(pupup_id); @@ -328,7 +335,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res .show(ui.ctx(), |ui| { ui.spacing_mut().slider_width = 256.0; Frame::popup(ui.style()).show(ui, |ui| { - color_picker_hsva_2d(ui, hsva, alpha); + if color_picker_hsva_2d(ui, hsva, alpha) { + button_response.mark_changed(); + } }) }); diff --git a/egui/src/widgets/drag_value.rs b/egui/src/widgets/drag_value.rs index 0e37f4f2..99c35f4c 100644 --- a/egui/src/widgets/drag_value.rs +++ b/egui/src/widgets/drag_value.rs @@ -210,7 +210,7 @@ impl<'a> Widget for DragValue<'a> { let kb_edit_id = ui.auto_id_with("edit"); let is_kb_editing = ui.memory().has_kb_focus(kb_edit_id); - if is_kb_editing { + let mut response = if is_kb_editing { let button_width = ui.spacing().interact_size.x; let mut value_text = ui .memory() @@ -283,6 +283,13 @@ impl<'a> Widget for DragValue<'a> { } } response + }; + + #[allow(clippy::float_cmp)] + { + response.changed = get(&mut get_set_value) != value; } + + response } } diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index cf7a6067..35a0092b 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -396,6 +396,8 @@ impl<'a> Widget for Slider<'a> { let font = &ui.fonts()[text_style]; let height = font.row_height().at_least(ui.spacing().interact_size.y); + let old_value = self.get_value(); + let inner_response = ui.horizontal(|ui| { let slider_response = self.allocate_slider_space(ui, height); self.slider_ui(ui, &slider_response); @@ -410,7 +412,10 @@ impl<'a> Widget for Slider<'a> { } slider_response }); - inner_response.inner | inner_response.response + + let mut response = inner_response.inner | inner_response.response; + response.changed = self.get_value() != old_value; + response } } diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index cdddf5c3..6a472ca1 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -320,7 +320,7 @@ impl<'t> TextEdit<'t> { } else { Sense::hover() }; - let response = ui.interact(rect, id, sense); + let mut response = ui.interact(rect, id, sense); if enabled { ui.memory().interested_in_kb_focus(id); @@ -346,6 +346,7 @@ impl<'t> TextEdit<'t> { primary: galley.from_ccursor(ccursorp.primary), secondary: galley.from_ccursor(ccursorp.secondary), }); + response.mark_changed(); } else if response.hovered() && ui.input().pointer.any_pressed() { ui.memory().request_kb_focus(id); if ui.input().modifiers.shift { @@ -357,9 +358,11 @@ impl<'t> TextEdit<'t> { } else { state.cursorp = Some(CursorPair::one(cursor_at_pointer)); } + response.mark_changed(); } else if ui.input().pointer.any_down() && response.is_pointer_button_down_on() { if let Some(cursorp) = &mut state.cursorp { cursorp.primary = cursor_at_pointer; + response.mark_changed(); } } } @@ -484,6 +487,8 @@ impl<'t> TextEdit<'t> { }; if let Some(new_ccursorp) = did_mutate_text { + response.mark_changed(); + // Layout again to avoid frame delay, and to keep `text` and `galley` in sync. let font = &ui.fonts()[text_style]; galley = if multiline { diff --git a/egui_demo_lib/src/apps/demo/toggle_switch.rs b/egui_demo_lib/src/apps/demo/toggle_switch.rs index 0dd05bc0..6fce6bb1 100644 --- a/egui_demo_lib/src/apps/demo/toggle_switch.rs +++ b/egui_demo_lib/src/apps/demo/toggle_switch.rs @@ -29,11 +29,12 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { // 2. Allocating space: // This is where we get a region of the screen assigned. // We also tell the Ui to sense clicks in the allocated region. - let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); + let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); - // 3. Interact: Time to check for clicks!. + // 3. Interact: Time to check for clicks! if response.clicked() { *on = !*on; + response.mark_changed(); // report back that the value changed } // 4. Paint! @@ -65,8 +66,11 @@ pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { #[allow(dead_code)] fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0); - let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); - *on ^= response.clicked(); // toggle if clicked + let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); + if response.clicked() { + *on = !*on; + response.mark_changed(); + } let how_on = ui.ctx().animate_bool(response.id, *on); let visuals = ui.style().interact_selectable(&response, *on);