Skip to main content

lingxia_platform/traits/
app_runtime.rs

1use std::io::Read;
2use std::path::{Path, PathBuf};
3
4use crate::AssetFileEntry;
5use crate::error::PlatformError;
6
7use super::PlatformFuture;
8use super::device::{Device, DeviceHardware};
9use super::file::FileService;
10use super::location::Location;
11use super::media_interaction::{MediaInteraction, MediaKind};
12use super::media_runtime::MediaRuntime;
13use super::network::Network;
14use super::secure_store::SecureStore;
15use super::share::ShareService;
16use super::ui::{SurfacePresenter, UIUpdate, UserFeedback};
17use super::update::UpdateService;
18use super::wifi::Wifi;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AnimationType {
22    None = 0,
23    Forward = 1,
24    Backward = 2,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum LxAppOpenMode {
29    #[default]
30    Normal = 0,
31    Panel = 1,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum OpenUrlTarget {
36    External = 0,
37    SelfTarget = 1,
38    /// Open a new browser tab unconditionally (skips "navigate current tab" heuristic).
39    NewBrowserTab = 2,
40    /// Open in the compact in-app browser as an API-managed aside tab. It uses
41    /// the one-row toolbar without address editing or user tab creation.
42    AsideBrowser = 3,
43}
44
45impl OpenUrlTarget {
46    pub fn parse(raw: Option<&str>) -> Self {
47        match raw.map(|v| v.trim().to_ascii_lowercase()) {
48            Some(v) if v == "self" => Self::SelfTarget,
49            Some(v) if v == "new_browser_tab" => Self::NewBrowserTab,
50            Some(v) if v == "aside" => Self::AsideBrowser,
51            Some(v) if v == "external" => Self::External,
52            Some(v) => {
53                log::warn!("Invalid openURL target='{}', fallback to external", v);
54                Self::External
55            }
56            None => Self::External,
57        }
58    }
59}
60
61#[derive(Debug, Clone)]
62pub struct OpenUrlRequest {
63    pub owner_appid: String,
64    pub owner_session_id: u64,
65    pub url: String,
66    pub target: OpenUrlTarget,
67    /// When true, the host should create the in-app tab before returning and
68    /// report its id. Fire-and-forget callers (new-window, navigation) leave
69    /// this false so the work can hop off a WebView UI thread.
70    pub want_tab_id: bool,
71}
72
73/// Outcome of [`AppRuntime::open_url`]. `tab_id` is set when the host named
74/// the tab it opened; `None` means the browser chrome owns the strip.
75#[derive(Debug, Clone, Default, PartialEq, Eq)]
76pub struct OpenUrlResult {
77    pub tab_id: Option<String>,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum BuiltinBrowserPage {
82    Downloads = 1,
83}
84
85impl From<i32> for AnimationType {
86    fn from(value: i32) -> Self {
87        match value {
88            1 => AnimationType::Forward,
89            2 => AnimationType::Backward,
90            _ => AnimationType::None,
91        }
92    }
93}
94
95pub trait AppRuntime:
96    Send
97    + Sync
98    + MediaInteraction
99    + MediaRuntime
100    + Network
101    + SurfacePresenter
102    + Device
103    + DeviceHardware
104    + SecureStore
105    + ShareService
106    + FileService
107    + Location
108    + UIUpdate
109    + UpdateService
110    + UserFeedback
111    + Wifi
112    + 'static
113{
114    /// Reads an asset file as a streaming reader.
115    fn read_asset<'a>(&'a self, path: &str) -> Result<Box<dyn Read + 'a>, PlatformError>;
116
117    /// Iterates over files in an asset directory.
118    fn asset_dir_iter<'a>(
119        &'a self,
120        asset_dir: &str,
121    ) -> Box<dyn Iterator<Item = Result<AssetFileEntry<'a>, PlatformError>> + 'a>;
122
123    /// Returns the app's data directory path.
124    fn app_data_dir(&self) -> PathBuf;
125
126    /// Returns the app's cache directory path.
127    fn app_cache_dir(&self) -> PathBuf;
128
129    /// Obtains the application identifier.
130    fn get_app_identifier(&self) -> Result<String, PlatformError>;
131
132    /// Copies media from the system album to a local file.
133    fn copy_album_media_to_file(
134        &self,
135        uri: &str,
136        dest_path: &Path,
137        kind: MediaKind,
138    ) -> Result<(), PlatformError> {
139        MediaRuntime::copy_album_media_to_file(self, uri, dest_path, kind)
140    }
141
142    /// Returns the current system locale.
143    fn get_system_locale(&self) -> &str;
144
145    /// Show the UI container for the given LxApp and route.
146    /// `webtag` is the page instance's full webview tag; page tags are
147    /// per-instance, so shells must not reconstruct them from the route.
148    /// Platforms whose containers resolve through the runtime by path may
149    /// ignore it.
150    fn show_lxapp(
151        &self,
152        appid: String,
153        title: String,
154        path: String,
155        webtag: String,
156        session_id: u64,
157        open_mode: LxAppOpenMode,
158        panel_id: String,
159    ) -> Result<(), PlatformError>;
160
161    /// Notify the desktop skin that the next layout publication is an explicit
162    /// request to put this lxapp in front. Most skins reconcile directly from
163    /// the layout plan; Windows uses the intent to replace a browser cover
164    /// without treating unrelated resize/aside publications as activations.
165    fn request_lxapp_main_activation(&self, _appid: &str) {}
166
167    /// Hide the UI container for the given LxApp (does not destroy its runtime state).
168    fn hide_lxapp(&self, appid: String, session_id: u64) -> Result<(), PlatformError>;
169
170    /// Exits the host app.
171    fn exit(&self) -> Result<(), PlatformError>;
172
173    // Tray / badge chrome. These are cosmetic enhancements, so platforms that
174    // lack the chrome (e.g. no menu-bar tray on mobile) no-op rather than error —
175    // portable code can call them unconditionally. A supporting platform returns
176    // Err only on genuine failure.
177
178    /// Set the tray (menu-bar / system-tray) badge. Desktop only; no-op elsewhere.
179    fn set_tray_badge(&self, _text: &str) -> Result<(), PlatformError> {
180        Ok(())
181    }
182
183    /// Set the tray icon (a resource path). Desktop only; no-op elsewhere.
184    fn set_tray_icon(&self, _icon: &str) -> Result<(), PlatformError> {
185        Ok(())
186    }
187
188    /// Replace the resolved shell sidebar action render list. Desktop skins only
189    /// render presentation metadata and report stable ids.
190    fn set_shell_sidebar_actions(
191        &self,
192        _items: &[lingxia_shell::ResolvedShellSidebarAction],
193    ) -> Result<(), PlatformError> {
194        Ok(())
195    }
196
197    /// Replace the ordered mixed user Pin list. Platform skins resolve visual
198    /// metadata only; target identity and the eight-item limit are shell-owned.
199    fn set_shell_pins(&self, _items: &[lingxia_shell::ShellPin]) -> Result<(), PlatformError> {
200        Ok(())
201    }
202
203    /// Set the tray title (text beside the icon, macOS). Desktop only; no-op elsewhere.
204    fn set_tray_title(&self, _text: &str) -> Result<(), PlatformError> {
205        Ok(())
206    }
207
208    /// Set the app-icon badge: dock (macOS) / taskbar (Windows) / launcher icon
209    /// (iOS, Android). No-op on platforms where it is not yet wired.
210    fn set_app_badge(&self, _text: &str) -> Result<(), PlatformError> {
211        Ok(())
212    }
213
214    /// Whether the app is registered to launch at system startup. Only reached
215    /// on macOS/Windows — `lx.app.autostart` is not registered elsewhere — so
216    /// the default is an error, not a no-op: a false answer here would be a lie.
217    fn autostart_is_enabled(&self) -> Result<bool, PlatformError> {
218        Err(PlatformError::NotSupported("autostart".to_string()))
219    }
220
221    /// Register or unregister the app as a per-user startup item.
222    fn autostart_set_enabled(&self, _enabled: bool) -> Result<(), PlatformError> {
223        Err(PlatformError::NotSupported("autostart".to_string()))
224    }
225
226    /// Replace the tray dropdown menu. `items_json` is a JSON array of
227    /// `{ label?, separator?, enabled?, checked? }`. Item clicks are delivered
228    /// back to JS by index. Desktop only; no-op elsewhere.
229    fn set_tray_menu(&self, _items_json: &str) -> Result<(), PlatformError> {
230        Ok(())
231    }
232
233    /// Show or hide the tray status item itself. Desktop only; no-op elsewhere.
234    fn set_tray_visible(&self, _visible: bool) -> Result<(), PlatformError> {
235        Ok(())
236    }
237
238    /// When intercepting, a left-click on the tray is delivered only to JS
239    /// (`lx.tray.onClick`) and does not run the tray's configured surface action.
240    /// Desktop only; no-op elsewhere.
241    fn set_tray_click_intercept(&self, _intercept: bool) -> Result<(), PlatformError> {
242        Ok(())
243    }
244
245    /// Navigates within the given LxApp using an animation.
246    /// `webtag` is the destination page instance's full webview tag; page
247    /// tags are per-instance, so shells must not reconstruct them from the
248    /// route. Platforms whose containers resolve through the runtime by path
249    /// may ignore it.
250    fn navigate(
251        &self,
252        appid: String,
253        path: String,
254        webtag: String,
255        animation_type: AnimationType,
256    ) -> Result<(), PlatformError>;
257
258    /// Opens the given URL according to the host policy for the requested target.
259    fn open_url(&self, req: OpenUrlRequest) -> Result<OpenUrlResult, PlatformError>;
260
261    /// Close a tab previously named by [`Self::open_url`]. Platforms that
262    /// cannot name tabs leave this unimplemented — the JS handle then reports
263    /// `scope: 'group'` and never calls it.
264    fn close_browser_tab(&self, _tab_id: &str) -> Result<(), PlatformError> {
265        Err(PlatformError::NotSupported("browser tab".to_string()))
266    }
267
268    /// Bring a tab previously named by [`Self::open_url`] to the front.
269    fn activate_browser_tab(&self, _tab_id: String) -> PlatformFuture {
270        Box::pin(async { Err(PlatformError::NotSupported("browser tab".to_string())) })
271    }
272
273    fn open_builtin_browser_page(&self, _page: BuiltinBrowserPage) -> Result<(), PlatformError> {
274        Err(PlatformError::NotSupported(
275            "built-in browser pages".to_string(),
276        ))
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::OpenUrlTarget;
283
284    #[test]
285    fn parse_supports_new_browser_tab() {
286        assert_eq!(
287            OpenUrlTarget::parse(Some("new_browser_tab")),
288            OpenUrlTarget::NewBrowserTab
289        );
290    }
291
292    #[test]
293    fn parse_unknown_falls_back_to_external() {
294        assert_eq!(
295            OpenUrlTarget::parse(Some("foobar")),
296            OpenUrlTarget::External
297        );
298    }
299}