Add support for scrolling via accesskit / kittest (#7286)
I need to scroll in a snapshot test in my app, and kittest had no utilities for this. Event::MouseWheel is error prone. This adds support for some accesskit scroll actions, and uses this in kittest to add helpers to scroll to a node / scroll the scroll area surrounding a node. The accesskit code says down/up/left/right `Scrolls by approximately one screen in a specific direction.`. Unfortunately it's difficult to get the size of a "screen" (I guess that would be the size of the containing scroll area)where I implemented the scrolling, so for now I've hardcoded it to 100px. I think scrolling a fixed amount is still better than not scrolling at all. --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
378e22e6ec
commit
6d312cc4c7
|
|
@ -1149,7 +1149,7 @@ impl Context {
|
|||
ID clashes happens when things like Windows or CollapsingHeaders share names,\n\
|
||||
or when things like Plot and Grid:s aren't given unique id_salt:s.\n\n\
|
||||
Sometimes the solution is to use ui.push_id.",
|
||||
if below { "above" } else { "below" })
|
||||
if below { "above" } else { "below" }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1216,6 +1216,51 @@ impl Context {
|
|||
self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.write(|ctx| {
|
||||
use crate::{Align, pass_state::ScrollTarget, style::ScrollAnimation};
|
||||
let viewport = ctx.viewport_for(ctx.viewport_id());
|
||||
|
||||
viewport
|
||||
.input
|
||||
.consume_accesskit_action_requests(res.id, |request| {
|
||||
// TODO(lucasmerlin): Correctly handle the scroll unit:
|
||||
// https://github.com/AccessKit/accesskit/blob/e639c0e0d8ccbfd9dff302d972fa06f9766d608e/common/src/lib.rs#L2621
|
||||
const DISTANCE: f32 = 100.0;
|
||||
|
||||
match &request.action {
|
||||
accesskit::Action::ScrollIntoView => {
|
||||
viewport.this_pass.scroll_target = [
|
||||
Some(ScrollTarget::new(
|
||||
res.rect.x_range(),
|
||||
Some(Align::Center),
|
||||
ScrollAnimation::none(),
|
||||
)),
|
||||
Some(ScrollTarget::new(
|
||||
res.rect.y_range(),
|
||||
Some(Align::Center),
|
||||
ScrollAnimation::none(),
|
||||
)),
|
||||
];
|
||||
}
|
||||
accesskit::Action::ScrollDown => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::UP;
|
||||
}
|
||||
accesskit::Action::ScrollUp => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::DOWN;
|
||||
}
|
||||
accesskit::Action::ScrollLeft => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::LEFT;
|
||||
}
|
||||
accesskit::Action::ScrollRight => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::RIGHT;
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
true
|
||||
});
|
||||
});
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -824,6 +824,23 @@ impl InputState {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn consume_accesskit_action_requests(
|
||||
&mut self,
|
||||
id: crate::Id,
|
||||
mut consume: impl FnMut(&accesskit::ActionRequest) -> bool,
|
||||
) {
|
||||
let accesskit_id = id.accesskit_id();
|
||||
self.events.retain(|event| {
|
||||
if let Event::AccessKitActionRequest(request) = event {
|
||||
if request.target == accesskit_id {
|
||||
return !consume(request);
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
|
||||
self.accesskit_action_requests(id, action).next().is_some()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub use builder::*;
|
|||
pub use node::*;
|
||||
pub use renderer::*;
|
||||
|
||||
use egui::style::ScrollAnimation;
|
||||
use egui::{Key, Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId};
|
||||
use kittest::Queryable;
|
||||
|
||||
|
|
@ -55,6 +56,10 @@ impl Display for ExceededMaxStepsError {
|
|||
/// The [Harness] has a optional generic state that can be used to pass data to the app / ui closure.
|
||||
/// In _most cases_ it should be fine to just store the state in the closure itself.
|
||||
/// The state functions are useful if you need to access the state after the harness has been created.
|
||||
///
|
||||
/// Some egui style options are changed from the defaults:
|
||||
/// - The cursor blinking is disabled
|
||||
/// - The scroll animation is disabled
|
||||
pub struct Harness<'a, State = ()> {
|
||||
pub ctx: egui::Context,
|
||||
input: egui::RawInput,
|
||||
|
|
@ -96,8 +101,12 @@ impl<'a, State> Harness<'a, State> {
|
|||
let ctx = ctx.unwrap_or_default();
|
||||
ctx.set_theme(theme);
|
||||
ctx.enable_accesskit();
|
||||
// Disable cursor blinking so it doesn't interfere with snapshots
|
||||
ctx.all_styles_mut(|style| style.visuals.text_cursor.blink = false);
|
||||
ctx.all_styles_mut(|style| {
|
||||
// Disable cursor blinking so it doesn't interfere with snapshots
|
||||
style.visuals.text_cursor.blink = false;
|
||||
style.scroll_animation = ScrollAnimation::none();
|
||||
style.animation_time = 0.0;
|
||||
});
|
||||
let mut input = egui::RawInput {
|
||||
screen_rect: Some(screen_rect),
|
||||
..Default::default()
|
||||
|
|
@ -564,7 +573,8 @@ impl<'a, State> Harness<'a, State> {
|
|||
.expect("Missing root viewport")
|
||||
}
|
||||
|
||||
fn root(&self) -> Node<'_> {
|
||||
/// The root node of the test harness.
|
||||
pub fn root(&self) -> Node<'_> {
|
||||
Node {
|
||||
accesskit_node: self.kittest.root(),
|
||||
queue: &self.queued_events,
|
||||
|
|
|
|||
|
|
@ -159,4 +159,49 @@ impl Node<'_> {
|
|||
pub fn is_focused(&self) -> bool {
|
||||
self.accesskit_node.is_focused()
|
||||
}
|
||||
|
||||
/// Scroll the node into view.
|
||||
pub fn scroll_to_me(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollIntoView,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node down (100px).
|
||||
pub fn scroll_down(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollDown,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node up (100px).
|
||||
pub fn scroll_up(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollUp,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node left (100px).
|
||||
pub fn scroll_left(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollLeft,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Scroll the [`egui::ScrollArea`] containing this node right (100px).
|
||||
pub fn scroll_right(&self) {
|
||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||
action: accesskit::Action::ScrollRight,
|
||||
target: self.accesskit_node.id(),
|
||||
data: None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:70d76e55327de17163bc9c7e128c28153f95db3229dec919352a024eb80544f1
|
||||
size 7399
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b4b7b3145401b7cf9815a652a0914b230892ffda3b5e23fea530dafee9c0c3d3
|
||||
size 8110
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use egui::{Modifiers, Vec2, include_image};
|
||||
use egui_kittest::Harness;
|
||||
use egui::{Modifiers, ScrollArea, Vec2, include_image};
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
use kittest::Queryable as _;
|
||||
|
||||
#[test]
|
||||
|
|
@ -81,3 +81,60 @@ fn should_wait_for_images() {
|
|||
|
||||
harness.snapshot("should_wait_for_images");
|
||||
}
|
||||
|
||||
fn test_scroll_harness() -> Harness<'static, bool> {
|
||||
Harness::builder()
|
||||
.with_size(Vec2::new(100.0, 200.0))
|
||||
.build_ui_state(
|
||||
|ui, state| {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
for i in 0..20 {
|
||||
ui.label(format!("Item {i}"));
|
||||
}
|
||||
if ui.button("Hidden Button").clicked() {
|
||||
*state = true;
|
||||
};
|
||||
});
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_to_me() {
|
||||
let mut harness = test_scroll_harness();
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
results.add(harness.try_snapshot("test_scroll_initial"));
|
||||
|
||||
harness.get_by_label("Hidden Button").scroll_to_me();
|
||||
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("test_scroll_scrolled"));
|
||||
|
||||
harness.get_by_label("Hidden Button").click();
|
||||
harness.run();
|
||||
|
||||
assert!(
|
||||
harness.state(),
|
||||
"The button was not clicked after scrolling."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_down() {
|
||||
let mut harness = test_scroll_harness();
|
||||
|
||||
let button = harness.get_by_label("Hidden Button");
|
||||
button.scroll_down();
|
||||
button.scroll_down();
|
||||
harness.run();
|
||||
|
||||
harness.get_by_label("Hidden Button").click();
|
||||
harness.run();
|
||||
|
||||
assert!(
|
||||
harness.state(),
|
||||
"The button was not clicked after scrolling down. (Probably not scrolled enough / at all)"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue