Skip to main content

polyhorn_ios/components/
keyboard_avoiding_view.rs

1use polyhorn_core::CommandBuffer;
2use polyhorn_ios_sys::coregraphics::CGRect;
3use polyhorn_ios_sys::foundation::NSNumber;
4use polyhorn_ios_sys::polykit::{PLYCallback, PLYKeyboardAvoidingView, PLYView};
5use polyhorn_ui::geometry::{ByEdge, Dimension};
6use polyhorn_ui::layout::LayoutAxisY;
7use polyhorn_ui::styles::{Position, Relative, ViewStyle};
8use std::sync::Arc;
9
10use crate::prelude::*;
11use crate::raw::{Builtin, Container, ContainerID, OpaqueContainer};
12use crate::{Key, Reference};
13
14/// Structure that contains all updates that should be made to a view's layout
15/// in response to a change in the visibility or dimensions of the system's
16/// virtual keyboard.
17#[derive(Default)]
18pub struct LayoutAdjustment {
19    margin: ByEdge<Dimension<f32>>,
20}
21
22impl LayoutAdjustment {
23    /// Returns a new and empty layout adjustment.
24    pub fn new() -> LayoutAdjustment {
25        Default::default()
26    }
27
28    /// Sets the margin bottom of this layout adjustment. Note that this will
29    /// be added to the existing margin of the view.
30    pub fn margin_bottom(self, bottom: Dimension<f32>) -> LayoutAdjustment {
31        LayoutAdjustment {
32            margin: ByEdge {
33                vertical: LayoutAxisY {
34                    bottom,
35                    ..self.margin.vertical
36                },
37                ..self.margin
38            },
39            ..self
40        }
41    }
42}
43
44/// A view that automatically adjusts its layout when the system keyboard
45/// appears, changes its dimensions or disappears.
46#[derive(Default)]
47pub struct KeyboardAvoidingView {
48    /// Transformation function that should return a adjustment based on the
49    /// keyboard's height, that we apply to the layout of this view.
50    pub transform: Option<Arc<dyn Fn(f32) -> LayoutAdjustment + Send + Sync>>,
51}
52
53impl Container for PLYKeyboardAvoidingView {
54    fn mount(&mut self, child: &mut OpaqueContainer) {
55        if let Some(view) = child.container().to_view() {
56            PLYKeyboardAvoidingView::to_view(self).add_subview(&view)
57        }
58    }
59
60    fn unmount(&mut self) {
61        PLYKeyboardAvoidingView::to_view(self).remove_from_superview();
62    }
63
64    fn to_view(&self) -> Option<PLYView> {
65        Some(PLYKeyboardAvoidingView::to_view(self))
66    }
67}
68
69impl Component for KeyboardAvoidingView {
70    fn render(&self, manager: &mut Manager) -> Element {
71        let view_ref: Reference<Option<ContainerID>> = use_reference!(manager, None);
72
73        let transform = self.transform.clone();
74
75        use_layout_effect!(manager, move |link, buffer| {
76            let id = match view_ref.apply(link, |id| id.to_owned()) {
77                Some(id) => id,
78                None => return,
79            };
80
81            buffer.mutate(&[id], move |containers, _| {
82                let container = &mut containers[0];
83
84                let layout = match container.layout() {
85                    Some(layout) => layout.clone(),
86                    None => return,
87                };
88
89                if let Some(view) = container.downcast_mut::<PLYKeyboardAvoidingView>() {
90                    {
91                        let layout = layout.clone();
92
93                        view.set_on_keyboard(PLYCallback::new(move |height: NSNumber| {
94                            if let Some(transform) = transform.as_ref() {
95                                let adjustment = transform(height.float_value());
96
97                                layout.set_style(ViewStyle {
98                                    position: Position::Relative(Relative {
99                                        flex_grow: 1.0,
100                                        ..Default::default()
101                                    }),
102                                    margin: ByEdge {
103                                        vertical: LayoutAxisY {
104                                            bottom: adjustment.margin.vertical.bottom,
105                                            ..Default::default()
106                                        },
107                                        ..Default::default()
108                                    },
109                                    ..Default::default()
110                                });
111
112                                layout.layouter().write().unwrap().recompute_roots();
113                            }
114                        }));
115                    }
116
117                    view.to_view().set_layout(move || {
118                        let current = layout.current();
119
120                        CGRect::new(
121                            current.origin.x as _,
122                            current.origin.y as _,
123                            current.size.width as _,
124                            current.size.height as _,
125                        )
126                    });
127                }
128            });
129        });
130
131        Element::builtin(
132            Key::new(()),
133            Builtin::KeyboardAvoidingView,
134            manager.children(),
135            Some(view_ref.weak(manager)),
136        )
137    }
138}