Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Manages a single "OS level" window state.
4
5use std::cell::{Cell, RefCell};
6use std::collections::{BTreeMap, HashMap};
7use std::hash::Hash;
8use std::rc::Rc;
9
10use euclid::{Point2D, Rect, Scale};
11use servo::{
12 DevicePixel, DevicePoint, InputEvent, InputEventId, InputEventResult, KeyboardEvent,
13 MouseButton as ServoMouseButton, MouseButtonAction, MouseButtonEvent, MouseLeftViewportEvent,
14 MouseMoveEvent, RenderingContext, Theme, TouchEvent, TouchEventType, TouchId, WebView,
15 WebViewId, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext,
16};
17use url::Url;
18use winit::dpi::PhysicalPosition;
19use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
20use winit::keyboard::ModifiersState;
21use winit::window::Window;
22
23use crate::keyutils::keyboard_event_from_winit;
24use crate::touch_event_simulator::TouchEventSimulator;
25
26/// Commands that the shell can queue for later processing.
27#[derive(Clone, Debug)]
28pub(crate) enum UserInterfaceCommand {
29 /// Open a new browser window.
30 NewWindow(Option<Url>, Option<String>),
31}
32
33/// A unique identifier for a browser window, derived from winit's WindowId.
34#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
35pub(crate) struct BrowserWindowId(u64);
36
37impl From<winit::window::WindowId> for BrowserWindowId {
38 fn from(id: winit::window::WindowId) -> Self {
39 Self(id.into())
40 }
41}
42
43/// A keyboard event sent to system webview, pending forwarding decision.
44struct PendingKeyboardEvent {
45 /// The original keyboard event for potential forwarding.
46 keyboard_event: KeyboardEvent,
47 /// The focused webview at time of event (target for forwarding).
48 target_webview_id: Option<WebViewId>,
49}
50
51/// Per-window state. Each OS-level window has its own BrowserWindow instance.
52pub(crate) struct BrowserWindow {
53 pub(crate) window: Window,
54 pub(crate) rendering_context: Rc<WindowRenderingContext>,
55 pub(crate) webviews: RefCell<BTreeMap<WebViewId, WebView>>,
56 webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
57 modifiers_state: Cell<ModifiersState>,
58 /// The WebViewId of the currently focused webview (for keyboard input routing).
59 /// Updated via notify_focus_changed delegate callback.
60 pub(crate) focused_webview_id: Cell<Option<WebViewId>>,
61 /// The WebViewId of the system UI
62 pub(crate) system_webview_id: Cell<Option<WebViewId>>,
63 /// Commands queued for later processing.
64 pending_commands: RefCell<Vec<UserInterfaceCommand>>,
65 /// Keyboard events awaiting callback from system webview to decide on forwarding.
66 pending_keyboard_events: RefCell<HashMap<InputEventId, PendingKeyboardEvent>>,
67 /// Simulates touch events from mouse when mobile simulation is enabled.
68 touch_event_simulator: Option<TouchEventSimulator>,
69}
70
71impl BrowserWindow {
72 pub(crate) fn new(
73 window: Window,
74 rendering_context: Rc<WindowRenderingContext>,
75 simulate_touch: bool,
76 ) -> Self {
77 Self {
78 window,
79 rendering_context,
80 webviews: Default::default(),
81 webview_relative_mouse_point: Cell::new(Point2D::zero()),
82 modifiers_state: Cell::new(ModifiersState::empty()),
83 focused_webview_id: Cell::new(None),
84 system_webview_id: Cell::new(None),
85 pending_commands: Default::default(),
86 pending_keyboard_events: Default::default(),
87 touch_event_simulator: simulate_touch.then(Default::default),
88 }
89 }
90
91 pub(crate) fn id(&self) -> BrowserWindowId {
92 self.window.id().into()
93 }
94
95 /// Get the focused webview, or fall back to the first webview.
96 pub(crate) fn focused_or_first_webview(&self) -> Option<WebView> {
97 let webviews = self.webviews.borrow();
98 let result = if let Some(focused_id) = self.focused_webview_id.get() {
99 webviews.get(&focused_id)
100 } else {
101 // Fall back to first webview
102 webviews.values().next()
103 };
104 result.cloned()
105 }
106
107 pub(crate) fn first_webview(&self) -> Option<WebView> {
108 let webviews = self.webviews.borrow();
109 webviews.values().next().cloned()
110 }
111
112 /// Get the system webview (browserhtml shell UI).
113 pub(crate) fn system_webview(&self) -> Option<WebView> {
114 let system_id = self.system_webview_id.get()?;
115 self.webviews.borrow().get(&system_id).cloned()
116 }
117
118 /// Get a webview by its ID.
119 pub(crate) fn webview(&self, id: WebViewId) -> Option<WebView> {
120 self.webviews.borrow().get(&id).cloned()
121 }
122
123 /// Helper function to handle a click
124 pub(crate) fn handle_mouse_button_event(
125 &self,
126 webview: &WebView,
127 button: MouseButton,
128 action: ElementState,
129 ) {
130 // `point` can be outside viewport, such as at toolbar with negative y-coordinate.
131 let point = self.webview_relative_mouse_point.get();
132 let webview_rect: Rect<_, _> = webview.size().into();
133 if !webview_rect.contains(point) {
134 return;
135 }
136
137 // Check for touch simulation first
138 if self
139 .touch_event_simulator
140 .as_ref()
141 .is_some_and(|sim| sim.maybe_consume_mouse_button_event(webview, button, action, point))
142 {
143 return;
144 }
145
146 let mouse_button = match &button {
147 MouseButton::Left => ServoMouseButton::Left,
148 MouseButton::Right => ServoMouseButton::Right,
149 MouseButton::Middle => ServoMouseButton::Middle,
150 MouseButton::Back => ServoMouseButton::Back,
151 MouseButton::Forward => ServoMouseButton::Forward,
152 MouseButton::Other(value) => ServoMouseButton::Other(*value),
153 };
154
155 let action = match action {
156 ElementState::Pressed => MouseButtonAction::Down,
157 ElementState::Released => MouseButtonAction::Up,
158 };
159
160 webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
161 action,
162 mouse_button,
163 point.into(),
164 )));
165 }
166
167 /// Helper function to handle mouse move events.
168 pub(crate) fn handle_mouse_move_event(
169 &self,
170 webview: &WebView,
171 position: PhysicalPosition<f64>,
172 ) {
173 let point = winit_position_to_euclid_point(position).to_f32();
174
175 let previous_point = self.webview_relative_mouse_point.get();
176 self.webview_relative_mouse_point.set(point);
177
178 let webview_rect: Rect<_, _> = webview.size().into();
179 if !webview_rect.contains(point) {
180 if webview_rect.contains(previous_point) {
181 webview.notify_input_event(InputEvent::MouseLeftViewport(
182 MouseLeftViewportEvent::default(),
183 ));
184 }
185 return;
186 }
187
188 // Check for touch simulation first
189 if self
190 .touch_event_simulator
191 .as_ref()
192 .is_some_and(|sim| sim.maybe_consume_mouse_move_event(webview, point))
193 {
194 return;
195 }
196
197 webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point.into())));
198 }
199
200 /// Queue a command for later processing.
201 pub(crate) fn queue_command(&self, command: UserInterfaceCommand) {
202 self.pending_commands.borrow_mut().push(command);
203 }
204
205 /// Take all pending commands.
206 pub(crate) fn take_pending_commands(&self) -> Vec<UserInterfaceCommand> {
207 self.pending_commands.borrow_mut().drain(..).collect()
208 }
209
210 /// Handle a keyboard event result callback. If the event was sent to the system
211 /// webview for interception, this will forward it to the target webview if not prevented.
212 pub(crate) fn handle_keyboard_event_result(
213 &self,
214 event_id: InputEventId,
215 result: InputEventResult,
216 ) {
217 let pending = self.pending_keyboard_events.borrow_mut().remove(&event_id);
218 let Some(pending_event) = pending else {
219 return;
220 };
221
222 // If system webview prevented or consumed the event, don't forward
223 if result.intersects(InputEventResult::DefaultPrevented | InputEventResult::Consumed) {
224 return;
225 }
226
227 // Forward to target webview if it exists and differs from system
228 let Some(target_id) = pending_event.target_webview_id else {
229 return;
230 };
231
232 if Some(target_id) == self.system_webview_id.get() {
233 return;
234 }
235
236 if let Some(target_webview) = self.webview(target_id) {
237 target_webview.notify_input_event(InputEvent::Keyboard(pending_event.keyboard_event));
238 }
239 }
240
241 pub(crate) fn contains_webview(&self, id: WebViewId) -> bool {
242 self.webviews.borrow().contains_key(&id)
243 }
244
245 pub(crate) fn window_event(&self, event: WindowEvent) {
246 match event {
247 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
248 let scale = Scale::new(scale_factor as _);
249 for webview in self.webviews.borrow().values() {
250 webview.set_hidpi_scale_factor(scale);
251 }
252 },
253 WindowEvent::RedrawRequested => {
254 if let Some(webview) = self.first_webview() {
255 webview.paint();
256 self.rendering_context.present();
257 }
258 },
259 WindowEvent::MouseInput { state, button, .. } => {
260 // Send mouse events to the system webview (browserhtml shell) first.
261 // The shell's DOM hit testing will determine if events should be
262 // forwarded to embedded webviews.
263 if let Some(webview) = self.system_webview().or_else(|| self.first_webview()) {
264 self.handle_mouse_button_event(&webview, button, state);
265 }
266 },
267 WindowEvent::CursorMoved { position, .. } => {
268 // Send mouse events to the system webview (browserhtml shell) first.
269 // The shell's DOM hit testing will determine if events should be
270 // forwarded to embedded webviews.
271 if let Some(webview) = self.system_webview().or_else(|| self.first_webview()) {
272 self.handle_mouse_move_event(&webview, position);
273 }
274 },
275 WindowEvent::CursorLeft { .. } => {
276 if let Some(webview) = self.system_webview().or_else(|| self.first_webview()) {
277 let webview_rect: Rect<_, _> = webview.size().into();
278 if webview_rect.contains(self.webview_relative_mouse_point.get()) {
279 webview.notify_input_event(InputEvent::MouseLeftViewport(
280 MouseLeftViewportEvent::default(),
281 ));
282 }
283 }
284 },
285 WindowEvent::MouseWheel { delta, .. } => {
286 // Send wheel events to the system webview (browserhtml shell) first.
287 // The shell's DOM hit testing will determine if events should be
288 // forwarded to embedded webviews.
289 if let Some(webview) = self.system_webview().or_else(|| self.first_webview()) {
290 let (delta_x, delta_y, mode) = match delta {
291 MouseScrollDelta::LineDelta(dx, dy) => (
292 (dx * 76.0) as f64,
293 (dy * 76.0) as f64,
294 WheelMode::DeltaPixel,
295 ),
296 MouseScrollDelta::PixelDelta(delta) => {
297 (delta.x * 6.0, delta.y * 6.0, WheelMode::DeltaPixel)
298 },
299 };
300
301 let point = self.webview_relative_mouse_point.get();
302 webview.notify_input_event(InputEvent::Wheel(WheelEvent::new(
303 WheelDelta {
304 x: delta_x,
305 y: delta_y,
306 z: 0.0,
307 mode,
308 },
309 point.into(),
310 )));
311 }
312 },
313 WindowEvent::Touch(touch) => {
314 // Send touch events to the system webview (browserhtml shell) first.
315 // The shell's DOM hit testing will determine if events should be
316 // forwarded to embedded webviews.
317 if let Some(webview) = self.system_webview().or_else(|| self.first_webview()) {
318 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
319 winit_phase_to_touch_event_type(touch.phase),
320 TouchId(touch.id as i32),
321 DevicePoint::new(touch.location.x as f32, touch.location.y as f32).into(),
322 )));
323 }
324 },
325 WindowEvent::PinchGesture { delta, .. } => {
326 if let Some(webview) = self.first_webview() {
327 webview.pinch_zoom(delta as f32 + 1.0, self.webview_relative_mouse_point.get());
328 }
329 },
330 WindowEvent::ThemeChanged(theme) => {
331 for webview in self.webviews.borrow().values() {
332 webview.notify_theme_change(match theme {
333 winit::window::Theme::Light => Theme::Light,
334 winit::window::Theme::Dark => Theme::Dark,
335 });
336 }
337 },
338 WindowEvent::Resized(new_size) => {
339 if let Some(webview) = self.first_webview() {
340 webview.resize(new_size);
341 }
342 },
343 WindowEvent::ModifiersChanged(modifiers) => {
344 self.modifiers_state.set(modifiers.state());
345 },
346 WindowEvent::KeyboardInput { event, .. } => {
347 let keyboard_event = keyboard_event_from_winit(&event, self.modifiers_state.get());
348
349 let focused_webview_id = self.focused_webview_id.get();
350 let system_webview_id = self.system_webview_id.get();
351
352 // Check if we need parent-first forwarding flow
353 let needs_forwarding = match (focused_webview_id, system_webview_id) {
354 (Some(focused), Some(system)) => focused != system,
355 _ => false,
356 };
357
358 if needs_forwarding {
359 // Send to system webview first, store for potential forwarding
360 if let Some(system_webview) = self.system_webview() {
361 let event_id = system_webview
362 .notify_input_event(InputEvent::Keyboard(keyboard_event.clone()));
363
364 self.pending_keyboard_events.borrow_mut().insert(
365 event_id,
366 PendingKeyboardEvent {
367 keyboard_event,
368 target_webview_id: focused_webview_id,
369 },
370 );
371 }
372 } else {
373 // No forwarding needed - send directly to focused/first webview
374 if let Some(webview) = self.focused_or_first_webview() {
375 webview.notify_input_event(InputEvent::Keyboard(keyboard_event));
376 }
377 }
378 },
379 _ => (),
380 }
381 }
382}
383
384fn winit_position_to_euclid_point<T>(position: PhysicalPosition<T>) -> Point2D<T, DevicePixel> {
385 Point2D::new(position.x, position.y)
386}
387
388fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
389 match phase {
390 TouchPhase::Started => TouchEventType::Down,
391 TouchPhase::Moved => TouchEventType::Move,
392 TouchPhase::Ended => TouchEventType::Up,
393 TouchPhase::Cancelled => TouchEventType::Cancel,
394 }
395}