use frus_widgets::{AutofillHint, Rect, WidgetId};
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct AutofillField {
pub widget: WidgetId,
pub virtual_id: i32,
pub hints: Vec<&'static str>,
pub bounds: [i32; 4],
pub value: String,
pub sensitive: bool,
}
pub(crate) fn virtual_id(id: WidgetId) -> i32 {
let raw = id.as_u64();
((raw ^ (raw >> 32)) & 0x7fff_ffff) as i32
}
pub(crate) fn structure<'a>(
stops: &[(WidgetId, Rect)],
scale: f32,
describe: impl Fn(WidgetId) -> Option<(&'a [AutofillHint], String)>,
) -> Vec<AutofillField> {
stops
.iter()
.filter_map(|&(widget, rect)| {
let (hints, value) = describe(widget)?;
if hints.is_empty() {
return None;
}
let px = |v: f32| (v * scale).round() as i32;
Some(AutofillField {
widget,
virtual_id: virtual_id(widget),
hints: hints.iter().map(|h| h.android()).collect(),
bounds: [
px(rect.x),
px(rect.y),
px(rect.width).max(1),
px(rect.height).max(1),
],
value,
sensitive: hints.iter().any(|h| h.is_secret()),
})
})
.collect()
}
pub(crate) fn route(
fields: &[AutofillField],
values: Vec<(i32, String)>,
) -> Vec<(WidgetId, String)> {
values
.into_iter()
.filter_map(|(id, value)| {
fields
.iter()
.find(|f| f.virtual_id == id)
.map(|f| (f.widget, value))
})
.collect()
}
pub(crate) fn with_gone(
mut current: Vec<AutofillField>,
shown: &[AutofillField],
) -> Vec<AutofillField> {
for field in shown {
if current.iter().all(|f| f.virtual_id != field.virtual_id) {
current.push(AutofillField {
bounds: [0, 0, 1, 1],
..field.clone()
});
}
}
current
}
pub(crate) fn changed(reported: &[AutofillField], now: &[AutofillField]) -> Vec<(i32, String)> {
now.iter()
.filter(|field| {
reported
.iter()
.find(|r| r.virtual_id == field.virtual_id)
.is_none_or(|r| r.value != field.value)
})
.map(|field| (field.virtual_id, field.value.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn id(raw: u64) -> WidgetId {
WidgetId::from_u64(raw)
}
fn form() -> Vec<(WidgetId, Rect)> {
vec![
(id(11), Rect::new(16.0, 100.0, 300.0, 56.0)),
(id(12), Rect::new(16.0, 170.0, 300.0, 56.0)),
(id(13), Rect::new(16.0, 240.0, 120.0, 40.0)),
]
}
const USERNAME: [AutofillHint; 2] = [AutofillHint::Username, AutofillHint::Email];
const PASSWORD: [AutofillHint; 1] = [AutofillHint::Password];
fn describe(widget: WidgetId) -> Option<(&'static [AutofillHint], String)> {
match widget.as_u64() {
11 => Some((&USERNAME, "someone@example.com".into())),
12 => Some((&PASSWORD, "hunter2".into())),
13 => Some((&[], String::new())),
_ => None,
}
}
#[test]
fn only_the_fields_that_say_what_they_are_for_are_shown() {
let fields = structure(&form(), 1.0, describe);
assert_eq!(
fields.iter().map(|f| f.widget).collect::<Vec<_>>(),
vec![id(11), id(12)],
"the button is not a field of the form, and the order is the form's"
);
assert_eq!(fields[0].hints, vec!["username", "emailAddress"]);
assert_eq!(fields[1].hints, vec!["password"]);
let gone = [(id(99), Rect::new(0.0, 0.0, 10.0, 10.0))];
assert!(structure(&gone, 1.0, describe).is_empty());
}
#[test]
fn a_box_is_given_in_physical_pixels() {
let fields = structure(&form(), 2.625, describe);
assert_eq!(fields[1].bounds, [42, 446, 788, 147]);
let thin = [(id(11), Rect::new(0.0, 0.0, 0.1, 0.0))];
assert_eq!(structure(&thin, 1.0, describe)[0].bounds[2..], [1, 1]);
}
#[test]
fn a_secret_is_marked_sensitive_and_still_carries_its_value() {
let fields = structure(&form(), 1.0, describe);
assert!(!fields[0].sensitive);
assert!(fields[1].sensitive);
assert_eq!(fields[1].value, "hunter2");
}
#[test]
fn a_field_keeps_its_id_and_two_fields_do_not_share_one() {
assert_eq!(virtual_id(id(11)), virtual_id(id(11)));
assert_ne!(virtual_id(id(11)), virtual_id(id(12)));
assert_ne!(virtual_id(id(5)), virtual_id(id(5 | (1 << 40))));
assert!(virtual_id(id(u64::MAX)) >= 0);
}
#[test]
fn a_value_goes_to_the_field_it_names_and_nowhere_else() {
let fields = structure(&form(), 1.0, describe);
let routed = route(
&fields,
vec![
(virtual_id(id(12)), "from the vault".into()),
(virtual_id(id(11)), "saved@example.com".into()),
(virtual_id(id(42)), "for a field that has gone".into()),
],
);
assert_eq!(
routed,
vec![
(id(12), "from the vault".to_string()),
(id(11), "saved@example.com".to_string()),
]
);
}
#[test]
fn a_step_behind_the_one_showing_is_still_part_of_the_form() {
let all = structure(&form(), 1.0, describe);
let (account, security) = (vec![all[0].clone()], vec![all[1].clone()]);
let merged = with_gone(security, &account);
assert_eq!(
merged.iter().map(|f| f.widget).collect::<Vec<_>>(),
vec![id(12), id(11)],
"the step showing, then the one behind it"
);
assert_eq!(
merged[1].value, "someone@example.com",
"with what it last held"
);
assert_eq!(merged[1].hints, vec!["username", "emailAddress"]);
assert_eq!(merged[1].bounds, [0, 0, 1, 1], "and not on screen anywhere");
assert_eq!(
merged[0].bounds, all[1].bounds,
"the field showing keeps its box"
);
assert_eq!(
with_gone(all.clone(), &all),
all,
"nothing on screen is repeated"
);
}
#[test]
fn only_a_value_the_service_has_not_heard_is_reported() {
let reported = structure(&form(), 1.0, describe);
let mut now = reported.clone();
assert!(changed(&reported, &now).is_empty(), "nothing typed");
now[1].value = "hunter22".into();
assert_eq!(
changed(&reported, &now),
vec![(virtual_id(id(12)), "hunter22".to_string())]
);
let unseen = changed(&reported[..1], &reported);
assert_eq!(unseen, vec![(virtual_id(id(12)), "hunter2".to_string())]);
}
}