diff --git a/core/src/debug_ui/display_object.rs b/core/src/debug_ui/display_object.rs index 80f56692f..ba5bc06c6 100644 --- a/core/src/debug_ui/display_object.rs +++ b/core/src/debug_ui/display_object.rs @@ -709,16 +709,11 @@ impl DisplayObjectWindow { }); let tab_order = focus_tracker.tab_order(context); - let tab_order_suffix = tab_order - .first() - .map(|o| { - if o.tab_index().is_some() { - "custom" - } else { - "automatic" - } - }) - .unwrap_or("empty"); + let tab_order_suffix = if tab_order.is_custom() { + "custom" + } else { + "automatic" + }; CollapsingHeader::new(format!("Tab Order ({})", tab_order_suffix)) .id_source(ui.id().with("tab_order")) .show(ui, |ui| { diff --git a/core/src/display_object/container.rs b/core/src/display_object/container.rs index 284e8838b..6740f0927 100644 --- a/core/src/display_object/container.rs +++ b/core/src/display_object/container.rs @@ -10,9 +10,8 @@ use crate::display_object::avm1_button::Avm1Button; use crate::display_object::loader_display::LoaderDisplay; use crate::display_object::movie_clip::MovieClip; use crate::display_object::stage::Stage; -use crate::display_object::{ - Depth, DisplayObject, InteractiveObject, TDisplayObject, TInteractiveObject, -}; +use crate::display_object::{Depth, DisplayObject, TDisplayObject, TInteractiveObject}; +use crate::focus_tracker::TabOrder; use crate::string::WStr; use crate::tag_utils::SwfMovie; use gc_arena::{Collect, Mutation}; @@ -512,11 +511,7 @@ pub trait TDisplayObjectContainer<'gc>: } } - fn fill_tab_order( - &self, - tab_order: &mut Vec>, - context: &mut UpdateContext<'_, 'gc>, - ) { + fn fill_tab_order(&self, tab_order: &mut TabOrder<'gc>, context: &mut UpdateContext<'_, 'gc>) { if !self.is_tab_children(context) { // AS3 docs say that objects with custom ordering (tabIndex set) // are included even when tabChildren is false. @@ -531,7 +526,7 @@ pub trait TDisplayObjectContainer<'gc>: } if let Some(child) = child.as_interactive() { if child.is_tabbable(context) { - tab_order.push(child); + tab_order.add_object(child); } } if let Some(container) = child.as_container() { diff --git a/core/src/focus_tracker.rs b/core/src/focus_tracker.rs index c18868d47..2f87ec1fe 100644 --- a/core/src/focus_tracker.rs +++ b/core/src/focus_tracker.rs @@ -5,14 +5,15 @@ pub use crate::display_object::{ DisplayObject, TDisplayObject, TDisplayObjectContainer, TextSelection, }; use crate::display_object::{EditText, InteractiveObject, TInteractiveObject}; -use crate::events::ClipEvent; +use crate::events::{ClipEvent, KeyCode}; use crate::Player; use either::Either; use gc_arena::barrier::unlock; use gc_arena::lock::Lock; use gc_arena::{Collect, Gc, Mutation}; use std::cell::RefCell; -use swf::{Color, Twips}; +use std::slice::Iter; +use swf::{Color, Rectangle, Twips}; #[derive(Collect)] #[collect(no_drop)] @@ -183,17 +184,9 @@ impl<'gc> FocusTracker<'gc> { } } - pub fn tab_order(&self, context: &mut UpdateContext<'_, 'gc>) -> Vec> { - let stage = context.stage; - let mut tab_order = vec![]; - stage.fill_tab_order(&mut tab_order, context); - - let custom_ordering = tab_order.iter().any(|o| o.tab_index().is_some()); - if custom_ordering { - Self::order_custom(&mut tab_order); - } else { - Self::order_automatic(&mut tab_order); - }; + pub fn tab_order(&self, context: &mut UpdateContext<'_, 'gc>) -> TabOrder<'gc> { + let mut tab_order = TabOrder::fill(context); + tab_order.sort(); tab_order } @@ -278,61 +271,152 @@ impl<'gc> FocusTracker<'gc> { let bounds = focus.highlight_bounds(); context.draw_rect_outline(Self::HIGHLIGHT_COLOR, bounds, Self::HIGHLIGHT_THICKNESS); } +} - fn order_custom(tab_order: &mut Vec) { - // Custom ordering disables automatic ordering and - // ignores all objects without tabIndex. - tab_order.retain(|o| o.tab_index().is_some()); +/// A list of interactive objects ordered +/// according to a specific tab order. +pub struct TabOrder<'gc> { + objects: Vec>, - // Then, items are sorted according to their tab indices. - // When two objects have the same index, they are ordered by - // their respective positions in hierarchy as returned by fill_tab_order(). - tab_order.sort_by_key(|o| o.tab_index()); + /// When any object has tab index set, objects without + /// tab indices are filtered out and this value is `true`. + is_custom: bool, +} + +impl<'gc> TabOrder<'gc> { + fn empty() -> Self { + Self { + objects: Vec::new(), + is_custom: false, + } } - /// The automatic ordering depends only on the position of - /// the top-left highlight bound corner, referred to as `(x,y)`. - /// It does not depend on object's size or other corners. - /// - /// The value of `6y+x` is used to order objects by it. - /// This means that the next object to be tabbed is the next one - /// that touches the line `y=-(x-p)/6` (with the smallest `p`). - /// - /// When two objects have the same value of `6y+x` - /// (i.e. when the line touches two objects at the same time), - /// only one of them is included. - /// - /// This behavior is similar to the naive approach of - /// "left-to-right, top-to-bottom", but (besides being sometimes - /// seen as random jumps) takes into account the fact that - /// the next object to the right may be positioned slightly higher. - /// This is especially true for objects placed by hand or objects with - /// different heights (as FP uses the top left corner instead of the center). - /// - /// This behavior has been discovered experimentally by placing - /// tabbable objects randomly and bisecting one of their - /// coordinates to find a difference in behavior. - /// - /// See the test `avm2/tab_ordering_automatic_advanced`. - /// - /// *WARNING:* Be careful when testing automatic order in FP, - /// as its behavior is slightly different with a zoom other than 100%. - fn order_automatic(tab_order: &mut Vec) { - fn key_extractor(o: &InteractiveObject) -> i64 { - let bounds = o.highlight_bounds(); + fn fill(context: &mut UpdateContext<'_, 'gc>) -> Self { + let stage = context.stage; + let mut tab_order = Self::empty(); + stage.fill_tab_order(&mut tab_order, context); + tab_order + } - let x = bounds.x_min.get() as i64; - let y = bounds.y_min.get() as i64; + pub fn is_custom(&self) -> bool { + self.is_custom + } - y * 6 + x + pub fn iter(&self) -> Iter<'_, InteractiveObject<'gc>> { + self.objects.iter() + } + + pub fn add_object(&mut self, object: InteractiveObject<'gc>) { + let has_tab_index = object.tab_index().is_some(); + if has_tab_index && !self.is_custom { + // If an object has tab index, we have to switch to a custom order, + // and retain only objects with tab index, even for keyboard navigation. + self.is_custom = true; + self.objects.retain(|&o| o.tab_index().is_some()); } - tab_order.sort_by_cached_key(key_extractor); + if has_tab_index || !self.is_custom { + self.objects.push(object); + } + } + fn sort(&mut self) { + if self.is_custom() { + self.sort_with(CustomTabOrdering); + } else { + self.sort_with(AutomaticTabOrdering); + } + } + + fn sort_with(&mut self, ordering: impl TabOrdering) { + self.objects.sort_by_cached_key(|&o| ordering.key(o)); + + let to_skip = self + .objects + .iter() + .take_while(|&&o| ordering.key(o).is_none()) + .count(); + self.objects.drain(..to_skip); + + if ordering.ignore_duplicates() { + self.objects.dedup_by_key(|&mut o| ordering.key(o)); + } + } + + fn first(&self, ordering: impl TabOrdering) -> Option> { + self.objects + .iter() + .filter(|&&object| ordering.key(object).is_some()) + .min_by_key(|&&object| ordering.key(object)) + .cloned() + } +} + +trait TabOrdering { + fn key(&self, object: InteractiveObject) -> Option; + + fn ignore_duplicates(&self) -> bool; +} + +/// In custom ordering, items are sorted according to their tab indices. +/// When two objects have the same index, they are ordered by +/// their respective positions in hierarchy +struct CustomTabOrdering; + +impl TabOrdering for CustomTabOrdering { + fn key(&self, object: InteractiveObject) -> Option { + object.tab_index() + } + + fn ignore_duplicates(&self) -> bool { + false + } +} + +/// The automatic ordering depends only on the position of +/// the top-left highlight bound corner, referred to as `(x,y)`. +/// It does not depend on object's size or other corners. +/// +/// The value of `6y+x` is used to order objects by it. +/// This means that the next object to be tabbed is the next one +/// that touches the line `y=-(x-p)/6` (with the smallest `p`). +/// +/// When two objects have the same value of `6y+x` +/// (i.e. when the line touches two objects at the same time), +/// only one of them is included. +/// +/// This behavior is similar to the naive approach of +/// "left-to-right, top-to-bottom", but (besides being sometimes +/// seen as random jumps) takes into account the fact that +/// the next object to the right may be positioned slightly higher. +/// This is especially true for objects placed by hand or objects with +/// different heights (as FP uses the top left corner instead of the center). +/// +/// This behavior has been discovered experimentally by placing +/// tabbable objects randomly and bisecting one of their +/// coordinates to find a difference in behavior. +/// +/// See the test `avm2/tab_ordering_automatic_advanced`. +/// +/// *WARNING:* Be careful when testing automatic order in FP, +/// as its behavior is slightly different with a zoom other than 100%. +struct AutomaticTabOrdering; + +impl TabOrdering for AutomaticTabOrdering { + fn key(&self, object: InteractiveObject) -> Option { + let bounds = object.highlight_bounds(); + + let x = bounds.x_min.get() as i64; + let y = bounds.y_min.get() as i64; + + Some(y * 6 + x) + } + + fn ignore_duplicates(&self) -> bool { // Objects with duplicate keys are removed, retaining only // the first instance with respect to the order of fill_tab_order(). // This of course causes some objects to be skipped, even if far from one another, // but that's unfortunately how FP behaves. - tab_order.dedup_by_key(|o| key_extractor(o)); + true } }