Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: Add compatibility flags #18974

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ id3 = "1.16.0"
either = "1.13.0"
chardetng = "0.1.17"
tracy-client = { version = "0.17.6", optional = true, default-features = false }
lazy_static = "1.5.0"

[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
workspace = true
Expand Down
5 changes: 5 additions & 0 deletions core/assets/texts/en-US/flags.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
flag-tab_skip-name = Skip some objects when tabbing
flag-tab_skip-description =
Flash Player skips some objects from automatic tab ordering based on their coordinates.
Disable it when some objects are not being focused when tabbing.
Enable it when some objects are being focused when they shouldn't be.
5 changes: 5 additions & 0 deletions core/src/backend/ui.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::backend::navigator::OwnedFuture;
use crate::flags::CompatibilityFlag;
pub use crate::loader::Error as DialogLoaderError;
use chrono::{DateTime, Utc};
use downcast_rs::Downcast;
Expand Down Expand Up @@ -128,6 +129,10 @@ pub trait UiBackend: Downcast {

/// Mark that any previously open dialog has been closed
fn close_file_dialog(&mut self);

fn flag_enabled(&self, flag: CompatibilityFlag) -> bool {
flag.definition().default_value
}
}
impl_downcast!(UiBackend);

Expand Down
279 changes: 279 additions & 0 deletions core/src/flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
//! Compatibility flags help configure Ruffle by enabling and disabling specific behaviors.
//!
//! They are meant to be used for instance in the following situations.
//!
//! 1. They fix bugs in Flash Player that make some content misbehave.
//! Note that in general we don't fix bugs in Flash Player -- we are bug compatible.
//! However, there are examples where a bug is so sneaky, some content could have
//! been created with an assumption that the bug doesn't exist and the bug affects it.
//!
//! 2. They genuinely improve the experience of using Ruffle at the cost
//! of lowering compatibility with Flash Player.
//!
//! 3. They improve the "perceived" compatibility at the cost of the "real" compatibility.
//! For instance, something does not work in Flash Player, and we make it work.

use fluent_templates::LanguageIdentifier;
use lazy_static::lazy_static;
use std::{collections::HashMap, fmt::Display, str::FromStr};

use crate::i18n::core_text;

/// The definition of a compatibility flag.
///
/// It's a static definition, it's meant to describe flags statically.
/// See [`define_ruffle_flags!`].
pub struct CompatibilityFlagDefinition {
/// The flag we're defining.
pub flag: CompatibilityFlag,

/// Short identifier of the flag.
///
/// Has to be unique, used for e.g. specifying the flag in config.
pub id: &'static str,

/// Whether the flag is enabled by default in Ruffle.
pub default_value: bool,

/// Whether Flash Player behaves as if the flag is enabled or disabled.
pub flash_player_value: bool,
}

impl CompatibilityFlagDefinition {
pub fn name(&self, language: &LanguageIdentifier) -> String {
core_text(language, &format!("flag-{}-name", self.id))
}

pub fn description(&self, language: &LanguageIdentifier) -> String {
core_text(language, &format!("flag-{}-description", self.id))
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompatibilityFlags(HashMap<CompatibilityFlag, bool>);

impl CompatibilityFlags {
pub fn empty() -> Self {
Self(HashMap::new())
}

pub fn new(flags: HashMap<CompatibilityFlag, bool>) -> Self {
Self(flags)
}

pub fn all_flags() -> &'static Vec<CompatibilityFlag> {
&RUFFLE_ALL_FLAGS
}

pub fn enabled(&self, flag: CompatibilityFlag) -> Result<bool, bool> {
self.0
.get(&flag)
.cloned()
.ok_or_else(|| flag.definition().default_value)
}

pub fn set(&mut self, flag: CompatibilityFlag, enabled: bool) {
if enabled == flag.definition().default_value {
self.0.remove(&flag);
} else {
self.0.insert(flag, enabled);
}
}

pub fn override_with(&mut self, other: &CompatibilityFlags) {
for (flag, value) in &other.0 {
self.0.insert(*flag, *value);
}
}

pub fn overridden(&self) -> std::collections::hash_map::Keys<'_, CompatibilityFlag, bool> {
self.0.keys()
}
}

impl FromStr for CompatibilityFlags {
type Err = String;

fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.is_empty() {
return Ok(CompatibilityFlags::new(HashMap::new()));
}

let mut flags = HashMap::new();
for flag in value.split(",") {
let flag = flag.trim();
if flag.is_empty() {
continue;
}
let (id, value) = if let Some(flag) = flag.strip_prefix('-') {
(flag, false)
} else if let Some(flag) = flag.strip_prefix('+') {
(flag, true)
} else {
(flag, true)
};
flags.insert(
CompatibilityFlag::from_id(id).ok_or_else(|| flag.to_string())?,
value,
);
}
Ok(CompatibilityFlags::new(flags))
}
}

