use ferroui_base::animation::easings::IEasing;
use ferroui_base::media::{Color, Colors};
use ferroui_base::reactive::IDisposable;
use ferroui_base::{Rect, Size, Thickness};
use ferroui_controls::platform::{
IInputPane, IInsetsManager, InputPaneBase, InputPaneState, InputPaneStateEventArgs, InsetsManagerBase,
SafeAreaChangedArgs, SystemBarTheme,
};
use std::cell::Cell;
use std::rc::{Rc, Weak};
use std::time::Duration;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct RootInsets {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
pub navigation_bars_bottom: i32,
pub ime_bottom: i32,
pub ime_visible: bool,
pub system_bars_visible: bool,
}
impl RootInsets {
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
pub fn from_array(values: &[i32]) -> Option<RootInsets> {
match values {
[left, top, right, bottom, navigation_bars_bottom, ime_bottom, ime_visible, system_bars_visible] => {
Some(RootInsets {
left: *left,
top: *top,
right: *right,
bottom: *bottom,
navigation_bars_bottom: *navigation_bars_bottom,
ime_bottom: *ime_bottom,
ime_visible: *ime_visible != 0,
system_bars_visible: *system_bars_visible != 0,
})
}
_ => None,
}
}
}
pub(crate) trait IInsetsWindow {
fn sdk_int(&self) -> i32;
fn has_window(&self) -> bool;
fn root_insets(&self) -> Option<RootInsets>;
fn is_display_edge_to_edge_forced(&self) -> bool;
fn set_layout_in_display_cutout_mode(&self, short_edges: bool);
fn set_decor_fits_system_windows(&self, decor_fits_system_windows: bool);
fn add_translucent_bars(&self);
fn set_system_bar_color(&self, color: Color);
fn set_navigation_bar_contrast_enforced(&self, enforced: bool);
fn appearance_light_status_bars(&self) -> bool;
fn set_appearance_light_bars(&self, light: bool);
fn set_system_bars_visible(&self, visible: bool);
}
pub(crate) trait IInsetsTopLevel {
fn top_level_render_scaling(&self) -> f64;
fn top_level_client_size(&self) -> Size;
}
pub struct AndroidInsetsManager {
window: Box<dyn IInsetsWindow>,
top_level: Weak<dyn IInsetsTopLevel>,
displays_edge_to_edge: Cell<bool>,
system_ui_visibility: Cell<Option<bool>>,
status_bar_theme: Cell<Option<SystemBarTheme>>,
is_default_system_bar_light_theme: Cell<Option<bool>>,
system_bar_color: Cell<Option<Color>>,
state: Cell<InputPaneState>,
previous_rect: Cell<Rect>,
previous_ime_inset: Cell<Option<i32>>,
display_edge_to_edge_preference: Cell<bool>,
uses_legacy_layouts: bool,
is_display_edge_to_edge_forced: bool,
safe_area_changed: InsetsManagerBase,
state_changed: InputPaneBase,
}
const Q: i32 = 29;
const R: i32 = 30;
impl AndroidInsetsManager {
pub(crate) fn new(
window: Box<dyn IInsetsWindow>,
top_level: Weak<dyn IInsetsTopLevel>,
) -> Rc<AndroidInsetsManager> {
let is_display_edge_to_edge_forced = window.is_display_edge_to_edge_forced();
let uses_legacy_layouts = window.sdk_int() < R;
let this = Rc::new(AndroidInsetsManager {
window,
top_level,
displays_edge_to_edge: Cell::new(false),
system_ui_visibility: Cell::new(None),
status_bar_theme: Cell::new(None),
is_default_system_bar_light_theme: Cell::new(None),
system_bar_color: Cell::new(None),
state: Cell::new(InputPaneState::Closed),
previous_rect: Cell::new(Rect::default()),
previous_ime_inset: Cell::new(None),
display_edge_to_edge_preference: Cell::new(false),
uses_legacy_layouts,
is_display_edge_to_edge_forced,
safe_area_changed: InsetsManagerBase::new(),
state_changed: InputPaneBase::new(),
});
this.set_display_edge_to_edge_preference(false);
this
}
fn render_scaling(&self) -> f64 {
self.top_level.upgrade().map_or(1.0, |top_level| top_level.top_level_render_scaling())
}
fn client_size(&self) -> Size {
self.top_level.upgrade().map_or(Size::default(), |top_level| top_level.top_level_client_size())
}
fn set_state(&self, value: InputPaneState) {
let old_state = self.state.replace(value);
if old_state != value && self.window.sdk_int() <= Q {
let current_rect = self.occluded_rect();
self.notify_state_changed(value, Some(self.previous_rect.get()), current_rect, Duration::ZERO, None);
self.previous_rect.set(current_rect);
}
}
fn update_display_edge_to_egde_state(&self) {
if self.is_display_edge_to_edge_forced {
self.displays_edge_to_edge.set(true);
return;
}
let preference = self.display_edge_to_edge_preference.get();
self.displays_edge_to_edge.set(preference);
self.window.set_layout_in_display_cutout_mode(preference);
self.window.set_decor_fits_system_windows(!preference);
if preference {
self.window.add_translucent_bars();
} else {
self.set_system_bar_color(self.system_bar_color.get());
}
}
pub(crate) fn on_apply_window_insets(&self, has_insets: bool, ime_visible: bool, ime_bottom: i32) {
self.notify_safe_area_changed(self.safe_area_padding());
if self.previous_rect.get() == Rect::default() {
self.previous_rect.set(self.occluded_rect());
}
self.set_state(if has_insets && ime_visible { InputPaneState::Open } else { InputPaneState::Closed });
if self.window.sdk_int() == R {
let ime_inset = has_insets.then_some(ime_bottom);
if self.previous_ime_inset.get().is_none() {
self.previous_ime_inset.set(ime_inset);
}
if ime_inset.unwrap_or(0) != self.previous_ime_inset.get().unwrap_or(0) {
self.notify_state_changed(
self.state.get(),
Some(self.previous_rect.get()),
self.occluded_rect(),
Duration::ZERO,
None,
);
}
self.previous_ime_inset.set(ime_inset);
}
}
pub(crate) fn set_default_system_light_mode(&self, is_light_mode: bool) {
self.is_default_system_bar_light_theme.set(Some(is_light_mode));
}
fn notify_safe_area_changed(&self, safe_area_padding: Thickness) {
self.safe_area_changed.on_safe_area_changed(SafeAreaChangedArgs::new(safe_area_padding));
}
fn notify_state_changed(
&self,
new_state: InputPaneState,
start_rect: Option<Rect>,
end_rect: Rect,
animation_duration: Duration,
easing: Option<Rc<dyn IEasing>>,
) {
self.state_changed.on_state_changed(InputPaneStateEventArgs::with_animation(
new_state,
start_rect,
end_rect,
animation_duration,
easing,
));
}
pub(crate) fn on_global_layout(&self) {
self.notify_safe_area_changed(self.safe_area_padding());
if self.uses_legacy_layouts {
let insets = self.window.root_insets();
self.set_state(if insets.is_some_and(|insets| insets.ime_visible) {
InputPaneState::Open
} else {
InputPaneState::Closed
});
}
}
pub(crate) fn on_insets_animation_start(
&self,
lower_bound_bottom: i32,
upper_bound_bottom: i32,
duration_millis: i64,
easing: Option<Rc<dyn IEasing>>,
) {
if let Some(insets) = self.window.root_insets() {
let navbar_inset = insets.navigation_bars_bottom;
let client_size = self.client_size();
let bottom = self.safe_area_padding().bottom;
let rect_of = |bound_bottom: i32| {
let height = f64::from((((bound_bottom - navbar_inset) as f64 / self.render_scaling()) as f32).max(0.0));
Rect::new(0.0, client_size.height - bottom - height, client_size.width, height)
};
let upper_rect = rect_of(lower_bound_bottom);
let lower_rect = rect_of(upper_bound_bottom);
let duration = Duration::from_millis(u64::try_from(duration_millis).unwrap_or(0));
let is_opening = self.state.get() == InputPaneState::Open;
self.notify_state_changed(
self.state.get(),
Some(if is_opening { upper_rect } else { lower_rect }),
if is_opening { lower_rect } else { upper_rect },
duration,
easing,
);
}
}
pub fn system_bar_theme(&self) -> Option<SystemBarTheme> {
Some(if self.window.appearance_light_status_bars() { SystemBarTheme::Light } else { SystemBarTheme::Dark })
}
pub fn set_system_bar_theme(&self, value: Option<SystemBarTheme>) {
self.status_bar_theme.set(value);
if self.is_default_system_bar_light_theme.get().is_none() {
self.is_default_system_bar_light_theme.set(Some(self.window.appearance_light_status_bars()));
}
let value = value.unwrap_or(if self.is_default_system_bar_light_theme.get() == Some(true) {
SystemBarTheme::Light
} else {
SystemBarTheme::Dark
});
self.window.set_appearance_light_bars(value == SystemBarTheme::Light);
}
pub(crate) fn apply_status_bar_state(&self) {
self.set_is_system_bar_visible(self.system_ui_visibility.get());
self.set_system_bar_theme(self.status_bar_theme.get());
self.set_system_bar_color(self.system_bar_color.get());
}
}
pub(crate) fn safe_area_padding_of(insets: &RootInsets, displays_edge_to_edge: bool, render_scaling: f64) -> Thickness {
if !displays_edge_to_edge {
return Thickness::default();
}
Thickness::new(
f64::from(insets.left) / render_scaling,
f64::from(insets.top) / render_scaling,
f64::from(insets.right) / render_scaling,
f64::from(insets.bottom) / render_scaling,
)
}
pub(crate) fn occluded_rect_of(
insets: &RootInsets,
render_scaling: f64,
client_size: Size,
safe_area_bottom: f64,
) -> Rect {
let navbar_inset = insets.navigation_bars_bottom;
let ime_inset = insets.ime_bottom;
let height = f64::from((((ime_inset - navbar_inset) as f64 / render_scaling) as f32).max(0.0));
Rect::new(0.0, client_size.height - safe_area_bottom - height, client_size.width, height)
}
impl IInsetsManager for AndroidInsetsManager {
fn is_system_bar_visible(&self) -> Option<bool> {
if !self.window.has_window() {
return Some(true);
}
self.window.root_insets().map(|insets| insets.system_bars_visible)
}
fn set_is_system_bar_visible(&self, value: Option<bool>) {
self.system_ui_visibility.set(value);
self.window.set_system_bars_visible(value.unwrap_or(true));
}
fn display_edge_to_edge_preference(&self) -> bool {
self.display_edge_to_edge_preference.get()
}
fn set_display_edge_to_edge_preference(&self, value: bool) {
self.display_edge_to_edge_preference.set(value);
self.update_display_edge_to_egde_state();
}
fn displays_edge_to_edge(&self) -> bool {
self.displays_edge_to_edge.get()
}
fn safe_area_padding(&self) -> Thickness {
match self.window.root_insets() {
Some(insets) => safe_area_padding_of(&insets, self.displays_edge_to_edge(), self.render_scaling()),
None => Thickness::default(),
}
}
fn system_bar_color(&self) -> Option<Color> {
self.system_bar_color.get()
}
fn set_system_bar_color(&self, value: Option<Color>) {
self.system_bar_color.set(value);
if self.is_display_edge_to_edge_forced {
if self.window.sdk_int() >= 36 {
self.window.set_navigation_bar_contrast_enforced(false);
} else if self.window.sdk_int() >= 35 {
self.window.set_navigation_bar_contrast_enforced(value != Some(Colors::TRANSPARENT));
}
return;
}
if let Some(color) = value {
if !self.displays_edge_to_edge.get() && self.window.has_window() {
self.window.set_system_bar_color(color);
}
}
}
fn safe_area_changed(&self, handler: Rc<dyn Fn(&SafeAreaChangedArgs)>) -> Rc<dyn IDisposable> {
self.safe_area_changed.safe_area_changed(handler)
}
}
impl IInputPane for AndroidInsetsManager {
fn state(&self) -> InputPaneState {
self.state.get()
}
fn occluded_rect(&self) -> Rect {
match self.window.root_insets() {
Some(insets) => {
occluded_rect_of(&insets, self.render_scaling(), self.client_size(), self.safe_area_padding().bottom)
}
None => Rect::default(),
}
}
fn state_changed(&self, handler: Rc<dyn Fn(&InputPaneStateEventArgs)>) -> Rc<dyn IDisposable> {
self.state_changed.state_changed(handler)
}
}
#[cfg(target_os = "android")]
pub(crate) use imp::{ActivityInsetsWindow, AnimationEasing};
#[cfg(target_os = "android")]
mod imp {
use super::{IInsetsWindow, RootInsets};
use crate::interop::java::{
call_object, call_static_boolean, call_static_object, call_static_void, int_array_of, JavaClass, JavaObject,
JavaValue,
};
use crate::interop::natives::{sdk_int, PLATFORM_HELPER};
use ferroui_base::animation::easings::{IEasing, SharedEasing};
use ferroui_base::media::Color;
use std::sync::Arc;
pub(crate) struct AnimationEasing {
interpolator: JavaObject,
}
impl AnimationEasing {
pub fn new(interpolator: JavaObject) -> Self {
Self { interpolator }
}
}
impl IEasing for AnimationEasing {
fn ease(&self, progress: f64) -> f64 {
f64::from(crate::interop::java::call_float(
&self.interpolator,
"getInterpolation",
"(F)F",
&[JavaValue::Float(progress as f32)],
))
}
fn to_shared(&self) -> Arc<SharedEasing> {
Arc::new(AnimationEasing { interpolator: self.interpolator.clone() })
}
}
pub(crate) struct ActivityInsetsWindow {
activity: JavaObject,
}
impl ActivityInsetsWindow {
pub fn new(activity: JavaObject, handle: i64) -> Self {
call_static_void(
&JavaClass::find(PLATFORM_HELPER),
"setInsetsListener",
"(Landroid/app/Activity;J)V",
&[JavaValue::Object(Some(&activity)), JavaValue::Long(handle)],
);
Self { activity }
}
fn call(&self, name: &str, value: bool) {
call_static_void(
&JavaClass::find(PLATFORM_HELPER),
name,
"(Landroid/app/Activity;Z)V",
&[JavaValue::Object(Some(&self.activity)), JavaValue::Boolean(value)],
);
}
}
impl IInsetsWindow for ActivityInsetsWindow {
fn sdk_int(&self) -> i32 {
sdk_int()
}
fn has_window(&self) -> bool {
call_object(&self.activity, "getWindow", "()Landroid/view/Window;", &[]).is_some()
}
fn root_insets(&self) -> Option<RootInsets> {
let values = call_static_object(
&JavaClass::find(PLATFORM_HELPER),
"getRootInsets",
"(Landroid/app/Activity;)[I",
&[JavaValue::Object(Some(&self.activity))],
)?;
RootInsets::from_array(&int_array_of(&values))
}
fn is_display_edge_to_edge_forced(&self) -> bool {
call_static_boolean(
&JavaClass::find(PLATFORM_HELPER),
"isDisplayEdgeToEdgeForced",
"(Landroid/app/Activity;)Z",
&[JavaValue::Object(Some(&self.activity))],
)
}
fn set_layout_in_display_cutout_mode(&self, short_edges: bool) {
self.call("setLayoutInDisplayCutoutMode", short_edges);
}
fn set_decor_fits_system_windows(&self, decor_fits_system_windows: bool) {
self.call("setDecorFitsSystemWindows", decor_fits_system_windows);
}
fn add_translucent_bars(&self) {
self.call("setTranslucentBars", true);
}
fn set_system_bar_color(&self, color: Color) {
call_static_void(
&JavaClass::find(PLATFORM_HELPER),
"setSystemBarColor",
"(Landroid/app/Activity;I)V",
&[JavaValue::Object(Some(&self.activity)), JavaValue::Int(color.to_uint32() as i32)],
);
}
fn set_navigation_bar_contrast_enforced(&self, enforced: bool) {
self.call("setNavigationBarContrastEnforced", enforced);
}
fn appearance_light_status_bars(&self) -> bool {
call_static_boolean(
&JavaClass::find(PLATFORM_HELPER),
"getAppearanceLightStatusBars",
"(Landroid/app/Activity;)Z",
&[JavaValue::Object(Some(&self.activity))],
)
}
fn set_appearance_light_bars(&self, light: bool) {
self.call("setAppearanceLightBars", light);
}
fn set_system_bars_visible(&self, visible: bool) {
self.call("setSystemBarsVisible", visible);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
#[derive(Default)]
struct WindowState {
sdk_int: Cell<i32>,
forced: Cell<bool>,
insets: Cell<Option<RootInsets>>,
light_status_bars: Cell<bool>,
calls: RefCell<Vec<String>>,
}
struct Window(Rc<WindowState>);
impl IInsetsWindow for Window {
fn sdk_int(&self) -> i32 {
self.0.sdk_int.get()
}
fn has_window(&self) -> bool {
true
}
fn root_insets(&self) -> Option<RootInsets> {
self.0.insets.get()
}
fn is_display_edge_to_edge_forced(&self) -> bool {
self.0.forced.get()
}
fn set_layout_in_display_cutout_mode(&self, short_edges: bool) {
self.0.calls.borrow_mut().push(format!("cutout {short_edges}"));
}
fn set_decor_fits_system_windows(&self, decor_fits_system_windows: bool) {
self.0.calls.borrow_mut().push(format!("fits {decor_fits_system_windows}"));
}
fn add_translucent_bars(&self) {
self.0.calls.borrow_mut().push("translucent".to_string());
}
fn set_system_bar_color(&self, color: Color) {
self.0.calls.borrow_mut().push(format!("color {:08x}", color.to_uint32()));
}
fn set_navigation_bar_contrast_enforced(&self, enforced: bool) {
self.0.calls.borrow_mut().push(format!("contrast {enforced}"));
}
fn appearance_light_status_bars(&self) -> bool {
self.0.light_status_bars.get()
}
fn set_appearance_light_bars(&self, light: bool) {
self.0.light_status_bars.set(light);
self.0.calls.borrow_mut().push(format!("light {light}"));
}
fn set_system_bars_visible(&self, visible: bool) {
self.0.calls.borrow_mut().push(format!("visible {visible}"));
}
}
struct TopLevel;
impl IInsetsTopLevel for TopLevel {
fn top_level_render_scaling(&self) -> f64 {
2.0
}
fn top_level_client_size(&self) -> Size {
Size::new(540.0, 1200.0)
}
}
fn manager(sdk_int: i32, forced: bool) -> (Rc<AndroidInsetsManager>, Rc<WindowState>, Rc<dyn IInsetsTopLevel>) {
let state = Rc::new(WindowState::default());
state.sdk_int.set(sdk_int);
state.forced.set(forced);
let top_level: Rc<dyn IInsetsTopLevel> = Rc::new(TopLevel);
let manager = AndroidInsetsManager::new(Box::new(Window(state.clone())), Rc::downgrade(&top_level));
(manager, state, top_level)
}
const BARS: RootInsets = RootInsets {
left: 0,
top: 100,
right: 0,
bottom: 48,
navigation_bars_bottom: 48,
ime_bottom: 0,
ime_visible: false,
system_bars_visible: true,
};
#[test]
fn the_values_of_the_java_layer_are_read() {
assert_eq!(RootInsets::from_array(&[0, 100, 0, 48, 48, 0, 0, 1]), Some(BARS));
assert_eq!(RootInsets::from_array(&[0, 100]), None);
}
#[test]
fn a_window_that_is_not_edge_to_edge_has_no_safe_area() {
let (manager, state, _top_level) = manager(34, false);
state.insets.set(Some(BARS));
assert!(!manager.displays_edge_to_edge());
assert_eq!(manager.safe_area_padding(), Thickness::default());
assert_eq!(*state.calls.borrow(), ["cutout false", "fits true"]);
}
#[test]
fn the_preference_displays_edge_to_edge_and_the_safe_area_is_the_bars() {
let (manager, state, _top_level) = manager(34, false);
state.insets.set(Some(BARS));
state.calls.borrow_mut().clear();
manager.set_display_edge_to_edge_preference(true);
assert!(manager.displays_edge_to_edge());
assert_eq!(manager.safe_area_padding(), Thickness::new(0.0, 50.0, 0.0, 24.0));
assert_eq!(*state.calls.borrow(), ["cutout true", "fits false", "translucent"]);
}
#[test]
fn a_system_that_forces_edge_to_edge_ignores_the_preference() {
let (manager, state, _top_level) = manager(36, true);
state.insets.set(Some(BARS));
assert!(manager.displays_edge_to_edge());
assert!(!manager.display_edge_to_edge_preference());
assert_eq!(manager.safe_area_padding(), Thickness::new(0.0, 50.0, 0.0, 24.0));
assert!(state.calls.borrow().is_empty());
manager.set_system_bar_color(Some(Color::from_argb(255, 1, 2, 3)));
assert_eq!(manager.system_bar_color(), Some(Color::from_argb(255, 1, 2, 3)));
assert_eq!(*state.calls.borrow(), ["contrast false"]);
}
#[test]
fn on_api_35_the_contrast_is_enforced_unless_the_colour_is_transparent() {
let (manager, state, _top_level) = manager(35, true);
manager.set_system_bar_color(Some(Colors::TRANSPARENT));
manager.set_system_bar_color(None);
assert_eq!(*state.calls.borrow(), ["contrast false", "contrast true"]);
}
#[test]
fn a_colour_is_set_on_a_window_that_is_not_edge_to_edge() {
let (manager, state, _top_level) = manager(34, false);
state.calls.borrow_mut().clear();
manager.set_system_bar_color(Some(Color::from_argb(255, 0x10, 0x20, 0x30)));
assert_eq!(*state.calls.borrow(), ["color ff102030"]);
manager.set_display_edge_to_edge_preference(true);
state.calls.borrow_mut().clear();
manager.set_system_bar_color(Some(Color::from_argb(255, 1, 1, 1)));
assert!(state.calls.borrow().is_empty());
}
#[test]
fn the_occluded_rectangle_is_the_input_method_above_the_navigation_bar() {
let (manager, state, _top_level) = manager(36, true);
let mut insets = BARS;
insets.ime_bottom = 848;
insets.ime_visible = true;
insets.bottom = 48;
state.insets.set(Some(insets));
assert_eq!(manager.occluded_rect(), Rect::new(0.0, 1200.0 - 24.0 - 400.0, 540.0, 400.0));
state.insets.set(None);
assert_eq!(manager.occluded_rect(), Rect::default());
}
#[test]
fn applied_insets_report_the_safe_area_and_the_state_of_the_input_pane() {
let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
let (manager, state, _top_level) = manager(29, false);
manager.set_display_edge_to_edge_preference(true);
state.insets.set(Some(BARS));
let safe_areas = Rc::new(RefCell::new(Vec::new()));
let states = Rc::new(RefCell::new(Vec::new()));
let _ = manager.safe_area_changed(Rc::new({
let safe_areas = safe_areas.clone();
move |e: &SafeAreaChangedArgs| safe_areas.borrow_mut().push(e.safe_area_padding())
}));
let _ = manager.state_changed(Rc::new({
let states = states.clone();
move |e: &InputPaneStateEventArgs| states.borrow_mut().push((e.new_state(), e.end_rect()))
}));
manager.on_apply_window_insets(true, false, 0);
assert_eq!(*safe_areas.borrow(), [Thickness::new(0.0, 50.0, 0.0, 24.0)]);
assert_eq!(manager.state(), InputPaneState::Closed);
assert!(states.borrow().is_empty());
let mut insets = BARS;
insets.ime_bottom = 848;
insets.ime_visible = true;
state.insets.set(Some(insets));
manager.on_apply_window_insets(true, true, 848);
assert_eq!(manager.state(), InputPaneState::Open);
assert_eq!(*states.borrow(), [(InputPaneState::Open, Rect::new(0.0, 776.0, 540.0, 400.0))]);
assert_eq!(safe_areas.borrow().len(), 2);
}
#[test]
fn on_android_11_a_change_of_the_inset_of_the_input_method_is_reported() {
let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
let (manager, state, _top_level) = manager(30, false);
state.insets.set(Some(BARS));
let states = Rc::new(RefCell::new(Vec::new()));
let _ = manager.state_changed(Rc::new({
let states = states.clone();
move |e: &InputPaneStateEventArgs| states.borrow_mut().push(e.new_state())
}));
manager.on_apply_window_insets(true, false, 0);
assert!(states.borrow().is_empty());
manager.on_apply_window_insets(true, true, 700);
manager.on_apply_window_insets(true, true, 700);
assert_eq!(*states.borrow(), [InputPaneState::Open]);
}
#[test]
fn the_theme_of_the_bars_falls_back_to_the_theme_of_the_system() {
let (manager, state, _top_level) = manager(34, false);
state.calls.borrow_mut().clear();
manager.set_default_system_light_mode(true);
manager.set_system_bar_theme(Some(SystemBarTheme::Dark));
assert_eq!(manager.system_bar_theme(), Some(SystemBarTheme::Dark));
manager.set_system_bar_theme(None);
assert_eq!(manager.system_bar_theme(), Some(SystemBarTheme::Light));
assert_eq!(*state.calls.borrow(), ["light false", "light true"]);
}
#[test]
fn the_visibility_of_the_bars_is_asked_of_the_insets_and_set_on_the_window() {
let (manager, state, _top_level) = manager(34, false);
state.calls.borrow_mut().clear();
assert_eq!(manager.is_system_bar_visible(), None);
state.insets.set(Some(BARS));
assert_eq!(manager.is_system_bar_visible(), Some(true));
manager.set_is_system_bar_visible(Some(false));
manager.set_is_system_bar_visible(None);
assert_eq!(*state.calls.borrow(), ["visible false", "visible true"]);
}
}
#[cfg(test)]
mod input_pane_tests {
use super::*;
use std::cell::RefCell;
struct Window {
sdk_int: i32,
insets: Rc<Cell<Option<RootInsets>>>,
}
impl IInsetsWindow for Window {
fn sdk_int(&self) -> i32 {
self.sdk_int
}
fn has_window(&self) -> bool {
true
}
fn root_insets(&self) -> Option<RootInsets> {
self.insets.get()
}
fn is_display_edge_to_edge_forced(&self) -> bool {
true
}
fn set_layout_in_display_cutout_mode(&self, _short_edges: bool) {}
fn set_decor_fits_system_windows(&self, _decor_fits_system_windows: bool) {}
fn add_translucent_bars(&self) {}
fn set_system_bar_color(&self, _color: Color) {}
fn set_navigation_bar_contrast_enforced(&self, _enforced: bool) {}
fn appearance_light_status_bars(&self) -> bool {
true
}
fn set_appearance_light_bars(&self, _light: bool) {}
fn set_system_bars_visible(&self, _visible: bool) {}
}
struct TopLevel;
impl IInsetsTopLevel for TopLevel {
fn top_level_render_scaling(&self) -> f64 {
2.0
}
fn top_level_client_size(&self) -> Size {
Size::new(540.0, 1200.0)
}
}
type Seen = Rc<RefCell<Vec<(InputPaneState, Option<Rect>, Rect, Duration, bool)>>>;
fn manager(sdk_int: i32) -> (Rc<AndroidInsetsManager>, Rc<Cell<Option<RootInsets>>>, Seen, Rc<dyn IInsetsTopLevel>) {
let insets = Rc::new(Cell::new(Some(bars(0, false))));
let top_level: Rc<dyn IInsetsTopLevel> = Rc::new(TopLevel);
let manager =
AndroidInsetsManager::new(Box::new(Window { sdk_int, insets: insets.clone() }), Rc::downgrade(&top_level));
let seen: Seen = Rc::new(RefCell::new(Vec::new()));
std::mem::forget(manager.state_changed(Rc::new({
let seen = seen.clone();
move |e: &InputPaneStateEventArgs| {
seen.borrow_mut().push((
e.new_state(),
e.start_rect(),
e.end_rect(),
e.animation_duration(),
e.easing().is_some(),
));
}
})));
(manager, insets, seen, top_level)
}
fn bars(ime_bottom: i32, ime_visible: bool) -> RootInsets {
RootInsets {
left: 0,
top: 100,
right: 0,
bottom: 48,
navigation_bars_bottom: 48,
ime_bottom,
ime_visible,
system_bars_visible: true,
}
}
#[test]
fn from_api_30_the_animation_of_the_insets_reports_the_input_pane() {
let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
let (manager, insets, seen, _top_level) = manager(36);
insets.set(Some(bars(848, true)));
manager.on_apply_window_insets(true, true, 848);
assert_eq!(manager.state(), InputPaneState::Open);
assert!(seen.borrow().is_empty());
manager.on_insets_animation_start(0, 848, 285, None);
assert_eq!(
*seen.borrow(),
vec![(
InputPaneState::Open,
Some(Rect::new(0.0, 1176.0, 540.0, 0.0)),
Rect::new(0.0, 776.0, 540.0, 400.0),
Duration::from_millis(285),
false
)]
);
assert_eq!(manager.occluded_rect(), Rect::new(0.0, 776.0, 540.0, 400.0));
seen.borrow_mut().clear();
insets.set(Some(bars(0, false)));
manager.on_apply_window_insets(true, false, 0);
manager.on_insets_animation_start(0, 848, 200, None);
assert_eq!(
*seen.borrow(),
vec![(
InputPaneState::Closed,
Some(Rect::new(0.0, 776.0, 540.0, 400.0)),
Rect::new(0.0, 1176.0, 540.0, 0.0),
Duration::from_millis(200),
false
)]
);
}
#[test]
fn below_api_30_the_global_layout_reports_the_input_pane_at_once() {
let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
let (manager, insets, seen, _top_level) = manager(29);
manager.on_global_layout();
assert!(seen.borrow().is_empty());
insets.set(Some(bars(848, true)));
manager.on_global_layout();
assert_eq!(manager.state(), InputPaneState::Open);
assert_eq!(seen.borrow().len(), 1);
let (state, _, end, duration, easing) = seen.borrow()[0];
assert_eq!((state, end, duration, easing), (InputPaneState::Open, Rect::new(0.0, 776.0, 540.0, 400.0), Duration::ZERO, false));
manager.on_global_layout();
assert_eq!(seen.borrow().len(), 1);
}
#[test]
fn from_api_30_the_global_layout_does_not_touch_the_state() {
let _scope = ferroui_base::threading::Dispatcher::unit_test_scope();
let (manager, insets, seen, _top_level) = manager(36);
insets.set(Some(bars(848, true)));
manager.on_global_layout();
assert_eq!(manager.state(), InputPaneState::Closed);
assert!(seen.borrow().is_empty());
}
}