diff --git a/components/selectors/builder.rs b/components/selectors/builder.rs index 584a210152f..b17ed9bfc63 100644 --- a/components/selectors/builder.rs +++ b/components/selectors/builder.rs @@ -72,7 +72,7 @@ impl SelectorBuilder { /// Pushes a simple selector onto the current compound selector. #[inline(always)] pub fn push_simple_selector(&mut self, ss: Component) { - debug_assert!(!ss.is_combinator()); + assert!(!ss.is_combinator()); self.simple_selectors.push(ss); self.current_len += 1; } @@ -105,7 +105,7 @@ impl SelectorBuilder { parsed_slotted: bool, ) -> ThinArc> { // Compute the specificity and flags. - let mut spec = SpecificityAndFlags(specificity(&*self, self.simple_selectors.iter())); + let mut spec = SpecificityAndFlags(specificity(self.simple_selectors.iter())); if parsed_pseudo { spec.0 |= HAS_PSEUDO_BIT; } @@ -281,33 +281,26 @@ impl From for u32 { } } -fn specificity(builder: &SelectorBuilder, iter: slice::Iter>) -> u32 +fn specificity(iter: slice::Iter>) -> u32 where Impl: SelectorImpl, { - complex_selector_specificity(builder, iter).into() + complex_selector_specificity(iter).into() } -fn complex_selector_specificity( - builder: &SelectorBuilder, - mut iter: slice::Iter>, -) -> Specificity +fn complex_selector_specificity(iter: slice::Iter>) -> Specificity where Impl: SelectorImpl, { fn simple_selector_specificity( - builder: &SelectorBuilder, simple_selector: &Component, specificity: &mut Specificity, ) where Impl: SelectorImpl, { match *simple_selector { - Component::Combinator(ref combinator) => { - unreachable!( - "Found combinator {:?} in simple selectors vector? {:?}", - combinator, builder, - ); + Component::Combinator(..) => { + unreachable!("Found combinator in simple selectors vector?"); }, Component::PseudoElement(..) | Component::LocalName(..) => { specificity.element_selectors += 1 @@ -361,15 +354,15 @@ where }, Component::Negation(ref negated) => { for ss in negated.iter() { - simple_selector_specificity(builder, &ss, specificity); + simple_selector_specificity(&ss, specificity); } }, } } let mut specificity = Default::default(); - for simple_selector in &mut iter { - simple_selector_specificity(builder, &simple_selector, &mut specificity); + for simple_selector in iter { + simple_selector_specificity(&simple_selector, &mut specificity); } specificity } diff --git a/components/style/animation.rs b/components/style/animation.rs index 5d96f5cacd1..70db7c2c5b7 100644 --- a/components/style/animation.rs +++ b/components/style/animation.rs @@ -27,10 +27,10 @@ use std::sync::mpsc::Sender; use stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue}; use timer::Timer; use values::computed::Time; +use values::computed::TimingFunction; use values::computed::box_::TransitionProperty; -use values::computed::transform::TimingFunction; use values::generics::box_::AnimationIterationCount; -use values::generics::transform::{StepPosition, TimingFunction as GenericTimingFunction}; +use values::generics::easing::{StepPosition, TimingFunction as GenericTimingFunction}; /// This structure represents a keyframes animation current iteration state. @@ -363,27 +363,39 @@ impl PropertyAnimation { GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => { Bezier::new(x1, y1, x2, y2).solve(time, epsilon) }, - GenericTimingFunction::Steps(steps, StepPosition::Start) => { - (time * (steps as f64)).ceil() / (steps as f64) - }, - GenericTimingFunction::Steps(steps, StepPosition::End) => { - (time * (steps as f64)).floor() / (steps as f64) - }, - GenericTimingFunction::Frames(frames) => { - // https://drafts.csswg.org/css-timing/#frames-timing-functions - let mut out = (time * (frames as f64)).floor() / ((frames - 1) as f64); - if out > 1.0 { - // FIXME: Basically, during the animation sampling process, the input progress - // should be in the range of [0, 1]. However, |time| is not accurate enough - // here, which means |time| could be larger than 1.0 in the last animation - // frame. (It should be equal to 1.0 exactly.) This makes the output of frames - // timing function jumps to the next frame/level. - // However, this solution is still not correct because |time| is possible - // outside the range of [0, 1] after introducing Web Animations. We should fix - // this problem when implementing web animations. - out = 1.0; + GenericTimingFunction::Steps(steps, pos) => { + let mut current_step = (time * (steps as f64)).floor() as i32; + + if pos == StepPosition::Start || + pos == StepPosition::JumpStart || + pos == StepPosition::JumpBoth { + current_step = current_step + 1; } - out + + // FIXME: We should update current_step according to the "before flag". + // In order to get the before flag, we have to know the current animation phase + // and whether the iteration is reversed. For now, we skip this calculation. + // (i.e. Treat before_flag is unset,) + // https://drafts.csswg.org/css-easing/#step-timing-function-algo + + if time >= 0.0 && current_step < 0 { + current_step = 0; + } + + let jumps = match pos { + StepPosition::JumpBoth => steps + 1, + StepPosition::JumpNone => steps - 1, + StepPosition::JumpStart | + StepPosition::JumpEnd | + StepPosition::Start | + StepPosition::End => steps, + }; + + if time <= 1.0 && current_step > jumps { + current_step = jumps; + } + + (current_step as f64) / (jumps as f64) }, GenericTimingFunction::Keyword(keyword) => { let (x1, x2, y1, y2) = keyword.to_bezier(); diff --git a/components/style/cbindgen.toml b/components/style/cbindgen.toml index 8bcba69c057..1c8d14e16e8 100644 --- a/components/style/cbindgen.toml +++ b/components/style/cbindgen.toml @@ -42,12 +42,14 @@ include = [ "StyleComputedFontStretchRange", "StyleComputedFontStyleDescriptor", "StyleComputedFontWeightRange", + "StyleComputedTimingFunction", "StyleDisplay", "StyleDisplayMode", "StyleFillRule", "StyleFontDisplay", "StyleFontFaceSourceListComponent", "StyleFontLanguageOverride", + "StyleTimingFunction", "StylePathCommand", "StyleUnicodeRange", ] diff --git a/components/style/gecko/conversions.rs b/components/style/gecko/conversions.rs index 10b177558fa..dff1aef30cc 100644 --- a/components/style/gecko/conversions.rs +++ b/components/style/gecko/conversions.rs @@ -1032,13 +1032,13 @@ impl TrackSize { match *self { TrackSize::FitContent(ref lop) => { // Gecko sets min value to None and max value to the actual value in fit-content - // https://dxr.mozilla.org/mozilla-central/rev/0eef1d5/layout/style/nsRuleNode.cpp#8221 + // https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#7910 gecko_min.set_value(CoordDataValue::None); lop.to_gecko_style_coord(gecko_max); }, TrackSize::Breadth(ref breadth) => { // Set the value to both fields if there's one breadth value - // https://dxr.mozilla.org/mozilla-central/rev/0eef1d5/layout/style/nsRuleNode.cpp#8230 + // https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#7919 breadth.to_gecko_style_coord(gecko_min); breadth.to_gecko_style_coord(gecko_max); }, diff --git a/components/style/gecko_bindings/sugar/mod.rs b/components/style/gecko_bindings/sugar/mod.rs index a455cf2b714..6ca9881b750 100644 --- a/components/style/gecko_bindings/sugar/mod.rs +++ b/components/style/gecko_bindings/sugar/mod.rs @@ -12,7 +12,6 @@ pub mod ns_css_value; mod ns_style_auto_array; pub mod ns_style_coord; mod ns_t_array; -mod ns_timing_function; pub mod origin_flags; pub mod ownership; pub mod refptr; diff --git a/components/style/gecko_bindings/sugar/ns_timing_function.rs b/components/style/gecko_bindings/sugar/ns_timing_function.rs deleted file mode 100644 index 635b1f86768..00000000000 --- a/components/style/gecko_bindings/sugar/ns_timing_function.rs +++ /dev/null @@ -1,160 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use gecko_bindings::structs::{nsTimingFunction, nsTimingFunction_Type}; -use std::mem; -use values::computed::ToComputedValue; -use values::computed::transform::TimingFunction as ComputedTimingFunction; -use values::generics::transform::{StepPosition, TimingKeyword}; -use values::generics::transform::TimingFunction as GenericTimingFunction; -use values::specified::transform::TimingFunction; - -impl nsTimingFunction { - fn set_as_step(&mut self, function_type: nsTimingFunction_Type, steps: u32) { - debug_assert!( - function_type == nsTimingFunction_Type::StepStart || - function_type == nsTimingFunction_Type::StepEnd, - "function_type should be step-start or step-end" - ); - self.mType = function_type; - unsafe { - self.__bindgen_anon_1 - .__bindgen_anon_1 - .as_mut() - .mStepsOrFrames = steps; - } - } - - fn set_as_frames(&mut self, frames: u32) { - self.mType = nsTimingFunction_Type::Frames; - unsafe { - self.__bindgen_anon_1 - .__bindgen_anon_1 - .as_mut() - .mStepsOrFrames = frames; - } - } - - fn set_as_bezier( - &mut self, - function_type: nsTimingFunction_Type, - x1: f32, - y1: f32, - x2: f32, - y2: f32, - ) { - self.mType = function_type; - unsafe { - let ref mut gecko_cubic_bezier = self.__bindgen_anon_1.mFunc.as_mut(); - gecko_cubic_bezier.mX1 = x1; - gecko_cubic_bezier.mY1 = y1; - gecko_cubic_bezier.mX2 = x2; - gecko_cubic_bezier.mY2 = y2; - } - } -} - -impl From for nsTimingFunction { - fn from(function: ComputedTimingFunction) -> nsTimingFunction { - TimingFunction::from_computed_value(&function).into() - } -} - -impl From for nsTimingFunction { - fn from(function: TimingFunction) -> nsTimingFunction { - let mut tf: nsTimingFunction = unsafe { mem::zeroed() }; - - match function { - GenericTimingFunction::Steps(steps, StepPosition::Start) => { - debug_assert!(steps.value() >= 0); - tf.set_as_step(nsTimingFunction_Type::StepStart, steps.value() as u32); - }, - GenericTimingFunction::Steps(steps, StepPosition::End) => { - debug_assert!(steps.value() >= 0); - tf.set_as_step(nsTimingFunction_Type::StepEnd, steps.value() as u32); - }, - GenericTimingFunction::Frames(frames) => { - debug_assert!(frames.value() >= 2); - tf.set_as_frames(frames.value() as u32); - }, - GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => { - tf.set_as_bezier( - nsTimingFunction_Type::CubicBezier, - x1.get(), - y1.get(), - x2.get(), - y2.get(), - ); - }, - GenericTimingFunction::Keyword(keyword) => { - let (x1, y1, x2, y2) = keyword.to_bezier(); - tf.set_as_bezier(keyword.into(), x1, y1, x2, y2); - }, - } - tf - } -} - -impl From for ComputedTimingFunction { - fn from(function: nsTimingFunction) -> ComputedTimingFunction { - match function.mType { - nsTimingFunction_Type::StepStart => GenericTimingFunction::Steps( - unsafe { - function - .__bindgen_anon_1 - .__bindgen_anon_1 - .as_ref() - .mStepsOrFrames - }, - StepPosition::Start, - ), - nsTimingFunction_Type::StepEnd => GenericTimingFunction::Steps( - unsafe { - function - .__bindgen_anon_1 - .__bindgen_anon_1 - .as_ref() - .mStepsOrFrames - }, - StepPosition::End, - ), - nsTimingFunction_Type::Frames => GenericTimingFunction::Frames(unsafe { - function - .__bindgen_anon_1 - .__bindgen_anon_1 - .as_ref() - .mStepsOrFrames - }), - nsTimingFunction_Type::Ease => GenericTimingFunction::Keyword(TimingKeyword::Ease), - nsTimingFunction_Type::Linear => GenericTimingFunction::Keyword(TimingKeyword::Linear), - nsTimingFunction_Type::EaseIn => GenericTimingFunction::Keyword(TimingKeyword::EaseIn), - nsTimingFunction_Type::EaseOut => { - GenericTimingFunction::Keyword(TimingKeyword::EaseOut) - }, - nsTimingFunction_Type::EaseInOut => { - GenericTimingFunction::Keyword(TimingKeyword::EaseInOut) - }, - nsTimingFunction_Type::CubicBezier => unsafe { - GenericTimingFunction::CubicBezier { - x1: function.__bindgen_anon_1.mFunc.as_ref().mX1, - y1: function.__bindgen_anon_1.mFunc.as_ref().mY1, - x2: function.__bindgen_anon_1.mFunc.as_ref().mX2, - y2: function.__bindgen_anon_1.mFunc.as_ref().mY2, - } - }, - } - } -} - -impl From for nsTimingFunction_Type { - fn from(keyword: TimingKeyword) -> Self { - match keyword { - TimingKeyword::Linear => nsTimingFunction_Type::Linear, - TimingKeyword::Ease => nsTimingFunction_Type::Ease, - TimingKeyword::EaseIn => nsTimingFunction_Type::EaseIn, - TimingKeyword::EaseOut => nsTimingFunction_Type::EaseOut, - TimingKeyword::EaseInOut => nsTimingFunction_Type::EaseInOut, - } - } -} diff --git a/components/style/properties/gecko.mako.rs b/components/style/properties/gecko.mako.rs index 04e65b5fa7f..19d5ec8baba 100644 --- a/components/style/properties/gecko.mako.rs +++ b/components/style/properties/gecko.mako.rs @@ -2394,7 +2394,7 @@ fn static_assert() { /// from the parent. /// /// This is a port of Gecko's old ComputeScriptLevelSize function: - /// https://dxr.mozilla.org/mozilla-central/rev/35fbf14b9/layout/style/nsRuleNode.cpp#3197-3254 + /// https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3103 /// /// scriptlevel is a property that affects how font-size is inherited. If scriptlevel is /// +1, for example, it will inherit as the script size multiplier times @@ -2855,7 +2855,7 @@ fn static_assert() { ${impl_simple_copy('_moz_min_font_size_ratio', 'mMinFontSizeRatio')} -<%def name="impl_copy_animation_or_transition_value(type, ident, gecko_ffi_name)"> +<%def name="impl_copy_animation_or_transition_value(type, ident, gecko_ffi_name, member=None)"> #[allow(non_snake_case)] pub fn copy_${type}_${ident}_from(&mut self, other: &Self) { self.gecko.m${type.capitalize()}s.ensure_len(other.gecko.m${type.capitalize()}s.len()); @@ -2868,7 +2868,11 @@ fn static_assert() { ); for (ours, others) in iter { + % if member: + ours.m${gecko_ffi_name}.${member} = others.m${gecko_ffi_name}.${member}; + % else: ours.m${gecko_ffi_name} = others.m${gecko_ffi_name}; + % endif } } @@ -2923,14 +2927,14 @@ fn static_assert() { self.gecko.m${type.capitalize()}TimingFunctionCount = input_len as u32; for (gecko, servo) in self.gecko.m${type.capitalize()}s.iter_mut().take(input_len as usize).zip(v) { - gecko.mTimingFunction = servo.into(); + gecko.mTimingFunction.mTiming = servo; } } ${impl_animation_or_transition_count(type, 'timing_function', 'TimingFunction')} - ${impl_copy_animation_or_transition_value(type, 'timing_function', 'TimingFunction')} + ${impl_copy_animation_or_transition_value(type, 'timing_function', "TimingFunction", "mTiming")} pub fn ${type}_timing_function_at(&self, index: usize) -> longhands::${type}_timing_function::computed_value::SingleComputedValue { - self.gecko.m${type.capitalize()}s[index].mTimingFunction.into() + self.gecko.m${type.capitalize()}s[index].mTimingFunction.mTiming } @@ -2996,7 +3000,9 @@ fn static_assert() { % for value in keyword.gecko_values(): structs::${keyword.gecko_constant(value)} => Keyword::${to_camel_case(value)}, % endfor + % if keyword.gecko_inexhaustive: _ => panic!("Found unexpected value for animation-${ident}"), + % endif } } ${impl_animation_count(ident, gecko_ffi_name)} diff --git a/components/style/properties/helpers/animated_properties.mako.rs b/components/style/properties/helpers/animated_properties.mako.rs index 5259f66107d..b079f30b1a8 100644 --- a/components/style/properties/helpers/animated_properties.mako.rs +++ b/components/style/properties/helpers/animated_properties.mako.rs @@ -1356,11 +1356,11 @@ fn is_matched_operation(first: &ComputedTransformOperation, second: &ComputedTra &TransformOperation::RotateZ(..)) | (&TransformOperation::Perspective(..), &TransformOperation::Perspective(..)) => true, - // we animate scale and translate operations against each other + // Match functions that have the same primitive transform function (a, b) if a.is_translate() && b.is_translate() => true, (a, b) if a.is_scale() && b.is_scale() => true, (a, b) if a.is_rotate() && b.is_rotate() => true, - // InterpolateMatrix and AccumulateMatrix are for mismatched transform. + // InterpolateMatrix and AccumulateMatrix are for mismatched transforms _ => false } } @@ -1829,7 +1829,7 @@ impl Animate for Quaternion { self.3 * other.3) .min(1.0).max(-1.0); - if dot == 1.0 { + if dot.abs() == 1.0 { return Ok(*self); } @@ -2468,79 +2468,112 @@ impl Animate for ComputedTransform { return Ok(Transform(result)); } - // https://drafts.csswg.org/css-transforms-1/#transform-transform-neutral-extend-animation - fn match_operations_if_possible<'a>( - this: &mut Cow<'a, Vec>, - other: &mut Cow<'a, Vec>, - ) -> bool { - if !this.iter().zip(other.iter()).all(|(this, other)| is_matched_operation(this, other)) { - return false; - } + let this = Cow::Borrowed(&self.0); + let other = Cow::Borrowed(&other.0); - if this.len() == other.len() { - return true; - } + // Interpolate the common prefix + let mut result = this + .iter() + .zip(other.iter()) + .take_while(|(this, other)| is_matched_operation(this, other)) + .map(|(this, other)| this.animate(other, procedure)) + .collect::, _>>()?; - let (shorter, longer) = - if this.len() < other.len() { - (this.to_mut(), other) - } else { - (other.to_mut(), this) - }; + // Deal with the remainders + let this_remainder = if this.len() > result.len() { + Some(&this[result.len()..]) + } else { + None + }; + let other_remainder = if other.len() > result.len() { + Some(&other[result.len()..]) + } else { + None + }; - shorter.reserve(longer.len()); - for op in longer.iter().skip(shorter.len()) { - shorter.push(op.to_animated_zero().unwrap()); - } - - // The resulting operations won't be matched regardless if the - // extended component is already InterpolateMatrix / - // AccumulateMatrix. - // - // Otherwise they should be matching operations all the time. - let already_mismatched = matches!( - longer[0], - TransformOperation::InterpolateMatrix { .. } | - TransformOperation::AccumulateMatrix { .. } - ); - - debug_assert_eq!( - !already_mismatched, - longer.iter().zip(shorter.iter()).all(|(this, other)| is_matched_operation(this, other)), - "ToAnimatedZero should generate matched operations" - ); - - !already_mismatched - } - - let mut this = Cow::Borrowed(&self.0); - let mut other = Cow::Borrowed(&other.0); - - if match_operations_if_possible(&mut this, &mut other) { - return Ok(Transform( - this.iter().zip(other.iter()) - .map(|(this, other)| this.animate(other, procedure)) - .collect::, _>>()? - )); - } - - match procedure { - Procedure::Add => Err(()), - Procedure::Interpolate { progress } => { - Ok(Transform(vec![TransformOperation::InterpolateMatrix { - from_list: Transform(this.into_owned()), - to_list: Transform(other.into_owned()), - progress: Percentage(progress as f32), - }])) - }, - Procedure::Accumulate { count } => { - Ok(Transform(vec![TransformOperation::AccumulateMatrix { - from_list: Transform(this.into_owned()), - to_list: Transform(other.into_owned()), - count: cmp::min(count, i32::max_value() as u64) as i32, - }])) + match (this_remainder, other_remainder) { + // If there is a remainder from *both* lists we must have had mismatched functions. + // => Add the remainders to a suitable ___Matrix function. + (Some(this_remainder), Some(other_remainder)) => match procedure { + Procedure::Add => { + debug_assert!(false, "Should have already dealt with add by the point"); + return Err(()); + } + Procedure::Interpolate { progress } => { + result.push(TransformOperation::InterpolateMatrix { + from_list: Transform(this_remainder.to_vec()), + to_list: Transform(other_remainder.to_vec()), + progress: Percentage(progress as f32), + }); + } + Procedure::Accumulate { count } => { + result.push(TransformOperation::AccumulateMatrix { + from_list: Transform(this_remainder.to_vec()), + to_list: Transform(other_remainder.to_vec()), + count: cmp::min(count, i32::max_value() as u64) as i32, + }); + } }, + // If there is a remainder from just one list, then one list must be shorter but + // completely match the type of the corresponding functions in the longer list. + // => Interpolate the remainder with identity transforms. + (Some(remainder), None) | (None, Some(remainder)) => { + let fill_right = this_remainder.is_some(); + result.append( + &mut remainder + .iter() + .map(|transform| { + let identity = transform.to_animated_zero().unwrap(); + + match transform { + // We can't interpolate/accumulate ___Matrix types directly with a + // matrix. Instead we need to wrap it in another ___Matrix type. + TransformOperation::AccumulateMatrix { .. } + | TransformOperation::InterpolateMatrix { .. } => { + let transform_list = Transform(vec![transform.clone()]); + let identity_list = Transform(vec![identity]); + let (from_list, to_list) = if fill_right { + (transform_list, identity_list) + } else { + (identity_list, transform_list) + }; + + match procedure { + Procedure::Add => Err(()), + Procedure::Interpolate { progress } => { + Ok(TransformOperation::InterpolateMatrix { + from_list, + to_list, + progress: Percentage(progress as f32), + }) + } + Procedure::Accumulate { count } => { + Ok(TransformOperation::AccumulateMatrix { + from_list, + to_list, + count: cmp::min(count, i32::max_value() as u64) + as i32, + }) + } + } + } + _ => { + let (lhs, rhs) = if fill_right { + (transform, &identity) + } else { + (&identity, transform) + }; + lhs.animate(rhs, procedure) + } + } + }) + .collect::, _>>()?, + ); + } + (None, None) => {} } + + Ok(Transform(result)) } } diff --git a/components/style/properties/longhands/box.mako.rs b/components/style/properties/longhands/box.mako.rs index ed7839360d9..d5e6a9b1106 100644 --- a/components/style/properties/longhands/box.mako.rs +++ b/components/style/properties/longhands/box.mako.rs @@ -247,6 +247,7 @@ ${helpers.single_keyword( gecko_enum_prefix="PlaybackDirection", custom_consts=animation_direction_custom_consts, extra_prefixes=animation_extra_prefixes, + gecko_inexhaustive=True, spec="https://drafts.csswg.org/css-animations/#propdef-animation-direction", allowed_in_keyframe_block=False, )} @@ -258,6 +259,7 @@ ${helpers.single_keyword( animation_value_type="none", vector=True, extra_prefixes=animation_extra_prefixes, + gecko_enum_prefix="StyleAnimationPlayState", spec="https://drafts.csswg.org/css-animations/#propdef-animation-play-state", allowed_in_keyframe_block=False, )} @@ -270,6 +272,7 @@ ${helpers.single_keyword( vector=True, gecko_enum_prefix="FillMode", extra_prefixes=animation_extra_prefixes, + gecko_inexhaustive=True, spec="https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode", allowed_in_keyframe_block=False, )} diff --git a/components/style/style_adjuster.rs b/components/style/style_adjuster.rs index 7c65798286e..af53e3dcc7b 100644 --- a/components/style/style_adjuster.rs +++ b/components/style/style_adjuster.rs @@ -694,6 +694,34 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { .set_computed_justify_items(parent_justify_items.computed); } + /// If '-webkit-appearance' is 'menulist' on a