diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 22ab67d3..1c3e058b 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -595,6 +595,7 @@ impl Prepared { .layer_id(self.layer_id) .max_rect(max_rect) .layout(self.layout) + .accessibility_parent(self.move_response.id) .closable(); if !self.enabled { diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 15d1b32b..6e582b42 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -19,7 +19,8 @@ use emath::GuiRounding as _; use crate::{ Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef, - Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2, + Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetType, lerp, + vec2, }; fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 { @@ -390,6 +391,9 @@ impl SidePanel { .max_rect(available_rect), ); panel_ui.set_clip_rect(ctx.content_rect()); + panel_ui + .response() + .widget_info(|| WidgetInfo::new(WidgetType::Panel)); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index b5ac1cc4..da7e65c1 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -505,15 +505,14 @@ impl Window<'_> { // First check for resize to avoid frame delay: let last_frame_outer_rect = area.state().rect(); - let resize_interaction = ctx.with_accessibility_parent(area.id(), || { - resize_interaction( - ctx, - possible, - area_layer_id, - last_frame_outer_rect, - window_frame, - ) - }); + let resize_interaction = resize_interaction( + ctx, + possible, + area.id(), + area_layer_id, + last_frame_outer_rect, + window_frame, + ); { let margins = window_frame.total_margin().sum() @@ -538,109 +537,107 @@ impl Window<'_> { } let content_inner = { - ctx.with_accessibility_parent(area.id(), || { - // BEGIN FRAME -------------------------------- - let mut frame = window_frame.begin(&mut area_content_ui); + // BEGIN FRAME -------------------------------- + let mut frame = window_frame.begin(&mut area_content_ui); - let show_close_button = open.is_some(); + let show_close_button = open.is_some(); - let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); + let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); - let title_bar = if with_title_bar { - let title_bar = TitleBar::new( - &frame.content_ui, - title, - show_close_button, - collapsible, - window_frame, - title_bar_height_with_margin, - ); - resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width - - frame.content_ui.set_min_size(title_bar.inner_rect.size()); - - // Skip the title bar (and separator): - if is_collapsed { - frame.content_ui.add_space(title_bar.inner_rect.height()); - } else { - frame.content_ui.add_space( - title_bar.inner_rect.height() - + title_content_spacing - + window_frame.inner_margin.sum().y, - ); - } - - Some(title_bar) - } else { - None - }; - - let (content_inner, content_response) = collapsing - .show_body_unindented(&mut frame.content_ui, |ui| { - resize.show(ui, |ui| { - if scroll.is_any_scroll_enabled() { - scroll.show(ui, add_contents).inner - } else { - add_contents(ui) - } - }) - }) - .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); - - let outer_rect = frame.end(&mut area_content_ui).rect; - paint_resize_corner( - &area_content_ui, - &possible, - outer_rect, - &window_frame, - resize_interaction, + let title_bar = if with_title_bar { + let title_bar = TitleBar::new( + &frame.content_ui, + title, + show_close_button, + collapsible, + window_frame, + title_bar_height_with_margin, ); + resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width - // END FRAME -------------------------------- + frame.content_ui.set_min_size(title_bar.inner_rect.size()); - if let Some(mut title_bar) = title_bar { - title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width); - title_bar.inner_rect.max.y = - title_bar.inner_rect.min.y + title_bar_height_with_margin; - - if on_top && area_content_ui.visuals().window_highlight_topmost { - let mut round = - window_frame.corner_radius - window_frame.stroke.width.round() as u8; - - if !is_collapsed { - round.se = 0; - round.sw = 0; - } - - area_content_ui.painter().set( - *where_to_put_header_background, - RectShape::filled(title_bar.inner_rect, round, header_color), - ); - } - - if false { - ctx.debug_painter().debug_rect( - title_bar.inner_rect, - Color32::LIGHT_BLUE, - "title_bar.rect", - ); - } - - title_bar.ui( - &mut area_content_ui, - &content_response, - open.as_deref_mut(), - &mut collapsing, - collapsible, + // Skip the title bar (and separator): + if is_collapsed { + frame.content_ui.add_space(title_bar.inner_rect.height()); + } else { + frame.content_ui.add_space( + title_bar.inner_rect.height() + + title_content_spacing + + window_frame.inner_margin.sum().y, ); } - collapsing.store(ctx); + Some(title_bar) + } else { + None + }; - paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); + let (content_inner, content_response) = collapsing + .show_body_unindented(&mut frame.content_ui, |ui| { + resize.show(ui, |ui| { + if scroll.is_any_scroll_enabled() { + scroll.show(ui, add_contents).inner + } else { + add_contents(ui) + } + }) + }) + .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); - content_inner - }) + let outer_rect = frame.end(&mut area_content_ui).rect; + paint_resize_corner( + &area_content_ui, + &possible, + outer_rect, + &window_frame, + resize_interaction, + ); + + // END FRAME -------------------------------- + + if let Some(mut title_bar) = title_bar { + title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width); + title_bar.inner_rect.max.y = + title_bar.inner_rect.min.y + title_bar_height_with_margin; + + if on_top && area_content_ui.visuals().window_highlight_topmost { + let mut round = + window_frame.corner_radius - window_frame.stroke.width.round() as u8; + + if !is_collapsed { + round.se = 0; + round.sw = 0; + } + + area_content_ui.painter().set( + *where_to_put_header_background, + RectShape::filled(title_bar.inner_rect, round, header_color), + ); + } + + if false { + ctx.debug_painter().debug_rect( + title_bar.inner_rect, + Color32::LIGHT_BLUE, + "title_bar.rect", + ); + } + + title_bar.ui( + &mut area_content_ui, + &content_response, + open.as_deref_mut(), + &mut collapsing, + collapsible, + ); + } + + collapsing.store(ctx); + + paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); + + content_inner }; let full_response = area.end(ctx, area_content_ui); @@ -882,6 +879,7 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt fn resize_interaction( ctx: &Context, possible: PossibleInteractions, + _accessibility_parent: Id, layer_id: LayerId, outer_rect: Rect, window_frame: Frame, @@ -901,6 +899,8 @@ fn resize_interaction( let rect = outer_rect.shrink(window_frame.stroke.width / 2.0); let side_response = |rect, id| { + #[cfg(feature = "accesskit")] + ctx.register_accesskit_parent(id, _accessibility_parent); let response = ctx.create_widget( WidgetRect { layer_id, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 2e36bdde..aab81ab2 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -519,7 +519,7 @@ impl ContextImpl { nodes.insert(id, root_node); viewport.this_pass.accesskit_state = Some(AccessKitPassState { nodes, - parent_stack: vec![id], + parent_map: IdMap::default(), }); } @@ -595,8 +595,28 @@ impl ContextImpl { let builders = &mut state.nodes; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); - let parent_id = state.parent_stack.last().unwrap(); - let parent_builder = builders.get_mut(parent_id).unwrap(); + + /// Find the first ancestor that already has an accesskit node. + fn find_accesskit_parent( + parent_map: &IdMap, + node_map: &IdMap, + id: Id, + ) -> Option { + if let Some(parent_id) = parent_map.get(&id) { + if node_map.contains_key(parent_id) { + Some(*parent_id) + } else { + find_accesskit_parent(parent_map, node_map, *parent_id) + } + } else { + None + } + } + + let parent_id = find_accesskit_parent(&state.parent_map, builders, id) + .unwrap_or(crate::accesskit_root_id()); + + let parent_builder = builders.get_mut(&parent_id).unwrap(); parent_builder.push_child(id.accesskit_id()); } builders.get_mut(&id).unwrap() @@ -3464,43 +3484,10 @@ impl Context { /// ## Accessibility impl Context { - /// Call the provided function with the given ID pushed on the stack of - /// parent IDs for accessibility purposes. If the `accesskit` feature - /// is disabled or if AccessKit support is not active for this frame, - /// the function is still called, but with no other effect. - /// - /// No locks are held while the given closure is called. - #[allow(clippy::unused_self, clippy::let_and_return, clippy::allow_attributes)] - #[inline] - pub fn with_accessibility_parent(&self, _id: Id, f: impl FnOnce() -> R) -> R { - // TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls - #[cfg(feature = "accesskit")] - self.pass_state_mut(|fs| { - if let Some(state) = fs.accesskit_state.as_mut() { - state.parent_stack.push(_id); - } - }); - - let result = f(); - - #[cfg(feature = "accesskit")] - self.pass_state_mut(|fs| { - if let Some(state) = fs.accesskit_state.as_mut() { - assert_eq!( - state.parent_stack.pop(), - Some(_id), - "Mismatched push/pop in with_accessibility_parent" - ); - } - }); - - result - } - /// If AccessKit support is active for the current frame, get or create /// a node builder with the specified ID and return a mutable reference to it. - /// For newly created nodes, the parent is the node with the ID at the top - /// of the stack managed by [`Context::with_accessibility_parent`]. + /// For newly created nodes, the parent is the parent [`Ui`]s ID. + /// And an [`Ui`]s parent can be set with [`crate::UiBuilder::accessibility_parent`]. /// /// The `Context` lock is held while the given closure is called! /// @@ -3522,6 +3509,15 @@ impl Context { }) } + #[cfg(feature = "accesskit")] + pub(crate) fn register_accesskit_parent(&self, id: Id, parent_id: Id) { + self.write(|ctx| { + if let Some(state) = ctx.viewport().this_pass.accesskit_state.as_mut() { + state.parent_map.insert(id, parent_id); + } + }); + } + /// Enable generation of AccessKit tree updates in all future frames. #[cfg(feature = "accesskit")] pub fn enable_accesskit(&self) { diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index bd7f6291..deec5162 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -682,6 +682,7 @@ impl WidgetInfo { WidgetType::ColorButton => "color button", WidgetType::Image => "image", WidgetType::CollapsingHeader => "collapsing header", + WidgetType::Panel => "panel", WidgetType::ProgressIndicator => "progress indicator", WidgetType::Window => "window", WidgetType::Label | WidgetType::Other => "", diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c62d7c16..960480b2 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -674,6 +674,8 @@ pub enum WidgetType { CollapsingHeader, + Panel, + ProgressIndicator, Window, diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index 66a1f6e1..2be7e509 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -71,7 +71,7 @@ impl ScrollTarget { #[derive(Clone)] pub struct AccessKitPassState { pub nodes: IdMap, - pub parent_stack: Vec, + pub parent_map: IdMap, } #[cfg(debug_assertions)] diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 06ac7bc0..e17c1aff 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -858,6 +858,7 @@ impl Response { WidgetType::Slider => Role::Slider, WidgetType::DragValue => Role::SpinButton, WidgetType::ColorButton => Role::ColorWell, + WidgetType::Panel => Role::Pane, WidgetType::ProgressIndicator => Role::ProgressIndicator, WidgetType::Window => Role::Window, WidgetType::Other => Role::Unknown, diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index b1899554..4d64229c 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -40,60 +40,60 @@ pub fn update_accesskit_for_text_widget( return; }; - ctx.with_accessibility_parent(parent_id, || { - for (row_index, row) in galley.rows.iter().enumerate() { - let row_id = parent_id.with(row_index); - ctx.accesskit_node_builder(row_id, |builder| { - builder.set_role(accesskit::Role::TextRun); - let rect = global_from_galley * row.rect_without_leading_space(); - builder.set_bounds(accesskit::Rect { - x0: rect.min.x.into(), - y0: rect.min.y.into(), - x1: rect.max.x.into(), - y1: rect.max.y.into(), - }); - builder.set_text_direction(accesskit::TextDirection::LeftToRight); - // TODO(mwcampbell): Set more node fields for the row - // once AccessKit adapters expose text formatting info. - - let glyph_count = row.glyphs.len(); - let mut value = String::new(); - value.reserve(glyph_count); - let mut character_lengths = Vec::::with_capacity(glyph_count); - let mut character_positions = Vec::::with_capacity(glyph_count); - let mut character_widths = Vec::::with_capacity(glyph_count); - let mut word_lengths = Vec::::new(); - let mut was_at_word_end = false; - let mut last_word_start = 0usize; - - for glyph in &row.glyphs { - let is_word_char = is_word_char(glyph.chr); - if is_word_char && was_at_word_end { - word_lengths.push((character_lengths.len() - last_word_start) as _); - last_word_start = character_lengths.len(); - } - was_at_word_end = !is_word_char; - let old_len = value.len(); - value.push(glyph.chr); - character_lengths.push((value.len() - old_len) as _); - character_positions.push(glyph.pos.x - row.pos.x); - character_widths.push(glyph.advance_width); - } - - if row.ends_with_newline { - value.push('\n'); - character_lengths.push(1); - character_positions.push(row.size.x); - character_widths.push(0.0); - } - word_lengths.push((character_lengths.len() - last_word_start) as _); - - builder.set_value(value); - builder.set_character_lengths(character_lengths); - builder.set_character_positions(character_positions); - builder.set_character_widths(character_widths); - builder.set_word_lengths(word_lengths); + for (row_index, row) in galley.rows.iter().enumerate() { + let row_id = parent_id.with(row_index); + #[cfg(feature = "accesskit")] + ctx.register_accesskit_parent(row_id, parent_id); + ctx.accesskit_node_builder(row_id, |builder| { + builder.set_role(accesskit::Role::TextRun); + let rect = global_from_galley * row.rect_without_leading_space(); + builder.set_bounds(accesskit::Rect { + x0: rect.min.x.into(), + y0: rect.min.y.into(), + x1: rect.max.x.into(), + y1: rect.max.y.into(), }); - } - }); + builder.set_text_direction(accesskit::TextDirection::LeftToRight); + // TODO(mwcampbell): Set more node fields for the row + // once AccessKit adapters expose text formatting info. + + let glyph_count = row.glyphs.len(); + let mut value = String::new(); + value.reserve(glyph_count); + let mut character_lengths = Vec::::with_capacity(glyph_count); + let mut character_positions = Vec::::with_capacity(glyph_count); + let mut character_widths = Vec::::with_capacity(glyph_count); + let mut word_lengths = Vec::::new(); + let mut was_at_word_end = false; + let mut last_word_start = 0usize; + + for glyph in &row.glyphs { + let is_word_char = is_word_char(glyph.chr); + if is_word_char && was_at_word_end { + word_lengths.push((character_lengths.len() - last_word_start) as _); + last_word_start = character_lengths.len(); + } + was_at_word_end = !is_word_char; + let old_len = value.len(); + value.push(glyph.chr); + character_lengths.push((value.len() - old_len) as _); + character_positions.push(glyph.pos.x - row.pos.x); + character_widths.push(glyph.advance_width); + } + + if row.ends_with_newline { + value.push('\n'); + character_lengths.push(1); + character_positions.push(row.size.x); + character_widths.push(0.0); + } + word_lengths.push((character_lengths.len() - last_word_start) as _); + + builder.set_value(value); + builder.set_character_lengths(character_lengths); + builder.set_character_positions(character_positions); + builder.set_character_widths(character_widths); + builder.set_word_lengths(word_lengths); + }); + } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index e2d89c0b..d746b8fe 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -133,6 +133,8 @@ impl Ui { sizing_pass, style, sense, + #[cfg(feature = "accesskit")] + accessibility_parent, } = ui_builder; let layer_id = layer_id.unwrap_or(LayerId::background()); @@ -173,6 +175,12 @@ impl Ui { min_rect_already_remembered: false, }; + #[cfg(feature = "accesskit")] + if let Some(accessibility_parent) = accessibility_parent { + ui.ctx() + .register_accesskit_parent(ui.unique_id, accessibility_parent); + } + // Register in the widget stack early, to ensure we are behind all widgets we contain: let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called ui.ctx().create_widget( @@ -194,6 +202,11 @@ impl Ui { ui.set_invisible(); } + #[cfg(feature = "accesskit")] + ui.ctx().accesskit_node_builder(ui.unique_id, |node| { + node.set_role(accesskit::Role::GenericContainer); + }); + ui } @@ -260,6 +273,8 @@ impl Ui { sizing_pass, style, sense, + #[cfg(feature = "accesskit")] + accessibility_parent, } = ui_builder; let mut painter = self.painter.clone(); @@ -328,6 +343,12 @@ impl Ui { child_ui.disable(); } + #[cfg(feature = "accesskit")] + child_ui.ctx().register_accesskit_parent( + child_ui.unique_id, + accessibility_parent.unwrap_or(self.unique_id), + ); + // Register in the widget stack early, to ensure we are behind all widgets we contain: let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called child_ui.ctx().create_widget( @@ -342,6 +363,13 @@ impl Ui { true, ); + #[cfg(feature = "accesskit")] + child_ui + .ctx() + .accesskit_node_builder(child_ui.unique_id, |node| { + node.set_role(accesskit::Role::GenericContainer); + }); + child_ui } @@ -1101,6 +1129,9 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { + #[cfg(feature = "accesskit")] + self.ctx().register_accesskit_parent(id, self.unique_id); + self.ctx().create_widget( WidgetRect { id, diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index e83148da..51b8ec8a 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -24,6 +24,8 @@ pub struct UiBuilder { pub sizing_pass: bool, pub style: Option>, pub sense: Option, + #[cfg(feature = "accesskit")] + pub accessibility_parent: Option, } impl UiBuilder { @@ -180,4 +182,20 @@ impl UiBuilder { .insert(ClosableTag::NAME, Some(Arc::new(ClosableTag::default()))); self } + + /// Set the accessibility parent for this [`Ui`]. + /// + /// This will override the automatic parent assignment for accessibility purposes. + /// If not set, the parent [`Ui`]'s ID will be used as the accessibility parent. + /// + /// This does nothing if the `accesskit` feature is not enabled. + #[cfg_attr(not(feature = "accesskit"), expect(unused_mut, unused_variables))] + #[inline] + pub fn accessibility_parent(mut self, parent_id: Id) -> Self { + #[cfg(feature = "accesskit")] + { + self.accessibility_parent = Some(parent_id); + } + self + } } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index df80149d..f721b710 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -87,23 +87,21 @@ impl egui::Plugin for AccessibilityInspectorPlugin { ctx.enable_accesskit(); SidePanel::right(Self::id()).show(ctx, |ui| { - let response = ui.heading("🔎 AccessKit Inspector"); - ctx.with_accessibility_parent(response.id, || { - if let Some(selected_node) = self.selected_node { - TopBottomPanel::bottom(Self::id().with("details_panel")) - .frame(Frame::new()) - .show_separator_line(false) - .show_inside(ui, |ui| { - self.selection_ui(ui, selected_node); - }); - } + ui.heading("🔎 AccessKit Inspector"); + if let Some(selected_node) = self.selected_node { + TopBottomPanel::bottom(Self::id().with("details_panel")) + .frame(Frame::new()) + .show_separator_line(false) + .show_inside(ui, |ui| { + self.selection_ui(ui, selected_node); + }); + } - ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); - ScrollArea::vertical().show(ui, |ui| { - if let Some(tree) = &self.tree { - Self::node_ui(ui, &tree.state().root(), &mut self.selected_node); - } - }); + ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); + ScrollArea::vertical().show(ui, |ui| { + if let Some(tree) = &self.tree { + Self::node_ui(ui, &tree.state().root(), &mut self.selected_node); + } }); }); } @@ -157,6 +155,7 @@ impl AccessibilityInspectorPlugin { ui.label("Children"); ui.label(RichText::new(node.children().len().to_string()).strong()); + ui.end_row(); }); diff --git a/crates/egui_kittest/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs index 3f1f33ba..08a96bd7 100644 --- a/crates/egui_kittest/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -18,9 +18,20 @@ fn empty_ui_should_return_tree_with_only_root_window() { assert_eq!( output.nodes.len(), - 1, - "Empty ui should produce only the root window." + 4, + "Expected the root node and two Uis and a Frame for the panel" ); + + assert_eq!( + output + .nodes + .iter() + .filter(|(_, n)| n.role() == Role::GenericContainer) + .count(), + 3, + "Expected two Uis and one Frame as GenericContainer nodes.", + ); + let (id, root) = &output.nodes[0]; assert_eq!(*id, output.tree.unwrap().root); @@ -35,12 +46,6 @@ fn button_node() { CentralPanel::default().show(ctx, |ui| ui.button(button_text)); }); - assert_eq!( - output.nodes.len(), - 2, - "Expected only the root node and the button." - ); - let (_, button) = output .nodes .iter() @@ -61,12 +66,6 @@ fn disabled_button_node() { }); }); - assert_eq!( - output.nodes.len(), - 2, - "Expected only the root node and the button." - ); - let (_, button) = output .nodes .iter() @@ -86,12 +85,6 @@ fn toggle_button_node() { CentralPanel::default().show(ctx, |ui| ui.toggle_value(&mut selected, button_text)); }); - assert_eq!( - output.nodes.len(), - 2, - "Expected only the root node and the button." - ); - let (_, toggle) = output .nodes .iter() @@ -114,12 +107,6 @@ fn multiple_disabled_widgets() { }); }); - assert_eq!( - output.nodes.len(), - 4, - "Expected the root node and all the child widgets." - ); - assert_eq!( output .nodes @@ -194,15 +181,25 @@ fn assert_window_exists(tree: &TreeUpdate, title: &str, parent: NodeId) -> NodeI } #[track_caller] -fn assert_parent_child(tree: &TreeUpdate, parent: NodeId, child: NodeId) { +fn assert_parent_child(tree: &TreeUpdate, parent_id: NodeId, child: NodeId) { + assert!( + has_child_recursively(tree, parent_id, child), + "Node is not a child of the given parent." + ); +} + +fn has_child_recursively(tree: &TreeUpdate, parent: NodeId, child: NodeId) -> bool { let (_, parent) = tree .nodes .iter() .find(|(id, _)| id == &parent) .expect("Parent does not exist."); - assert!( - parent.children().contains(&child), - "Node is not a child of the given parent." - ); + for &c in parent.children() { + if c == child || has_child_recursively(tree, c, child) { + return true; + } + } + + false } diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index a6050e95..6a75e36a 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,3 +1,4 @@ +use egui::accesskit::Role; use egui::load::SizedTexture; use egui::{ Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event, @@ -277,8 +278,8 @@ impl<'a> VisualTests<'a> { node.hover(); }); self.add("pressed", |harness| { - harness.get_next().hover(); - let rect = harness.get_next().rect(); + harness.get_next_widget().hover(); + let rect = harness.get_next_widget().rect(); harness.input_mut().events.push(Event::PointerButton { button: PointerButton::Primary, pos: rect.center(), @@ -329,7 +330,7 @@ impl<'a> VisualTests<'a> { pub fn add_node(&mut self, name: &str, test: impl FnOnce(&Node<'_>)) { self.add(name, |harness| { - let node = harness.get_next(); + let node = harness.get_next_widget(); test(&node); }); } @@ -375,11 +376,13 @@ impl<'a> VisualTests<'a> { } trait HarnessExt { - fn get_next(&self) -> Node<'_>; + fn get_next_widget(&self) -> Node<'_>; } impl HarnessExt for Harness<'_> { - fn get_next(&self) -> Node<'_> { - self.get_all(by()).next().unwrap() + fn get_next_widget(&self) -> Node<'_> { + self.get_all(by().predicate(|node| node.role() != Role::GenericContainer)) + .next() + .unwrap() } }