1use 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 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 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 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}