use super::{Layout, LayoutContext};
use crate::compat::{fmt, Any, Box, Vec};
use crate::core::{ObjectId, Rect};
use crate::style::animation::EasingFunction;
pub struct KeyboardAwareLayout {
inner: Box<dyn Layout>,
keyboard_offset: i32,
animation_duration: u64,
current_offset: i32,
transition_start: i32,
elapsed_ms: Option<u64>,
}
impl fmt::Debug for KeyboardAwareLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeyboardAwareLayout")
.field("keyboard_offset", &self.keyboard_offset)
.field("current_offset", &self.current_offset)
.field("transition_start", &self.transition_start)
.field("animation_duration", &self.animation_duration)
.field("inner", &"<dyn Layout>")
.finish()
}
}
impl KeyboardAwareLayout {
pub fn new(inner: Box<dyn Layout>, animation_duration: u64) -> Self {
Self {
inner,
keyboard_offset: 0,
animation_duration,
current_offset: 0,
transition_start: 0,
elapsed_ms: None,
}
}
pub fn set_keyboard_offset(&mut self, offset: i32) {
if offset == self.keyboard_offset {
return;
}
self.keyboard_offset = offset;
if self.animation_duration == 0 {
self.current_offset = offset;
self.elapsed_ms = None;
} else {
self.transition_start = self.current_offset;
self.elapsed_ms = Some(0);
}
}
pub fn keyboard_offset(&self) -> i32 {
self.keyboard_offset
}
pub fn current_offset(&self) -> i32 {
self.current_offset
}
pub fn tick(&mut self, delta_ms: u32) -> bool {
let Some(elapsed) = self.elapsed_ms else {
return false;
};
if self.animation_duration == 0 {
self.current_offset = self.keyboard_offset;
self.elapsed_ms = None;
return false;
}
let elapsed = elapsed.saturating_add(u64::from(delta_ms));
let progress = (elapsed as f32 / self.animation_duration as f32).min(1.0);
let eased = EasingFunction::EaseInOut.apply(progress);
let from = self.transition_start as f32;
let to = self.keyboard_offset as f32;
self.current_offset = (from + (to - from) * eased).round() as i32;
if progress >= 1.0 {
self.current_offset = self.keyboard_offset;
self.elapsed_ms = None;
false
} else {
self.elapsed_ms = Some(elapsed);
true
}
}
pub fn animation_duration(&self) -> u64 {
self.animation_duration
}
pub fn set_animation_duration(&mut self, duration: u64) {
self.animation_duration = duration;
if duration == 0 {
self.current_offset = self.keyboard_offset;
self.elapsed_ms = None;
}
}
pub fn is_animating(&self) -> bool {
self.elapsed_ms.is_some()
}
pub fn inner_layout(&self) -> &dyn Layout {
self.inner.as_ref()
}
pub fn inner_layout_mut(&mut self) -> &mut dyn Layout {
self.inner.as_mut()
}
pub fn into_inner(self) -> Box<dyn Layout> {
self.inner
}
}
impl Layout for KeyboardAwareLayout {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn add_widget(&mut self, widget_id: ObjectId, stretch: u32) {
self.inner.add_widget(widget_id, stretch);
}
fn remove_widget(&mut self, widget_id: ObjectId) {
self.inner.remove_widget(widget_id);
}
fn child_ids(&self) -> Vec<ObjectId> {
self.inner.child_ids()
}
fn has_child(&self, id: ObjectId) -> bool {
self.inner.has_child(id)
}
fn clear(&mut self) {
self.inner.clear();
}
fn update(&self, rect: Rect, widgets: &mut dyn FnMut(ObjectId, Rect)) {
let adjusted = Rect::new(rect.x, rect.y - self.current_offset, rect.width, rect.height);
self.inner.update(adjusted, widgets);
}
fn update_with_context(
&self,
rect: Rect,
context: &LayoutContext,
widgets: &mut dyn FnMut(ObjectId, Rect),
) {
let adjusted = Rect::new(rect.x, rect.y - self.current_offset, rect.width, rect.height);
self.inner.update_with_context(adjusted, context, widgets);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::HashMap;
use crate::layout::stack::StackLayout;
fn collect_update(layout: &dyn Layout, rect: Rect) -> HashMap<ObjectId, Rect> {
let mut rects = HashMap::new();
layout.update(rect, &mut |id, r| {
rects.insert(id, r);
});
rects
}
#[test]
fn keyboard_aware_passes_through_add_remove() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 200);
layout.add_widget(1, 0);
layout.add_widget(2, 0);
assert!(layout.has_child(1));
assert!(layout.has_child(2));
assert_eq!(layout.child_ids().len(), 2);
layout.remove_widget(1);
assert!(!layout.has_child(1));
assert!(layout.has_child(2));
}
#[test]
fn keyboard_aware_clear() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 200);
layout.add_widget(1, 0);
layout.add_widget(2, 0);
assert_eq!(layout.child_ids().len(), 2);
layout.clear();
assert_eq!(layout.child_ids().len(), 0);
}
#[test]
fn keyboard_aware_no_offset_passthrough() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 200);
layout.add_widget(42, 0);
let rects = collect_update(&layout, Rect::new(0, 0, 100, 200));
assert_eq!(rects.get(&42), Some(&Rect::new(0, 0, 100, 200)));
}
#[test]
fn keyboard_aware_offset_shifts_content_up() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 0);
layout.add_widget(42, 0);
layout.set_keyboard_offset(150);
let rects = collect_update(&layout, Rect::new(0, 0, 100, 400));
assert_eq!(rects.get(&42), Some(&Rect::new(0, -150, 100, 400)));
}
#[test]
fn a_non_zero_duration_slides_the_offset_instead_of_snapping() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 100);
layout.add_widget(42, 0);
layout.set_keyboard_offset(100);
assert!(layout.is_animating(), "setting a target starts a transition");
assert_eq!(layout.current_offset(), 0, "it starts where it was");
assert!(layout.tick(50), "the transition is unfinished");
assert_eq!(layout.current_offset(), 50);
let rects = collect_update(&layout, Rect::new(0, 0, 100, 400));
assert_eq!(rects.get(&42), Some(&Rect::new(0, -50, 100, 400)));
assert!(!layout.tick(50), "the transition is finished");
assert_eq!(layout.current_offset(), 100);
assert!(!layout.is_animating());
let rects = collect_update(&layout, Rect::new(0, 0, 100, 400));
assert_eq!(rects.get(&42), Some(&Rect::new(0, -100, 100, 400)));
}
#[test]
fn a_reversed_slide_resumes_from_the_current_position() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 100);
layout.set_keyboard_offset(100);
layout.tick(50);
assert_eq!(layout.current_offset(), 50);
layout.set_keyboard_offset(0);
assert_eq!(layout.current_offset(), 50, "no jump on reversal");
layout.tick(50);
assert_eq!(layout.current_offset(), 25, "it walks back from where it was");
layout.tick(50);
assert_eq!(layout.current_offset(), 0);
}
#[test]
fn equal_total_time_produces_an_equal_offset_regardless_of_slicing() {
let mut one = KeyboardAwareLayout::new(Box::new(StackLayout::new()), 1000);
let mut two = KeyboardAwareLayout::new(Box::new(StackLayout::new()), 1000);
one.set_keyboard_offset(100);
two.set_keyboard_offset(100);
one.tick(500);
let single = one.current_offset();
two.tick(250);
two.tick(250);
let sliced = two.current_offset();
assert_eq!(single, sliced, "the same total time must yield the same value");
}
#[test]
fn disabling_the_duration_settles_an_in_flight_slide() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 100);
layout.set_keyboard_offset(80);
layout.tick(10);
assert!(layout.is_animating());
layout.set_animation_duration(0);
assert!(!layout.is_animating(), "the transition was resolved, not dropped");
assert_eq!(layout.current_offset(), 80);
}
#[test]
fn setting_the_same_offset_does_not_restart_the_slide() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 100);
layout.set_keyboard_offset(60);
layout.tick(99);
let nearly_there = layout.current_offset();
layout.set_keyboard_offset(60);
assert_eq!(layout.current_offset(), nearly_there, "the progress was preserved");
assert!(!layout.tick(1), "it still arrives on the same frame");
}
#[test]
fn keyboard_aware_zero_offset_after_set() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 0);
layout.add_widget(42, 0);
layout.set_keyboard_offset(200);
layout.set_keyboard_offset(0);
let rects = collect_update(&layout, Rect::new(0, 0, 100, 200));
assert_eq!(rects.get(&42), Some(&Rect::new(0, 0, 100, 200)));
}
#[test]
fn keyboard_aware_getters() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let layout = KeyboardAwareLayout::new(inner, 300);
assert_eq!(layout.animation_duration(), 300);
assert_eq!(layout.keyboard_offset(), 0);
assert_eq!(layout.current_offset(), 0);
assert!(!layout.is_animating());
}
#[test]
fn keyboard_aware_update_with_context_applies_offset() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 0);
layout.add_widget(7, 0);
layout.set_keyboard_offset(100);
let context = LayoutContext::default();
let mut rects = HashMap::new();
layout.update_with_context(Rect::new(0, 0, 200, 300), &context, &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&7), Some(&Rect::new(0, -100, 200, 300)));
}
#[test]
fn keyboard_aware_inner_layout_access() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let layout = KeyboardAwareLayout::new(inner, 200);
let inner = layout.inner_layout();
assert!(inner.as_any().is::<StackLayout>());
}
#[test]
fn keyboard_aware_inner_layout_mut_access() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let mut layout = KeyboardAwareLayout::new(inner, 200);
let inner_mut = layout.inner_layout_mut();
inner_mut.add_widget(99, 0);
assert!(layout.has_child(99));
}
#[test]
fn keyboard_aware_into_inner() {
let inner: Box<dyn Layout> = Box::new(StackLayout::new());
let layout = KeyboardAwareLayout::new(inner, 200);
let _inner = layout.into_inner();
}
}