impl Display for CompatibilityFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for (&flag, &value) in &self.0 {
let def = flag.definition();
if def.default_value != value {
if !first {
write!(f, ",")?;
} else {
first = false;
}

if !value {
write!(f, "-")?;
}

write!(f, "{}", def.id)?;
}
}
Ok(())
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for CompatibilityFlags {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
}
}

macro_rules! define_compatibility_flags {
($(flag($flag:ident, $id:expr, $($key:ident: $value:expr),* $(,)?));* $(;)?) => {
/// The collection of all compatibility flags.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CompatibilityFlag {
$($flag),*
}

lazy_static! {
static ref RUFFLE_FLAGS: HashMap<CompatibilityFlag, CompatibilityFlagDefinition> = HashMap::from([
$((CompatibilityFlag::$flag, CompatibilityFlagDefinition {
flag: CompatibilityFlag::$flag,
id: $id,
$($key: $value),*
})),*
]);

static ref RUFFLE_FLAG_IDS: HashMap<&'static str, CompatibilityFlag> = HashMap::from([
$(($id, CompatibilityFlag::$flag)),*
]);

static ref RUFFLE_ALL_FLAGS: Vec<CompatibilityFlag> = vec![
$(CompatibilityFlag::$flag),*
];
}

impl CompatibilityFlag {
pub fn from_id(id: &str) -> Option<Self> {
RUFFLE_FLAG_IDS.get(id).cloned()
}

pub fn definition(&self) -> &'static CompatibilityFlagDefinition {
RUFFLE_FLAGS.get(self).expect("Missing flag definition")
}
}
};
}

define_compatibility_flags!(
flag(
TabSkip, "tab_skip",
default_value: true,
flash_player_value: true,
);
);

