Add Response::changed(): see if e.g. text was entered or slider dragged

This commit is contained in:
Emil Ernerfeldt 2021-02-28 17:17:37 +01:00
parent a859b2a26e
commit d3fd51d6a4
10 changed files with 83 additions and 17 deletions

View file

@ -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 🔧

View file

@ -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() {

View file

@ -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,
}
}
}

View file

@ -897,9 +897,10 @@ impl Ui {
selected_value: Value,
text: impl Into<String>,
) -> 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<String>,
) -> 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

View file

@ -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

View file

@ -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();
}
})
});

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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);