Layers panel: test the Blend Mode dropdown's arrow keys with the Move tool active

The open dropdown takes ↑/↓ ahead of the Move tool's nudge, checked in
the full app. The arrow-key step reads the option with get() and uses the
response's own id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
This commit is contained in:
Claude authored and BFlat committed 2026-10-09 07:37:54 +07:00
1 parent 4f99e42dcb
commit 38c97bdc5d
3 files changed
+59 -7

No files matched your search

@@ -0,0 +1,47 @@
//! #1318: the Layers panel's Blend Mode dropdown takes the arrow keys once it is open, ahead of
//! the tool shortcuts (the Move tool's arrow-key nudge).
use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use photocraft_doc::BlendMode;
use serde_json::json;
use crate::PhotocraftApp;
use crate::state::Tool;
#[test]
fn arrow_keys_step_the_open_blend_mode_dropdown_without_nudging_the_layer() {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_max_steps(64).build_eframe(|cc| {
PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 64, "height": 64})).unwrap();
app.run("layer.new.layer", json!({})).unwrap();
app.run("edit.fill", json!({"color": "#ff0000"})).unwrap();
app.ui.tool = Tool::Move;
app
});
h.run_steps(6);
let layer = |h: &Harness<'_, PhotocraftApp>| {
let st = h.state().session.active().unwrap();
let l = st.doc.layer(st.active_layer.unwrap()).unwrap();
(l.blend, l.surface().unwrap().content_bounds())
};
let (blend, bounds) = layer(&h);
assert_eq!(blend, BlendMode::Normal);
let named = |n: &egui_kittest::Node<'_>| {
let a = n.accesskit_node();
a.value().as_deref() == Some("Normal") || a.label().as_deref() == Some("Normal")
};
let combo = h.query_all_by_role(Role::ComboBox).find(|n| named(n));
combo.expect("the Layers panel's Blend Mode dropdown").click();
h.run_steps(2);
h.key_press(egui::Key::ArrowDown);
h.run_steps(2);
let (blend, moved) = layer(&h);
assert_ne!(blend, BlendMode::Normal, "↓ picks the next mode");
assert_eq!(moved, bounds, "the Move tool's nudge doesn't also take the key");
h.key_press(egui::Key::ArrowUp);
h.run_steps(2);
assert_eq!(layer(&h).0, BlendMode::Normal);
}
+3
View File
@@ -1519,6 +1519,9 @@ mod move_auto_select_tests;
#[cfg(test)]
mod hidden_layer_tests;
#[cfg(test)]
mod blend_dropdown_keys_tests;
#[cfg(test)]
mod marquee_tests;
+9 -7
View File
@@ -681,20 +681,20 @@ pub fn dropdown_hovered<T: PartialEq + Clone>(ui: &mut Ui, id: &str, current: &m
}
}
});
let stepped = combo_box_arrow_keys(ui, response.response.id, &response.response, current, options);
let stepped = combo_box_arrow_keys(ui, &response.response, current, options);
(changed || stepped, hovered)
}
/// Give a dropdown keyboard focus when it opens, then use the arrow keys to move through its
/// choices. `egui::ComboBox` opens a popup but leaves focus on the canvas by default, which makes
/// controls such as the Layers panel's Blend Mode dropdown unreachable from the keyboard.
fn combo_box_arrow_keys<T: PartialEq + Clone>(ui: &mut Ui, combo_id: egui::Id, response: &Response, current: &mut T, options: &[(T, &str)]) -> bool {
fn combo_box_arrow_keys<T: PartialEq + Clone>(ui: &mut Ui, response: &Response, current: &mut T, options: &[(T, &str)]) -> bool {
if response.clicked() {
response.request_focus();
}
// The popup, rather than its button, becomes the focused egui layer after it opens. While it
// is open it owns its navigation keys, even though `response.has_focus()` is then false.
if !egui::ComboBox::is_open(ui.ctx(), combo_id) || options.is_empty() {
if !egui::ComboBox::is_open(ui.ctx(), response.id) || options.is_empty() {
return false;
}
let step = if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown)) {
@@ -706,11 +706,13 @@ fn combo_box_arrow_keys<T: PartialEq + Clone>(ui: &mut Ui, combo_id: egui::Id, r
};
let index = options.iter().position(|(value, _)| value == current).unwrap_or(0);
let next = if step > 0 { (index + 1).min(options.len() - 1) } else { index.saturating_sub(1) };
if next == index {
return false;
match options.get(next) {
Some((value, _)) if next != index => {
*current = value.clone();
true
}
_ => false,
}
*current = options[next].0.clone();
true
}
/// The body of a right-click menu: as tall as its items up to the part of the window that can be