mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-10 12:22:22 +00:00
PR #12268: Tolerance range alert follows the last typed text and a field that goes away
CodeRabbit's two notes on the shared whole-number Tolerance field (Color Replacement and Background Eraser): - typing 150, then replacing it with "abc" and leaving the field still opened the range alert; text that is no number now clears the owed alert (number fields note whether their typed text parses); - typing 150 and changing the tool by a shortcut the field passes by clamped the value without the alert; the options bar now shows the alert the field owed when the field was not drawn again. Co-Authored-By: Hector Villarreal <1501713+hectorviov@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42ebea1c9f
commit
c36d5ae588
5 files changed
+87
-4
No files matched your search
@@ -65,6 +65,40 @@ fn a_fraction_stores_a_whole_number_and_text_changes_nothing() {
|
||||
assert_eq!(alert(&h), None, "and no alert");
|
||||
}
|
||||
|
||||
/// Types `text` into the Tolerance field and leaves it there, with the keyboard.
|
||||
fn type_without_leaving(h: &mut Harness<'static, PhotocraftApp>, text: &str) {
|
||||
h.query_all_by_role(Role::SpinButton).find(|n| n.accesskit_node().label().as_deref() == Some("Tolerance:")).expect("the Tolerance field").click();
|
||||
h.run_steps(1);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Text(text.into()));
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_typed_over_a_value_past_the_range_does_not_alert() {
|
||||
let mut h = bar();
|
||||
type_without_leaving(&mut h, "150");
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Text("abc".into()));
|
||||
h.run_steps(1);
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(3);
|
||||
assert_eq!(alert(&h), None, "the field is left holding text that is no number: no alert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_change_while_typing_past_the_range_still_alerts() {
|
||||
let mut h = bar();
|
||||
type_without_leaving(&mut h, "150");
|
||||
assert_eq!(alert(&h), None, "not yet: the field still has the keyboard");
|
||||
// A tool shortcut the field passes by (a custom function key) changes the tool before the
|
||||
// field is left, so the field is never drawn again.
|
||||
h.state_mut().ui.tool = Tool::Brush;
|
||||
h.run_steps(4);
|
||||
assert_eq!(tolerance(&h), 100.0);
|
||||
assert_eq!(alert(&h).as_deref(), Some(TOLERANCE_RANGE_ALERT), "the alert the field owed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bar_has_the_reference_tooltips() {
|
||||
for (role, label, tip) in [
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(super) fn options_bar_height(pro: bool, narrow: bool) -> f32 {
|
||||
|
||||
pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
crate::vector_ui::track_arrow_default_selection(app, ui.ctx());
|
||||
crate::eraser_ui::alert_for_a_field_gone(app, ui.ctx());
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let narrow = app.session.prefs().workspace.narrow_options_bar();
|
||||
egui::Panel::top("options_bar")
|
||||
|
||||
@@ -227,7 +227,10 @@ pub(crate) fn whole_tolerance_field(ui: &mut egui::Ui, value: &mut f32, draft: &
|
||||
// An edit the field never got to finish (the options bar was hidden while it had the
|
||||
// keyboard) has nothing left to say once the field is entered again.
|
||||
if r.gained_focus() {
|
||||
ui.ctx().data_mut(|d| d.remove_temp::<bool>(out_id));
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.remove_temp::<bool>(out_id);
|
||||
d.remove_temp::<bool>(r.id.with("parses"));
|
||||
});
|
||||
}
|
||||
if r.changed() && typed.is_finite() {
|
||||
*value = typed.round().clamp(1.0, 100.0);
|
||||
@@ -243,10 +246,47 @@ pub(crate) fn whole_tolerance_field(ui: &mut egui::Ui, value: &mut f32, draft: &
|
||||
ui.ctx().data_mut(|d| d.remove_temp::<(f32, f32)>(draft_id));
|
||||
}
|
||||
}
|
||||
// Text replaced by something that is no number (`150`, then `abc`) leaves the value alone
|
||||
// when the field is left, with no alert (#12268).
|
||||
if r.has_focus() && widgets::typed_text_parses(ui.ctx(), r.id) == Some(false) {
|
||||
ui.ctx().data_mut(|d| d.remove_temp::<bool>(out_id));
|
||||
}
|
||||
if !r.has_focus() {
|
||||
ui.ctx().data_mut(|d| d.remove_temp::<(f32, f32)>(draft_id));
|
||||
}
|
||||
r.lost_focus() && ui.ctx().data_mut(|d| d.remove_temp::<bool>(out_id)).unwrap_or(false)
|
||||
let alert = r.lost_focus() && ui.ctx().data_mut(|d| d.remove_temp::<bool>(out_id)).unwrap_or(false);
|
||||
// While a value typed out of range waits for the field to be left, the bar notes the pass, so
|
||||
// a field that goes away instead (a tool shortcut that passes the field by) still alerts.
|
||||
let owed = r.has_focus() && ui.ctx().data(|d| d.get_temp::<bool>(out_id)).unwrap_or(false);
|
||||
let pass = ui.ctx().cumulative_pass_nr();
|
||||
ui.ctx().data_mut(|d| {
|
||||
if owed {
|
||||
d.insert_temp(range_alert_owed_id(), pass);
|
||||
} else {
|
||||
d.remove_temp::<u64>(range_alert_owed_id());
|
||||
}
|
||||
});
|
||||
alert
|
||||
}
|
||||
|
||||
fn range_alert_owed_id() -> egui::Id {
|
||||
egui::Id::unique("whole-tolerance-range-alert-owed")
|
||||
}
|
||||
|
||||
/// Shows [`TOLERANCE_RANGE_ALERT`] for a whole-number Tolerance field that went away in the last
|
||||
/// pass while it held a value typed out of range (the tool changed by a shortcut, #12268). The
|
||||
/// options bar calls it before drawing a tool's options. An edit older than that (the bar was
|
||||
/// hidden meanwhile) is dropped, as the field drops it once entered again.
|
||||
pub(crate) fn alert_for_a_field_gone(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
let Some(seen) = ctx.data(|d| d.get_temp::<u64>(range_alert_owed_id())) else { return };
|
||||
let now = ctx.cumulative_pass_nr();
|
||||
if seen.saturating_add(1) >= now {
|
||||
return;
|
||||
}
|
||||
ctx.data_mut(|d| d.remove_temp::<u64>(range_alert_owed_id()));
|
||||
if seen.saturating_add(2) == now {
|
||||
open_range_alert(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Tolerance as the field stores it: a whole number in 1..=100, the default 50 for a value
|
||||
|
||||
@@ -50,8 +50,8 @@ pub(crate) use fields::compact_wrapping_value_field;
|
||||
#[cfg(test)]
|
||||
use fields::{FieldOpts, NUMERIC_INPUT_LIMIT, value_field_in};
|
||||
pub use fields::{
|
||||
POPUP_ARROW_W, PopupFieldResponse, VALUE_FIELD_H, compact_value_field, elided_label, elided_label_sensed, popup_value_field, value_field,
|
||||
value_field_named, value_field_whole_alerting,
|
||||
POPUP_ARROW_W, PopupFieldResponse, VALUE_FIELD_H, compact_value_field, elided_label, elided_label_sensed, popup_value_field, typed_text_parses,
|
||||
value_field, value_field_named, value_field_whole_alerting,
|
||||
};
|
||||
pub(crate) use panel::body_margin;
|
||||
pub use panel::{
|
||||
|
||||
@@ -538,6 +538,8 @@ fn number_edit(
|
||||
};
|
||||
let s = ready.as_str();
|
||||
let v = if hold_typed { zoom_number(s, units) } else { parse_num_in(s, units) };
|
||||
// Whether the text being typed is a number at all, for [`typed_text_parses`].
|
||||
ctx.data_mut(|d| d.insert_temp(id.with("parses"), v.is_some()));
|
||||
if ctx.memory(|m| m.has_focus(id)) && plain(s).is_none() {
|
||||
// Still typing arithmetic: hold it, and stop applying keystrokes once it parses.
|
||||
ctx.data_mut(|d| d.insert_temp(held, v.is_some()));
|
||||
@@ -570,6 +572,12 @@ fn number_edit(
|
||||
resp
|
||||
}
|
||||
|
||||
/// Whether the text last typed into the number field `id` (a [`value_field`]'s response id) reads
|
||||
/// as a number, an arithmetic entry included once it is complete: `None` before anything was typed.
|
||||
pub fn typed_text_parses(ctx: &egui::Context, id: egui::Id) -> Option<bool> {
|
||||
ctx.data(|d| d.get_temp::<bool>(id.with("parses")))
|
||||
}
|
||||
|
||||
/// The zoom a zoom field's text stands for, with at most one decimal separator (`,` or `.`): a
|
||||
/// number, or a ratio (`1/2` is 50 %, `5/2` is 250 %). `None` when it is neither, or holds a
|
||||
/// second separator (`zoom_field::read` takes that apart).
|
||||
|
||||
Reference in new issue
Block a user