Skip to main content

telegram_webapp_sdk/webapp/
viewport.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use js_sys::Reflect;
5use wasm_bindgen::JsValue;
6
7use crate::webapp::{TelegramWebApp, types::SafeAreaInset};
8
9impl TelegramWebApp {
10    /// Returns the current viewport height in pixels.
11    ///
12    /// # Examples
13    /// ```no_run
14    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
15    /// # let app = TelegramWebApp::instance().unwrap();
16    /// let _ = app.viewport_height();
17    /// ```
18    pub fn viewport_height(&self) -> Option<f64> {
19        Reflect::get(&self.inner, &"viewportHeight".into())
20            .ok()?
21            .as_f64()
22    }
23
24    /// Returns the current viewport width in pixels.
25    ///
26    /// # Examples
27    /// ```no_run
28    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
29    /// # let app = TelegramWebApp::instance().unwrap();
30    /// let _ = app.viewport_width();
31    /// ```
32    pub fn viewport_width(&self) -> Option<f64> {
33        Reflect::get(&self.inner, &"viewportWidth".into())
34            .ok()?
35            .as_f64()
36    }
37
38    /// Returns the stable viewport height in pixels.
39    ///
40    /// # Examples
41    /// ```no_run
42    /// # use telegram_webapp_sdk::webapp::TelegramWebApp;
43    /// # let app = TelegramWebApp::instance().unwrap();
44    /// let _ = app.viewport_stable_height();
45    /// ```
46    pub fn viewport_stable_height(&self) -> Option<f64> {
47        Reflect::get(&self.inner, &"viewportStableHeight".into())
48            .ok()?
49            .as_f64()
50    }
51
52    /// Call `WebApp.expand()` to expand the viewport.
53    ///
54    /// # Errors
55    /// Returns [`JsValue`] if the underlying JS call fails.
56    pub fn expand_viewport(&self) -> Result<(), JsValue> {
57        self.call0("expand")
58    }
59
60    pub(super) fn safe_area_from_property(&self, property: &str) -> Option<SafeAreaInset> {
61        let value = Reflect::get(&self.inner, &property.into()).ok()?;
62        SafeAreaInset::from_js(value)
63    }
64
65    /// Returns the safe area insets reported by Telegram.
66    ///
67    /// # Examples
68    /// ```no_run
69    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
70    ///
71    /// if let Some(app) = TelegramWebApp::instance() {
72    ///     let _ = app.safe_area_inset();
73    /// }
74    /// ```
75    pub fn safe_area_inset(&self) -> Option<SafeAreaInset> {
76        self.safe_area_from_property("safeAreaInset")
77    }
78
79    /// Returns the content safe area insets reported by Telegram.
80    ///
81    /// # Examples
82    /// ```no_run
83    /// use telegram_webapp_sdk::webapp::TelegramWebApp;
84    ///
85    /// if let Some(app) = TelegramWebApp::instance() {
86    ///     let _ = app.content_safe_area_inset();
87    /// }
88    /// ```
89    pub fn content_safe_area_inset(&self) -> Option<SafeAreaInset> {
90        self.safe_area_from_property("contentSafeAreaInset")
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use std::{cell::Cell, rc::Rc};
97
98    use js_sys::{Object, Reflect};
99    use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
100    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
101    use web_sys::window;
102
103    use crate::webapp::TelegramWebApp;
104
105    wasm_bindgen_test_configure!(run_in_browser);
106
107    fn setup_webapp() -> Object {
108        let win = window().expect("window");
109        let telegram = Object::new();
110        let webapp = Object::new();
111        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
112        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
113        webapp
114    }
115
116    #[wasm_bindgen_test]
117    #[allow(dead_code, clippy::unused_unit)]
118    fn viewport_height_returns_f64() {
119        let webapp = setup_webapp();
120        let _ = Reflect::set(&webapp, &"viewportHeight".into(), &JsValue::from_f64(640.0));
121        let app = TelegramWebApp::instance().expect("instance");
122        assert_eq!(app.viewport_height(), Some(640.0));
123    }
124
125    #[wasm_bindgen_test]
126    #[allow(dead_code, clippy::unused_unit)]
127    fn viewport_height_returns_none_when_property_missing() {
128        let _ = setup_webapp();
129        let app = TelegramWebApp::instance().expect("instance");
130        assert!(app.viewport_height().is_none());
131    }
132
133    #[wasm_bindgen_test]
134    #[allow(dead_code, clippy::unused_unit)]
135    fn viewport_width_reads_viewport_width_property() {
136        // NOTE: real Telegram does not expose `viewportWidth`; this test pins
137        // current crate behaviour so a future removal is intentional.
138        let webapp = setup_webapp();
139        let _ = Reflect::set(&webapp, &"viewportWidth".into(), &JsValue::from_f64(360.0));
140        let app = TelegramWebApp::instance().expect("instance");
141        assert_eq!(app.viewport_width(), Some(360.0));
142    }
143
144    #[wasm_bindgen_test]
145    #[allow(dead_code, clippy::unused_unit)]
146    fn viewport_stable_height_returns_f64() {
147        let webapp = setup_webapp();
148        let _ = Reflect::set(
149            &webapp,
150            &"viewportStableHeight".into(),
151            &JsValue::from_f64(600.0)
152        );
153        let app = TelegramWebApp::instance().expect("instance");
154        assert_eq!(app.viewport_stable_height(), Some(600.0));
155    }
156
157    #[wasm_bindgen_test]
158    #[allow(dead_code, clippy::unused_unit)]
159    fn expand_viewport_calls_js_expand() {
160        let webapp = setup_webapp();
161        let called = Rc::new(Cell::new(false));
162        let called_ref = called.clone();
163        let cb = Closure::<dyn FnMut()>::new(move || called_ref.set(true));
164        let _ = Reflect::set(&webapp, &"expand".into(), cb.as_ref().unchecked_ref());
165        cb.forget();
166
167        let app = TelegramWebApp::instance().expect("instance");
168        app.expand_viewport().expect("ok");
169        assert!(called.get());
170    }
171
172    #[wasm_bindgen_test]
173    #[allow(dead_code, clippy::unused_unit)]
174    fn expand_viewport_errors_when_method_missing() {
175        let _ = setup_webapp();
176        let app = TelegramWebApp::instance().expect("instance");
177        assert!(app.expand_viewport().is_err());
178    }
179}