Skip to main content

servoshell/desktop/
app.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Application entry point, runs the event loop.
6
7use std::path::Path;
8use std::rc::Rc;
9use std::time::Instant;
10use std::{env, fs};
11
12use servo::protocol_handler::ProtocolRegistry;
13use servo::{
14    EventLoopWaker, Opts, Preferences, ServoBuilder, ServoUrl, UserContentManager, UserScript,
15};
16use url::Url;
17use winit::application::ApplicationHandler;
18use winit::event::WindowEvent;
19use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
20use winit::window::WindowId;
21
22use super::event_loop::AppEvent;
23use crate::desktop::event_loop::ServoShellEventLoop;
24use crate::desktop::headed_window::HeadedWindow;
25use crate::desktop::headless_window::HeadlessWindow;
26use crate::desktop::protocols;
27use crate::desktop::tracing::trace_winit_event;
28use crate::parser::get_default_url;
29use crate::prefs::ServoShellPreferences;
30use crate::running_app_state::RunningAppState;
31#[cfg(feature = "gamepad")]
32use crate::running_app_state::ServoshellGamepadDelegate;
33use crate::window::{PlatformWindow, ServoShellWindowId, TopLevelWebViewCreationRequest};
34
35pub(crate) enum AppState {
36    Initializing,
37    Running(Rc<RunningAppState>),
38    ShuttingDown,
39}
40
41pub struct App {
42    opts: Opts,
43    preferences: Preferences,
44    servoshell_preferences: ServoShellPreferences,
45    waker: Box<dyn EventLoopWaker>,
46    event_loop_proxy: Option<EventLoopProxy<AppEvent>>,
47    initial_url: ServoUrl,
48    t_start: Instant,
49    t: Instant,
50    state: AppState,
51}
52
53impl App {
54    pub fn new(
55        opts: Opts,
56        preferences: Preferences,
57        servo_shell_preferences: ServoShellPreferences,
58        event_loop: &ServoShellEventLoop,
59    ) -> Self {
60        let initial_url = get_default_url(
61            servo_shell_preferences.url.as_deref(),
62            env::current_dir().unwrap(),
63            |path| fs::metadata(path).is_ok(),
64            &servo_shell_preferences,
65        );
66
67        let t = Instant::now();
68        App {
69            opts,
70            preferences,
71            servoshell_preferences: servo_shell_preferences,
72            waker: event_loop.create_event_loop_waker(),
73            event_loop_proxy: event_loop.event_loop_proxy(),
74            initial_url,
75            t_start: t,
76            t,
77            state: AppState::Initializing,
78        }
79    }
80
81    /// Initialize Application once event loop start running.
82    pub fn init(&mut self, active_event_loop: Option<&ActiveEventLoop>) {
83        let mut protocol_registry = ProtocolRegistry::default();
84        let _ = protocol_registry.register(
85            "urlinfo",
86            protocols::urlinfo::UrlInfoProtocolHander::default(),
87        );
88        let _ =
89            protocol_registry.register("servo", protocols::servo::ServoProtocolHandler::default());
90        let _ = protocol_registry.register(
91            "resource",
92            protocols::resource::ResourceProtocolHandler::default(),
93        );
94
95        let servo_builder = ServoBuilder::default()
96            .opts(self.opts.clone())
97            .preferences(self.preferences.clone())
98            .protocol_registry(protocol_registry)
99            .event_loop_waker(self.waker.clone());
100
101        let url = self.initial_url.as_url().clone();
102
103        let servo = servo_builder.build();
104        let platform_window = self.create_platform_window(url, active_event_loop);
105
106        #[cfg(feature = "webxr")]
107        servo.register_webxr_registry(super::webxr::XrDiscoveryWebXrRegistry::new_boxed(
108            platform_window.clone(),
109            active_event_loop,
110            &self.preferences,
111        ));
112
113        servo.setup_logging();
114
115        let user_content_manager = Rc::new(UserContentManager::new(&servo));
116        for script in load_userscripts(self.servoshell_preferences.userscripts_directory.as_deref())
117            .expect("Loading userscripts failed")
118        {
119            user_content_manager.add_script(Rc::new(script));
120        }
121
122        for user_stylesheet in &self.servoshell_preferences.user_stylesheets {
123            user_content_manager.add_stylesheet(user_stylesheet.clone());
124        }
125
126        let running_state = Rc::new(RunningAppState::new(
127            servo,
128            self.servoshell_preferences.clone(),
129            self.waker.clone(),
130            user_content_manager,
131            self.preferences.clone(),
132            #[cfg(feature = "gamepad")]
133            self.event_loop_proxy
134                .clone()
135                .map(ServoshellGamepadDelegate::new)
136                .map(Rc::new),
137        ));
138        let initial_url = self.initial_url.as_url().clone();
139        running_state.open_window(
140            platform_window,
141            TopLevelWebViewCreationRequest::WithUrl(initial_url),
142        );
143
144        self.state = AppState::Running(running_state);
145    }
146
147    #[servo::servo_tracing::instrument(level = "debug", skip_all)]
148    fn create_platform_window(
149        &self,
150        url: Url,
151        active_event_loop: Option<&ActiveEventLoop>,
152    ) -> Rc<dyn PlatformWindow> {
153        assert_eq!(
154            self.servoshell_preferences.headless,
155            active_event_loop.is_none()
156        );
157
158        let Some(active_event_loop) = active_event_loop else {
159            return HeadlessWindow::new(&self.servoshell_preferences);
160        };
161
162        HeadedWindow::new(
163            &self.servoshell_preferences,
164            active_event_loop,
165            self.event_loop_proxy
166                .clone()
167                .expect("Should always have event loop proxy in headed mode."),
168            url,
169        )
170    }
171
172    pub fn pump_servo_event_loop(&mut self, active_event_loop: Option<&ActiveEventLoop>) -> bool {
173        let AppState::Running(state) = &self.state else {
174            return false;
175        };
176
177        let create_platform_window = |url: Url| self.create_platform_window(url, active_event_loop);
178        if !state.spin_event_loop(Some(&create_platform_window)) {
179            self.state = AppState::ShuttingDown;
180            return false;
181        }
182        true
183    }
184}
185
186impl ApplicationHandler<AppEvent> for App {
187    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
188        self.init(Some(event_loop));
189    }
190
191    fn window_event(
192        &mut self,
193        event_loop: &ActiveEventLoop,
194        window_id: WindowId,
195        window_event: WindowEvent,
196    ) {
197        let now = Instant::now();
198        trace_winit_event!(
199            window_event,
200            "@{:?} (+{:?}) {window_event:?}",
201            now - self.t_start,
202            now - self.t
203        );
204        self.t = now;
205
206        let AppState::Running(state) = &self.state else {
207            return;
208        };
209
210        if let Some(window) = state.window(ServoShellWindowId::from(u64::from(window_id))) &&
211            let Some(headed_window) = window.platform_window().as_headed_window()
212        {
213            headed_window.handle_winit_window_event(state.clone(), window, window_event);
214        }
215
216        if !self.pump_servo_event_loop(event_loop.into()) {
217            event_loop.exit();
218        }
219        // Block until the window gets an event
220        event_loop.set_control_flow(ControlFlow::Wait);
221    }
222
223    fn user_event(&mut self, event_loop: &ActiveEventLoop, app_event: AppEvent) {
224        let AppState::Running(state) = &self.state else {
225            return;
226        };
227
228        match app_event {
229            AppEvent::Waker => (),
230            AppEvent::Accessibility(ref event) => {
231                if let Some(window) =
232                    state.window(ServoShellWindowId::from(u64::from(event.window_id))) &&
233                    let Some(headed_window) = window.platform_window().as_headed_window()
234                {
235                    headed_window.handle_winit_app_event(state.clone(), app_event);
236                }
237            },
238            #[cfg(feature = "gamepad")]
239            AppEvent::Gamepad(event, gamepad_name, gamepad_index) => {
240                state.handle_gamepad_events(event, gamepad_name, gamepad_index);
241            },
242        }
243
244        if !self.pump_servo_event_loop(event_loop.into()) {
245            event_loop.exit();
246        }
247
248        // Block until the window gets an event
249        event_loop.set_control_flow(ControlFlow::Wait);
250    }
251}
252
253fn load_userscripts(userscripts_directory: Option<&Path>) -> std::io::Result<Vec<UserScript>> {
254    let mut userscripts = Vec::new();
255    if let Some(userscripts_directory) = &userscripts_directory {
256        let mut files = std::fs::read_dir(userscripts_directory)?
257            .map(|e| e.map(|entry| entry.path()))
258            .collect::<Result<Vec<_>, _>>()?;
259        files.sort_unstable();
260        for file in files {
261            let script = std::fs::read_to_string(&file)?;
262            userscripts.push(UserScript::new(script, Some(file)));
263        }
264    }
265    Ok(userscripts)
266}