diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 0025d12d3..30e9c7fba 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -4,7 +4,7 @@ use crate::avm1::function::{Avm1Function, ExecutionReason, FunctionObject}; use crate::avm1::object::{Object, TObject}; use crate::avm1::property::Attribute; use crate::avm1::runtime::skip_actions; -use crate::avm1::scope::Scope; +use crate::avm1::scope::{Scope, ScopeClass}; use crate::avm1::{fscommand, globals, scope, ArrayObject, ScriptObject, Value}; use crate::backend::navigator::{NavigationMethod, Request}; use crate::context::UpdateContext; @@ -2970,6 +2970,17 @@ impl<'a, 'gc> Activation<'a, 'gc> { self.scope = scope; } + pub fn set_scope_to_display_object(&mut self, object: DisplayObject<'gc>) { + self.scope = Gc::new( + self.context.gc_context, + Scope::new( + self.scope, + ScopeClass::Target, + object.object().coerce_to_object(self), + ), + ); + } + /// Whether this activation operates in a local scope. pub fn in_local_scope(&self) -> bool { let mut current_scope = Some(self.scope); diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index 01e9f9e32..8552d1d2e 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -1600,6 +1600,45 @@ impl<'gc> EditText<'gc> { x: union_bounds.offset_x() + Twips::from_pixels(EditText::INTERNAL_PADDING), }) } + + fn execute_avm1_asfunction( + self, + context: &mut UpdateContext<'_, 'gc>, + address: &WStr, + ) -> Result<(), crate::avm1::Error<'gc>> { + let mut activation = Avm1Activation::from_nothing( + context.reborrow(), + ActivationIdentifier::root("[EditText URL]"), + self.avm1_root(), + ); + // [NA]: Should all `from_nothings` be scoped to root? It definitely should here. + activation.set_scope_to_display_object(self.avm1_root()); + let this = self.avm1_root().object().coerce_to_object(&mut activation); + + if let Some((name, args)) = address.split_once(b',') { + let name = AvmString::new(activation.context.gc_context, name); + let args = AvmString::new(activation.context.gc_context, args); + let function = activation.get_variable(name)?; + function.call_with_default_this(this, name, &mut activation, &[args.into()])?; + } else { + let name = AvmString::new(activation.context.gc_context, address); + let function = activation.get_variable(name)?; + function.call_with_default_this(this, name, &mut activation, &[])?; + } + Ok(()) + } + + fn open_url(self, context: &mut UpdateContext<'_, 'gc>, url: &WStr, target: &WStr) { + if let Some(address) = url.strip_prefix(WStr::from_units(b"asfunction:")) { + if let Err(e) = self.execute_avm1_asfunction(context, address) { + error!("Couldn't execute URL \"{url:?}\": {e:?}"); + } + } else { + context + .navigator + .navigate_to_url(&url.to_utf8_lossy(), &target.to_utf8_lossy(), None); + } + } } impl<'gc> TDisplayObject<'gc> for EditText<'gc> { @@ -1955,11 +1994,19 @@ impl<'gc> TInteractiveObject<'gc> for EditText<'gc> { let tracker = context.focus_tracker; tracker.set(Some(self.into()), context); } - if let Some(position) = self - .screen_position_to_index(*context.mouse_position) - .map(TextSelection::for_position) - { - self.0.write(context.gc_context).selection = Some(position); + if let Some(position) = self.screen_position_to_index(*context.mouse_position) { + self.0.write(context.gc_context).selection = + Some(TextSelection::for_position(position)); + + let format = self.0.read().text_spans.get_text_format(position, position); + if let Some(url) = format.url { + if !url.is_empty() { + // TODO: This fires on mouse DOWN but it should be mouse UP... + // but only if it went down in the same span. + // Needs more advanced focus handling than we have at time of writing this comment. + self.open_url(context, &url, &format.target.unwrap_or_default()); + } + } } else { self.0.write(context.gc_context).selection = Some(TextSelection::for_position(self.text_length())); diff --git a/tests/tests/swfs/avm1/asfunction/input.json b/tests/tests/swfs/avm1/asfunction/input.json new file mode 100644 index 000000000..81681fc0d --- /dev/null +++ b/tests/tests/swfs/avm1/asfunction/input.json @@ -0,0 +1,42 @@ +[ + { + "type": "MouseDown", + "pos": [134, 69], + "btn": "Left" + }, + { + "type": "MouseUp", + "pos": [134, 69], + "btn": "Left" + }, + { + "type": "MouseDown", + "pos": [166, 95], + "btn": "Left" + }, + { + "type": "MouseUp", + "pos": [166, 95], + "btn": "Left" + }, + { + "type": "MouseDown", + "pos": [143, 128], + "btn": "Left" + }, + { + "type": "MouseUp", + "pos": [143, 128], + "btn": "Left" + }, + { + "type": "MouseDown", + "pos": [142, 160], + "btn": "Left" + }, + { + "type": "MouseUp", + "pos": [142, 160], + "btn": "Left" + } +] \ No newline at end of file diff --git a/tests/tests/swfs/avm1/asfunction/output.txt b/tests/tests/swfs/avm1/asfunction/output.txt new file mode 100644 index 000000000..dae38b37c --- /dev/null +++ b/tests/tests/swfs/avm1/asfunction/output.txt @@ -0,0 +1,6 @@ +alert1 called with input: undefined +// this +_level0 + +_level0.callback with input: Second Test +text1.callback with input: a,b,c,d diff --git a/tests/tests/swfs/avm1/asfunction/test.fla b/tests/tests/swfs/avm1/asfunction/test.fla new file mode 100644 index 000000000..20c65cf6e Binary files /dev/null and b/tests/tests/swfs/avm1/asfunction/test.fla differ diff --git a/tests/tests/swfs/avm1/asfunction/test.swf b/tests/tests/swfs/avm1/asfunction/test.swf new file mode 100644 index 000000000..91d6bdd37 Binary files /dev/null and b/tests/tests/swfs/avm1/asfunction/test.swf differ diff --git a/tests/tests/swfs/avm1/asfunction/test.toml b/tests/tests/swfs/avm1/asfunction/test.toml new file mode 100644 index 000000000..cf6123969 --- /dev/null +++ b/tests/tests/swfs/avm1/asfunction/test.toml @@ -0,0 +1 @@ +num_ticks = 1 diff --git a/wstr/src/common.rs b/wstr/src/common.rs index d9a5aed4e..9889bd56a 100644 --- a/wstr/src/common.rs +++ b/wstr/src/common.rs @@ -382,6 +382,12 @@ impl WStr { (&self[..index], &self[index..]) } + /// Analogue of [`str::split_once`]. + #[inline] + pub fn split_once<'a, P: Pattern<'a>>(&'a self, pattern: P) -> Option<(&'a WStr, &'a WStr)> { + super::ops::str_split_once(self, pattern) + } + /// Analogue of [`str::rsplit_once`]. #[inline] pub fn rsplit_once<'a, P: Pattern<'a>>(&'a self, pattern: P) -> Option<(&'a WStr, &'a WStr)> { diff --git a/wstr/src/ops.rs b/wstr/src/ops.rs index 9d9250048..9facb19d9 100644 --- a/wstr/src/ops.rs +++ b/wstr/src/ops.rs @@ -327,6 +327,14 @@ pub fn str_split<'a, P: Pattern<'a>>(string: &'a WStr, pattern: P) -> Split<'a, } } +pub fn str_split_once<'a, P: Pattern<'a>>( + string: &'a WStr, + pattern: P, +) -> Option<(&'a WStr, &'a WStr)> { + let (start, end) = pattern.into_searcher(string).next_match()?; + Some((&string[..start], &string[end..])) +} + pub fn str_rsplit_once<'a, P: Pattern<'a>>( string: &'a WStr, pattern: P,