#[cfg(test)]
mod tests {
use crate::flags::{CompatibilityFlag, CompatibilityFlags};

#[test]
fn test_parse_empty() {
let flags = "".parse::<CompatibilityFlags>();
assert_eq!(flags, Ok(CompatibilityFlags::empty()));

let flags = flags.unwrap();
assert_eq!(flags.enabled(CompatibilityFlag::TabSkip), Err(true));
}

#[test]
fn test_parse_positive() {
let flags = "tab_skip".parse::<CompatibilityFlags>();
assert!(flags.is_ok());

let flags = flags.unwrap();
assert_eq!(flags.enabled(CompatibilityFlag::TabSkip), Ok(true));
}

#[test]
fn test_parse_positive2() {
let flags = "+tab_skip".parse::<CompatibilityFlags>();
assert!(flags.is_ok());

let flags = flags.unwrap();
assert_eq!(flags.enabled(CompatibilityFlag::TabSkip), Ok(true));
}

#[test]
fn test_parse_negative() {
let flags = "-tab_skip".parse::<CompatibilityFlags>();
assert!(flags.is_ok());

let flags = flags.unwrap();
assert_eq!(flags.enabled(CompatibilityFlag::TabSkip), Ok(false));
}

#[test]
fn test_parse_space() {
let flags = " tab_skip , ".parse::<CompatibilityFlags>();
assert!(flags.is_ok());

let flags = flags.unwrap();
assert_eq!(flags.enabled(CompatibilityFlag::TabSkip), Ok(true));
}

#[test]
fn test_parse_space2() {
let flags = " , tab_skip ".parse::<CompatibilityFlags>();
assert!(flags.is_ok());

let flags = flags.unwrap();
assert_eq!(flags.enabled(CompatibilityFlag::TabSkip), Ok(true));
}

#[test]
fn test_to_string1() {
let flags = "tab_skip".parse::<CompatibilityFlags>();
assert!(flags.is_ok());

let flags = flags.unwrap();
assert_eq!(flags.to_string(), "");
}

#[test]
fn test_to_string2() {
let flags = "-tab_skip".parse::<CompatibilityFlags>();
assert!(flags.is_ok());

let flags = flags.unwrap();
assert_eq!(flags.to_string(), "-tab_skip");
}
}
13 changes: 7 additions & 6 deletions core/src/focus_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub use crate::display_object::{
};
use crate::display_object::{EditText, InteractiveObject, TInteractiveObject};
use crate::events::{ClipEvent, KeyCode};
use crate::flags::CompatibilityFlag;
use crate::prelude::Avm2Value;
use crate::Player;
use either::Either;
Expand Down Expand Up @@ -279,7 +280,7 @@ impl<'gc> FocusTracker<'gc> {

pub fn tab_order(&self, context: &mut UpdateContext<'gc>) -> TabOrder<'gc> {
let mut tab_order = TabOrder::fill(context);
tab_order.sort();
tab_order.sort(context.ui.flag_enabled(CompatibilityFlag::TabSkip));
tab_order
}

Expand Down Expand Up @@ -425,15 +426,15 @@ impl<'gc> TabOrder<'gc> {
}
}

fn sort(&mut self) {
fn sort(&mut self, allow_ignoring_duplicates: bool) {
if self.is_custom() {
self.sort_with(CustomTabOrdering);
self.sort_with(CustomTabOrdering, allow_ignoring_duplicates);
} else {
self.sort_with(AutomaticTabOrdering);
self.sort_with(AutomaticTabOrdering, allow_ignoring_duplicates);
}
}

fn sort_with(&mut self, ordering: impl TabOrdering) {
fn sort_with(&mut self, ordering: impl TabOrdering, allow_ignoring_duplicates: bool) {
self.objects.sort_by_cached_key(|&o| ordering.key(o));

let to_skip = self
Expand All @@ -443,7 +444,7 @@ impl<'gc> TabOrder<'gc> {
.count();
self.objects.drain(..to_skip);

if ordering.ignore_duplicates() {
if allow_ignoring_duplicates && ordering.ignore_duplicates() {
self.objects.dedup_by_key(|&mut o| ordering.key(o));
}
}
Expand Down
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod context_menu;
mod drawing;
mod ecma_conversions;
pub mod events;
pub mod flags;
pub mod focus_tracker;
mod font;
mod frame_lifecycle;
Expand Down
11 changes: 11 additions & 0 deletions desktop/assets/texts/en-US/preferences_dialog.ftl
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
preferences-dialog = Ruffle Preferences

preferences-panel-general = General
preferences-panel-compatibility-flags = Compatibility Flags

preferences-panel-flag-overridden = This flag has been overridden by the user
preferences-panel-flag-id = ID
preferences-panel-flag-name = Name
preferences-panel-flag-description = Description
preferences-panel-flag-enabled = Enabled
preferences-panel-flag-fp = FP
preferences-panel-flag-fp-tooltip = The value which imitates the behavior of Flash Player

preference-locked-by-cli = Read-Only (Set by CLI)

graphics-backend = Graphics Backend
Expand Down
Loading