1use std::{
2 collections::{HashMap, HashSet},
3 fmt,
4 sync::Arc,
5};
6
7use cbf::{
8 command::BrowserCommand,
9 data::{
10 background::BackgroundPolicy as GenericBackgroundPolicy, context_menu::ContextMenu,
11 drag::DragStartRequest, ids::BrowsingContextId, ime::ImeBoundsUpdate,
12 },
13 event::{BrowserEvent, BrowsingContextEvent, TransientBrowsingContextEvent},
14};
15#[cfg(feature = "chrome")]
16use cbf_chrome::{data::choice_menu::ChromeChoiceMenu, event::ChromeEvent};
17
18use crate::{
19 BackendCommand,
20 core::CompositionCommand,
21 error::CompositorError,
22 model::{
23 BackgroundPolicy, CompositionItemId, CompositionItemSpec, CompositorWindowId, SurfaceTarget,
24 },
25 platform::host::{
26 PlatformSceneItem, PlatformSurfaceHandle, PlatformWindowHost, attach_window_host,
27 },
28 state::{
29 composition_state::CompositionState, focus_state::FocusState,
30 ownership_state::OwnershipState, surface_state::SurfaceState,
31 },
32 window::WindowHost,
33};
34
35pub struct Compositor {
38 next_window_id: u64,
39 windows: HashMap<CompositorWindowId, AttachedWindow>,
40 ownership_state: OwnershipState,
41 composition_state: CompositionState,
42 focus_state: FocusState,
43 surface_state: SurfaceState,
44}
45
46pub type EventRouter = Arc<dyn Fn(&RoutedEventContext) -> EventRoutingDecision + Send + Sync>;
48
49#[derive(Clone, Default)]
51pub struct AttachWindowOptions {
52 pub event_router: Option<EventRouter>,
53}
54
55impl fmt::Debug for AttachWindowOptions {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.debug_struct("AttachWindowOptions")
58 .field(
59 "event_router",
60 &self.event_router.as_ref().map(|_| "<callback>"),
61 )
62 .finish()
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum EventRoutingDecision {
69 Dispatch,
70 Consume,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum RoutedEventKind {
76 PointerDown,
77 PointerUp,
78 Wheel,
79 KeyDown,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct RoutedEventContext {
85 pub window_id: CompositorWindowId,
86 pub kind: RoutedEventKind,
87 pub target: Option<SurfaceTarget>,
88 pub active_target: Option<SurfaceTarget>,
89}
90
91struct AttachedWindow {
92 _host: Box<dyn WindowHost>,
93 _options: AttachWindowOptions,
94 platform_host: Box<dyn PlatformWindowHost>,
95}
96
97impl Default for Compositor {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl Compositor {
104 pub fn new() -> Self {
106 Self {
107 next_window_id: 1,
108 windows: HashMap::new(),
109 ownership_state: OwnershipState::default(),
110 composition_state: CompositionState::default(),
111 focus_state: FocusState::default(),
112 surface_state: SurfaceState::default(),
113 }
114 }
115
116 pub fn attach_window<W, E>(
118 &mut self,
119 window: W,
120 options: AttachWindowOptions,
121 emit: E,
122 ) -> Result<CompositorWindowId, CompositorError>
123 where
124 W: WindowHost + 'static,
125 E: FnMut(BackendCommand) + 'static,
126 {
127 let window_id = CompositorWindowId::new(self.next_window_id);
128 self.next_window_id = self.next_window_id.saturating_add(1);
129 let platform_host = attach_window_host(&window, window_id, options.clone(), emit)?;
130
131 self.composition_state.ensure_window(window_id);
132 self.windows.insert(
133 window_id,
134 AttachedWindow {
135 _host: Box::new(window),
136 _options: options,
137 platform_host,
138 },
139 );
140
141 Ok(window_id)
142 }
143
144 pub fn detach_window(
146 &mut self,
147 window_id: CompositorWindowId,
148 mut emit: impl FnMut(BackendCommand),
149 ) -> Result<(), CompositorError> {
150 _ = &mut emit;
151 if self.windows.remove(&window_id).is_none() {
152 return Err(CompositorError::UnknownWindow);
153 }
154
155 let removed_item_ids = self.composition_state.remove_window(window_id);
156 self.focus_state.clear_removed_items(&removed_item_ids);
157
158 Ok(())
159 }
160
161 pub fn apply(
163 &mut self,
164 command: CompositionCommand,
165 mut emit: impl FnMut(BackendCommand),
166 ) -> Result<(), CompositorError> {
167 match command {
168 CompositionCommand::SetWindowComposition {
169 window_id,
170 composition,
171 } => {
172 self.ensure_window(window_id)?;
173 let previous_items = self
174 .composition_state
175 .items_for_window(window_id)
176 .unwrap_or_default();
177 let next_items = composition.items.clone();
178 let removed = self
179 .composition_state
180 .set_window_composition(window_id, composition)?;
181 self.focus_state.clear_removed_items(&removed);
182 self.emit_background_policy_updates(&previous_items, &next_items, &mut emit);
183 self.sync_window_scene(window_id)
184 }
185 CompositionCommand::UpdateItemBounds {
186 window_id,
187 item_id,
188 bounds,
189 } => {
190 self.ensure_window(window_id)?;
191 self.composition_state
192 .update_item_bounds(window_id, item_id, bounds)?;
193 self.sync_window_scene(window_id)
194 }
195 CompositionCommand::SetItemVisibility {
196 window_id,
197 item_id,
198 visible,
199 } => {
200 self.ensure_window(window_id)?;
201 self.composition_state
202 .set_item_visibility(window_id, item_id, visible)?;
203 self.sync_window_scene(window_id)
204 }
205 CompositionCommand::SetItemHitTestRegions {
206 window_id,
207 item_id,
208 snapshot_id,
209 coordinate_space,
210 mode,
211 regions,
212 } => {
213 self.ensure_window(window_id)?;
214 if self.composition_state.set_item_hit_test_regions(
215 window_id,
216 item_id,
217 snapshot_id,
218 coordinate_space,
219 mode,
220 regions,
221 )? {
222 self.sync_window_scene(window_id)
223 } else {
224 Ok(())
225 }
226 }
227 CompositionCommand::RemoveItem { window_id, item_id } => {
228 self.ensure_window(window_id)?;
229 self.composition_state.remove_item(window_id, item_id)?;
230 self.focus_state.clear_removed_items(&[item_id]);
231 self.sync_window_scene(window_id)
232 }
233 }
234 }
235
236 pub fn update_browser_event(
238 &mut self,
239 event: &BrowserEvent,
240 mut emit: impl FnMut(BrowserCommand),
241 ) -> Result<(), CompositorError> {
242 _ = &mut emit;
243
244 match event {
245 BrowserEvent::BrowsingContext {
246 browsing_context_id,
247 event,
248 ..
249 } => match event.as_ref() {
250 BrowsingContextEvent::Closed => {
251 self.remove_target_and_owned_transients(
252 SurfaceTarget::BrowsingContext(*browsing_context_id),
253 *browsing_context_id,
254 )?;
255 }
256 BrowsingContextEvent::RenderProcessGone { .. } => {
257 self.remove_owned_transients(*browsing_context_id)?;
258 }
259 BrowsingContextEvent::ImeBoundsUpdated { update } => {
260 self.set_ime_bounds_for_target(
261 SurfaceTarget::BrowsingContext(*browsing_context_id),
262 update.clone(),
263 )?;
264 }
265 BrowsingContextEvent::ExternalDragOperationChanged { operation } => {
266 let target = SurfaceTarget::BrowsingContext(*browsing_context_id);
267 if let Some(window_id) = self.window_id_for_target(target)
268 && let Some(window) = self.windows.get_mut(&window_id)
269 {
270 window
271 .platform_host
272 .set_external_drag_operation(target, *operation)?;
273 }
274 }
275 _ => {}
276 },
277 BrowserEvent::TransientBrowsingContext {
278 transient_browsing_context_id,
279 parent_browsing_context_id,
280 event,
281 ..
282 } => match event.as_ref() {
283 TransientBrowsingContextEvent::Opened { kind, .. } => {
284 self.ownership_state.upsert(
285 *transient_browsing_context_id,
286 *parent_browsing_context_id,
287 *kind,
288 );
289 }
290 TransientBrowsingContextEvent::Resized { width, height } => {
291 self.set_transient_preferred_size(
292 *transient_browsing_context_id,
293 (*width, *height),
294 );
295 }
296 TransientBrowsingContextEvent::ImeBoundsUpdated { update } => {
297 self.set_ime_bounds_for_target(
298 SurfaceTarget::TransientBrowsingContext(*transient_browsing_context_id),
299 update.clone(),
300 )?;
301 }
302 TransientBrowsingContextEvent::Closed { .. }
303 | TransientBrowsingContextEvent::RenderProcessGone { .. } => {
304 self.remove_transient(*transient_browsing_context_id)?;
305 }
306 _ => {}
307 },
308 _ => {}
309 }
310
311 Ok(())
312 }
313
314 #[cfg(feature = "chrome")]
315 pub fn update_chrome_event(&mut self, event: &ChromeEvent) -> Result<(), CompositorError> {
317 crate::backend::chrome::apply_chrome_event(self, event)
318 }
319
320 #[cfg(not(feature = "chrome"))]
321 pub fn update_chrome_event(&mut self, _event: &()) -> Result<(), CompositorError> {
323 Ok(())
324 }
325
326 pub fn surface_target_for_item(&self, item_id: CompositionItemId) -> Option<SurfaceTarget> {
328 self.composition_state.surface_target_for_item(item_id)
329 }
330
331 pub fn item_ids_for_target(&self, target: SurfaceTarget) -> Vec<CompositionItemId> {
333 self.composition_state.item_ids_for_target(target)
334 }
335
336 pub fn window_id_for_item(&self, item_id: CompositionItemId) -> Option<CompositorWindowId> {
338 self.composition_state.window_id_for_item(item_id)
339 }
340
341 pub fn set_active_item(&mut self, item_id: CompositionItemId) -> Result<(), CompositorError> {
343 let spec = self
344 .composition_state
345 .item_spec(item_id)
346 .ok_or(CompositorError::UnknownItem)?;
347 if !spec.visible || matches!(spec.hit_test, crate::model::HitTestPolicy::Passthrough) {
348 return Err(CompositorError::ItemNotInteractive);
349 }
350
351 let window_id = self
352 .composition_state
353 .window_id_for_item(item_id)
354 .ok_or(CompositorError::UnknownItem)?;
355 self.focus_state.active_item_id = Some(item_id);
356 self.windows
357 .get_mut(&window_id)
358 .ok_or(CompositorError::UnknownWindow)?
359 .platform_host
360 .set_active_item(Some(item_id))
361 }
362
363 pub fn transient_preferred_size(
365 &self,
366 transient_browsing_context_id: cbf::data::ids::TransientBrowsingContextId,
367 ) -> Option<(u32, u32)> {
368 self.surface_state
369 .get(SurfaceTarget::TransientBrowsingContext(
370 transient_browsing_context_id,
371 ))
372 .and_then(|state| state.transient_preferred_size)
373 }
374
375 pub fn show_context_menu(
377 &mut self,
378 target: SurfaceTarget,
379 menu: ContextMenu,
380 ) -> Result<(), CompositorError> {
381 let window_id = self
382 .window_id_for_target(target)
383 .ok_or(CompositorError::UnknownTarget)?;
384 let window = self
385 .windows
386 .get_mut(&window_id)
387 .ok_or(CompositorError::UnknownWindow)?;
388 window.platform_host.show_context_menu(target, menu)
389 }
390
391 #[cfg(feature = "chrome")]
392 pub fn show_choice_menu(
394 &mut self,
395 target: SurfaceTarget,
396 menu: ChromeChoiceMenu,
397 ) -> Result<(), CompositorError> {
398 let window_id = self
399 .window_id_for_target(target)
400 .ok_or(CompositorError::UnknownTarget)?;
401 let window = self
402 .windows
403 .get_mut(&window_id)
404 .ok_or(CompositorError::UnknownWindow)?;
405 window.platform_host.show_choice_menu(target, menu)
406 }
407
408 pub fn start_native_drag(
410 &mut self,
411 request: DragStartRequest,
412 ) -> Result<bool, CompositorError> {
413 let target = SurfaceTarget::BrowsingContext(request.browsing_context_id);
414 let window_id = self
415 .window_id_for_target(target)
416 .ok_or(CompositorError::UnknownTarget)?;
417 let window = self
418 .windows
419 .get_mut(&window_id)
420 .ok_or(CompositorError::UnknownWindow)?;
421 window.platform_host.start_native_drag(target, request)
422 }
423
424 pub(crate) fn set_surface_handle_for_target(
425 &mut self,
426 target: SurfaceTarget,
427 handle: PlatformSurfaceHandle,
428 ) -> Result<(), CompositorError> {
429 self.surface_state.set_surface(target, handle);
430 for window_id in self.composition_state.window_ids_for_target(target) {
431 self.sync_window_scene(window_id)?;
432 }
433 Ok(())
434 }
435
436 pub(crate) fn set_transient_preferred_size(
437 &mut self,
438 transient_browsing_context_id: cbf::data::ids::TransientBrowsingContextId,
439 size: (u32, u32),
440 ) {
441 self.surface_state.set_transient_preferred_size(
442 SurfaceTarget::TransientBrowsingContext(transient_browsing_context_id),
443 size,
444 );
445 }
446
447 fn emit_background_policy_updates(
448 &self,
449 previous_items: &[CompositionItemSpec],
450 next_items: &[CompositionItemSpec],
451 emit: &mut impl FnMut(BackendCommand),
452 ) {
453 let previous = previous_items
454 .iter()
455 .map(|item| (item.target, item.background))
456 .collect::<HashMap<_, _>>();
457 let next = next_items
458 .iter()
459 .map(|item| (item.target, item.background))
460 .collect::<HashMap<_, _>>();
461
462 let mut targets = previous.keys().copied().collect::<HashSet<_>>();
463 targets.extend(next.keys().copied());
464
465 for target in targets {
466 let Some(next_policy) = next.get(&target).copied() else {
467 continue;
468 };
469 if previous.get(&target).copied() == Some(next_policy) {
470 continue;
471 }
472 self.emit_background_policy_command(target, next_policy, emit);
473 }
474 }
475
476 fn emit_background_policy_command(
477 &self,
478 target: SurfaceTarget,
479 policy: BackgroundPolicy,
480 emit: &mut impl FnMut(BackendCommand),
481 ) {
482 let policy: GenericBackgroundPolicy = policy.into();
483 match target {
484 SurfaceTarget::BrowsingContext(browsing_context_id) => {
485 emit(BackendCommand::Browser(
486 BrowserCommand::SetBrowsingContextBackgroundPolicy {
487 browsing_context_id,
488 policy,
489 },
490 ));
491 }
492 SurfaceTarget::TransientBrowsingContext(transient_browsing_context_id) => {
493 emit(BackendCommand::Browser(
494 BrowserCommand::SetTransientBrowsingContextBackgroundPolicy {
495 transient_browsing_context_id,
496 policy,
497 },
498 ));
499 }
500 }
501 }
502
503 fn ensure_window(&self, window_id: CompositorWindowId) -> Result<(), CompositorError> {
504 if self.windows.contains_key(&window_id) {
505 Ok(())
506 } else {
507 Err(CompositorError::UnknownWindow)
508 }
509 }
510
511 fn window_id_for_target(&self, target: SurfaceTarget) -> Option<CompositorWindowId> {
512 self.composition_state
513 .window_ids_for_target(target)
514 .into_iter()
515 .next()
516 }
517
518 fn sync_window_scene(&mut self, window_id: CompositorWindowId) -> Result<(), CompositorError> {
519 let scene = self
520 .composition_state
521 .window_scene_items(window_id)
522 .ok_or(CompositorError::UnknownWindow)?
523 .into_iter()
524 .map(|item| {
525 let runtime_state = self.surface_state.get(item.spec.target);
526 PlatformSceneItem {
527 item_id: item.spec.item_id,
528 target: item.spec.target,
529 bounds: item.spec.bounds,
530 visible: item.spec.visible,
531 hit_test: item.spec.hit_test,
532 hit_test_snapshot: item.hit_test_snapshot,
533 surface: runtime_state.and_then(|state| state.surface.clone()),
534 ime_bounds: runtime_state.and_then(|state| state.ime_bounds.clone()),
535 }
536 })
537 .collect::<Vec<_>>();
538
539 self.windows
540 .get_mut(&window_id)
541 .ok_or(CompositorError::UnknownWindow)?
542 .platform_host
543 .sync_scene(&scene)
544 }
545
546 fn remove_target_and_owned_transients(
547 &mut self,
548 target: SurfaceTarget,
549 parent_browsing_context_id: BrowsingContextId,
550 ) -> Result<(), CompositorError> {
551 let removed = self.composition_state.remove_target(target);
552 self.focus_state
553 .clear_removed_items(&removed.removed_item_ids);
554 self.surface_state.remove(&target);
555
556 for transient_id in self
557 .ownership_state
558 .remove_by_parent(parent_browsing_context_id)
559 {
560 let transient_target = SurfaceTarget::TransientBrowsingContext(transient_id);
561 let removed = self.composition_state.remove_target(transient_target);
562 self.focus_state
563 .clear_removed_items(&removed.removed_item_ids);
564 self.surface_state.remove(&transient_target);
565
566 for window_id in removed.affected_windows {
567 self.sync_window_scene(window_id)?;
568 }
569 }
570
571 for window_id in removed.affected_windows {
572 self.sync_window_scene(window_id)?;
573 }
574
575 Ok(())
576 }
577
578 fn remove_owned_transients(
579 &mut self,
580 parent_browsing_context_id: BrowsingContextId,
581 ) -> Result<(), CompositorError> {
582 for transient_id in self
583 .ownership_state
584 .remove_by_parent(parent_browsing_context_id)
585 {
586 let transient_target = SurfaceTarget::TransientBrowsingContext(transient_id);
587 let removed = self.composition_state.remove_target(transient_target);
588 self.focus_state
589 .clear_removed_items(&removed.removed_item_ids);
590 self.surface_state.remove(&transient_target);
591
592 for window_id in removed.affected_windows {
593 self.sync_window_scene(window_id)?;
594 }
595 }
596 Ok(())
597 }
598
599 fn remove_transient(
600 &mut self,
601 transient_browsing_context_id: cbf::data::ids::TransientBrowsingContextId,
602 ) -> Result<(), CompositorError> {
603 self.ownership_state.remove(transient_browsing_context_id);
604 let target = SurfaceTarget::TransientBrowsingContext(transient_browsing_context_id);
605 let removed = self.composition_state.remove_target(target);
606 self.focus_state
607 .clear_removed_items(&removed.removed_item_ids);
608 self.surface_state.remove(&target);
609 for window_id in removed.affected_windows {
610 self.sync_window_scene(window_id)?;
611 }
612 Ok(())
613 }
614
615 fn set_ime_bounds_for_target(
616 &mut self,
617 target: SurfaceTarget,
618 update: ImeBoundsUpdate,
619 ) -> Result<(), CompositorError> {
620 self.surface_state.set_ime_bounds(target, update);
621 for window_id in self.composition_state.window_ids_for_target(target) {
622 self.sync_window_scene(window_id)?;
623 }
624 Ok(())
625 }
626
627 #[cfg(test)]
628 pub(crate) fn attach_test_window(
629 &mut self,
630 window_id: CompositorWindowId,
631 platform_host: Box<dyn PlatformWindowHost>,
632 ) {
633 self.composition_state.ensure_window(window_id);
634 self.windows.insert(
635 window_id,
636 AttachedWindow {
637 _host: Box::new(crate::core::compositor::tests::TestWindowHost),
638 _options: AttachWindowOptions::default(),
639 platform_host,
640 },
641 );
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use std::{cell::RefCell, rc::Rc};
648
649 use cbf::data::background::BackgroundPolicy as GenericBackgroundPolicy;
650 use raw_window_handle::{
651 AppKitDisplayHandle, AppKitWindowHandle, DisplayHandle, HandleError, HasDisplayHandle,
652 HasWindowHandle, WindowHandle,
653 };
654
655 use super::*;
656 use crate::{
657 model::{
658 BackgroundPolicy, CompositionItemId, CompositionItemSpec, HitTestCoordinateSpace,
659 HitTestPolicy, HitTestRegion, HitTestRegionMode, Rect, WindowCompositionSpec,
660 },
661 platform::host::{PlatformInputState, PlatformSceneItem},
662 };
663
664 #[derive(Default)]
665 pub(crate) struct TestWindowHost;
666
667 impl HasWindowHandle for TestWindowHost {
668 fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
669 let raw = AppKitWindowHandle::new(core::ptr::NonNull::dangling());
670 Ok(unsafe { WindowHandle::borrow_raw(raw.into()) })
671 }
672 }
673
674 impl HasDisplayHandle for TestWindowHost {
675 fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
676 Ok(unsafe { DisplayHandle::borrow_raw(AppKitDisplayHandle::new().into()) })
677 }
678 }
679
680 impl WindowHost for TestWindowHost {
681 fn inner_size(&self) -> (u32, u32) {
682 (800, 600)
683 }
684 }
685
686 struct TestPlatformHost {
687 last_scene: Rc<RefCell<Vec<PlatformSceneItem>>>,
688 last_active_item: Rc<RefCell<Option<CompositionItemId>>>,
689 }
690
691 impl Default for TestPlatformHost {
692 fn default() -> Self {
693 Self {
694 last_scene: Rc::new(RefCell::new(Vec::new())),
695 last_active_item: Rc::new(RefCell::new(None)),
696 }
697 }
698 }
699
700 impl PlatformWindowHost for TestPlatformHost {
701 fn sync_scene(&mut self, items: &[PlatformSceneItem]) -> Result<(), CompositorError> {
702 self.last_scene.replace(items.to_vec());
703 Ok(())
704 }
705
706 fn set_active_item(
707 &mut self,
708 item_id: Option<CompositionItemId>,
709 ) -> Result<(), CompositorError> {
710 self.last_active_item.replace(item_id);
711 Ok(())
712 }
713
714 fn show_context_menu(
715 &mut self,
716 _target: SurfaceTarget,
717 _menu: cbf::data::context_menu::ContextMenu,
718 ) -> Result<(), CompositorError> {
719 Ok(())
720 }
721
722 #[cfg(feature = "chrome")]
723 fn show_choice_menu(
724 &mut self,
725 _target: SurfaceTarget,
726 _menu: cbf_chrome::data::choice_menu::ChromeChoiceMenu,
727 ) -> Result<(), CompositorError> {
728 Ok(())
729 }
730
731 fn start_native_drag(
732 &mut self,
733 _target: SurfaceTarget,
734 _request: cbf::data::drag::DragStartRequest,
735 ) -> Result<bool, CompositorError> {
736 Ok(false)
737 }
738
739 fn input_state(&self) -> PlatformInputState {
740 PlatformInputState::default()
741 }
742 }
743
744 fn item(item_id: u64, target: SurfaceTarget) -> CompositionItemSpec {
745 CompositionItemSpec {
746 item_id: CompositionItemId::new(item_id),
747 target,
748 bounds: Rect::new(0.0, 0.0, 100.0, 100.0),
749 visible: true,
750 hit_test: HitTestPolicy::Bounds,
751 background: BackgroundPolicy::Opaque,
752 }
753 }
754
755 fn region_item(item_id: u64, target: SurfaceTarget) -> CompositionItemSpec {
756 CompositionItemSpec {
757 hit_test: HitTestPolicy::RegionSnapshot,
758 ..item(item_id, target)
759 }
760 }
761
762 fn transparent_item(item_id: u64, target: SurfaceTarget) -> CompositionItemSpec {
763 CompositionItemSpec {
764 background: BackgroundPolicy::Transparent,
765 ..item(item_id, target)
766 }
767 }
768
769 #[test]
770 fn attach_window_options_defaults_to_no_event_router() {
771 let options = AttachWindowOptions::default();
772
773 assert!(options.event_router.is_none());
774 }
775
776 #[test]
777 fn routed_event_context_can_represent_transient_target() {
778 let transient_target = SurfaceTarget::TransientBrowsingContext(
779 cbf::data::ids::TransientBrowsingContextId::new(7),
780 );
781 let context = RoutedEventContext {
782 window_id: CompositorWindowId::new(3),
783 kind: RoutedEventKind::PointerDown,
784 target: Some(transient_target),
785 active_target: Some(transient_target),
786 };
787
788 assert_eq!(context.target, Some(transient_target));
789 assert_eq!(context.active_target, Some(transient_target));
790 }
791
792 #[test]
793 fn parent_close_removes_transient_items_across_windows() {
794 let mut compositor = Compositor::new();
795 let first_window = CompositorWindowId::new(1);
796 let second_window = CompositorWindowId::new(2);
797 compositor.attach_test_window(first_window, Box::<TestPlatformHost>::default());
798 compositor.attach_test_window(second_window, Box::<TestPlatformHost>::default());
799
800 let parent_id = BrowsingContextId::new(10);
801 let transient_id = cbf::data::ids::TransientBrowsingContextId::new(20);
802 compositor.ownership_state.upsert(
803 transient_id,
804 parent_id,
805 cbf::data::transient_browsing_context::TransientBrowsingContextKind::Popup,
806 );
807 compositor
808 .composition_state
809 .set_window_composition(
810 first_window,
811 WindowCompositionSpec {
812 items: vec![item(1, SurfaceTarget::BrowsingContext(parent_id))],
813 },
814 )
815 .unwrap();
816 compositor
817 .composition_state
818 .set_window_composition(
819 second_window,
820 WindowCompositionSpec {
821 items: vec![item(
822 2,
823 SurfaceTarget::TransientBrowsingContext(transient_id),
824 )],
825 },
826 )
827 .unwrap();
828
829 compositor
830 .update_browser_event(
831 &BrowserEvent::BrowsingContext {
832 profile_id: "p".into(),
833 browsing_context_id: parent_id,
834 event: Box::new(BrowsingContextEvent::Closed),
835 },
836 |_| {},
837 )
838 .unwrap();
839
840 assert!(
841 compositor
842 .item_ids_for_target(SurfaceTarget::BrowsingContext(parent_id))
843 .is_empty()
844 );
845 assert!(
846 compositor
847 .item_ids_for_target(SurfaceTarget::TransientBrowsingContext(transient_id))
848 .is_empty()
849 );
850 assert!(compositor.ownership_state.get(transient_id).is_none());
851 }
852
853 #[test]
854 fn sync_window_scene_preserves_front_to_back_item_order() {
855 let mut compositor = Compositor::new();
856 let window_id = CompositorWindowId::new(1);
857 let host = TestPlatformHost::default();
858 let scene_log = Rc::clone(&host.last_scene);
859 compositor.attach_test_window(window_id, Box::new(host));
860
861 compositor
862 .composition_state
863 .set_window_composition(
864 window_id,
865 WindowCompositionSpec {
866 items: vec![
867 item(
868 3,
869 SurfaceTarget::BrowsingContext(BrowsingContextId::new(30)),
870 ),
871 item(
872 1,
873 SurfaceTarget::BrowsingContext(BrowsingContextId::new(10)),
874 ),
875 item(
876 2,
877 SurfaceTarget::BrowsingContext(BrowsingContextId::new(20)),
878 ),
879 ],
880 },
881 )
882 .unwrap();
883
884 compositor.sync_window_scene(window_id).unwrap();
885
886 let scene = scene_log.borrow();
887 let ordered_ids = scene.iter().map(|item| item.item_id).collect::<Vec<_>>();
888 assert_eq!(
889 ordered_ids,
890 vec![
891 CompositionItemId::new(3),
892 CompositionItemId::new(1),
893 CompositionItemId::new(2),
894 ]
895 );
896 }
897
898 #[test]
899 fn set_window_composition_rejects_duplicate_target_across_windows() {
900 let mut compositor = Compositor::new();
901 let first_window = CompositorWindowId::new(1);
902 let second_window = CompositorWindowId::new(2);
903 let target = SurfaceTarget::BrowsingContext(BrowsingContextId::new(10));
904 compositor.attach_test_window(first_window, Box::<TestPlatformHost>::default());
905 compositor.attach_test_window(second_window, Box::<TestPlatformHost>::default());
906
907 compositor
908 .apply(
909 CompositionCommand::SetWindowComposition {
910 window_id: first_window,
911 composition: WindowCompositionSpec {
912 items: vec![item(1, target)],
913 },
914 },
915 |_| {},
916 )
917 .unwrap();
918
919 let error = compositor
920 .apply(
921 CompositionCommand::SetWindowComposition {
922 window_id: second_window,
923 composition: WindowCompositionSpec {
924 items: vec![item(2, target)],
925 },
926 },
927 |_| {},
928 )
929 .unwrap_err();
930
931 assert!(matches!(error, CompositorError::DuplicateSurfaceTarget));
932 }
933
934 #[test]
935 fn set_window_composition_emits_background_policy_commands_only_for_changes() {
936 let mut compositor = Compositor::new();
937 let window_id = CompositorWindowId::new(1);
938 let target = SurfaceTarget::BrowsingContext(BrowsingContextId::new(10));
939 compositor.attach_test_window(window_id, Box::<TestPlatformHost>::default());
940
941 let emitted = Rc::new(RefCell::new(Vec::new()));
942 compositor
943 .apply(
944 CompositionCommand::SetWindowComposition {
945 window_id,
946 composition: WindowCompositionSpec {
947 items: vec![transparent_item(1, target)],
948 },
949 },
950 {
951 let emitted = Rc::clone(&emitted);
952 move |command| emitted.borrow_mut().push(command)
953 },
954 )
955 .unwrap();
956
957 compositor
958 .apply(
959 CompositionCommand::SetWindowComposition {
960 window_id,
961 composition: WindowCompositionSpec {
962 items: vec![transparent_item(1, target)],
963 },
964 },
965 {
966 let emitted = Rc::clone(&emitted);
967 move |command| emitted.borrow_mut().push(command)
968 },
969 )
970 .unwrap();
971
972 compositor
973 .apply(
974 CompositionCommand::SetWindowComposition {
975 window_id,
976 composition: WindowCompositionSpec {
977 items: vec![item(1, target)],
978 },
979 },
980 {
981 let emitted = Rc::clone(&emitted);
982 move |command| emitted.borrow_mut().push(command)
983 },
984 )
985 .unwrap();
986
987 let emitted = emitted.take();
988 assert_eq!(emitted.len(), 2);
989 assert!(matches!(
990 emitted.first(),
991 Some(BrowserCommand::SetBrowsingContextBackgroundPolicy {
992 browsing_context_id,
993 policy: GenericBackgroundPolicy::Transparent,
994 }) if *browsing_context_id == BrowsingContextId::new(10)
995 ));
996 assert!(matches!(
997 emitted.get(1),
998 Some(BrowserCommand::SetBrowsingContextBackgroundPolicy {
999 browsing_context_id,
1000 policy: GenericBackgroundPolicy::Opaque,
1001 }) if *browsing_context_id == BrowsingContextId::new(10)
1002 ));
1003 }
1004
1005 #[test]
1006 fn set_window_composition_emits_transient_background_policy_command() {
1007 let mut compositor = Compositor::new();
1008 let window_id = CompositorWindowId::new(1);
1009 let target = SurfaceTarget::TransientBrowsingContext(
1010 cbf::data::ids::TransientBrowsingContextId::new(20),
1011 );
1012 compositor.attach_test_window(window_id, Box::<TestPlatformHost>::default());
1013
1014 let emitted = Rc::new(RefCell::new(Vec::new()));
1015 compositor
1016 .apply(
1017 CompositionCommand::SetWindowComposition {
1018 window_id,
1019 composition: WindowCompositionSpec {
1020 items: vec![transparent_item(1, target)],
1021 },
1022 },
1023 {
1024 let emitted = Rc::clone(&emitted);
1025 move |command| emitted.borrow_mut().push(command)
1026 },
1027 )
1028 .unwrap();
1029
1030 let emitted = emitted.take();
1031 assert_eq!(emitted.len(), 1);
1032 assert!(matches!(
1033 emitted.first(),
1034 Some(BrowserCommand::SetTransientBrowsingContextBackgroundPolicy {
1035 transient_browsing_context_id,
1036 policy: GenericBackgroundPolicy::Transparent,
1037 }) if *transient_browsing_context_id
1038 == cbf::data::ids::TransientBrowsingContextId::new(20)
1039 ));
1040 }
1041
1042 #[test]
1043 fn set_active_item_updates_platform_host() {
1044 let mut compositor = Compositor::new();
1045 let window_id = CompositorWindowId::new(1);
1046 let host = TestPlatformHost::default();
1047 let active_item_log = Rc::clone(&host.last_active_item);
1048 compositor.attach_test_window(window_id, Box::new(host));
1049
1050 compositor
1051 .apply(
1052 CompositionCommand::SetWindowComposition {
1053 window_id,
1054 composition: WindowCompositionSpec {
1055 items: vec![item(
1056 1,
1057 SurfaceTarget::BrowsingContext(BrowsingContextId::new(10)),
1058 )],
1059 },
1060 },
1061 |_| {},
1062 )
1063 .unwrap();
1064
1065 compositor
1066 .set_active_item(CompositionItemId::new(1))
1067 .unwrap();
1068
1069 assert_eq!(*active_item_log.borrow(), Some(CompositionItemId::new(1)));
1070 }
1071
1072 #[test]
1073 fn set_active_item_rejects_hidden_item() {
1074 let mut compositor = Compositor::new();
1075 let window_id = CompositorWindowId::new(1);
1076 compositor.attach_test_window(window_id, Box::<TestPlatformHost>::default());
1077
1078 compositor
1079 .apply(
1080 CompositionCommand::SetWindowComposition {
1081 window_id,
1082 composition: WindowCompositionSpec {
1083 items: vec![CompositionItemSpec {
1084 visible: false,
1085 ..item(
1086 1,
1087 SurfaceTarget::BrowsingContext(BrowsingContextId::new(10)),
1088 )
1089 }],
1090 },
1091 },
1092 |_| {},
1093 )
1094 .unwrap();
1095
1096 let err = compositor
1097 .set_active_item(CompositionItemId::new(1))
1098 .unwrap_err();
1099 assert!(matches!(err, CompositorError::ItemNotInteractive));
1100 }
1101
1102 #[test]
1103 fn set_active_item_rejects_unknown_item() {
1104 let mut compositor = Compositor::new();
1105 let window_id = CompositorWindowId::new(1);
1106 compositor.attach_test_window(window_id, Box::<TestPlatformHost>::default());
1107
1108 let err = compositor
1109 .set_active_item(CompositionItemId::new(999))
1110 .unwrap_err();
1111 assert!(matches!(err, CompositorError::UnknownItem));
1112 }
1113
1114 #[test]
1115 fn set_item_hit_test_regions_updates_platform_scene_snapshot() {
1116 let mut compositor = Compositor::new();
1117 let window_id = CompositorWindowId::new(1);
1118 let host = TestPlatformHost::default();
1119 let scene_log = Rc::clone(&host.last_scene);
1120 compositor.attach_test_window(window_id, Box::new(host));
1121
1122 compositor
1123 .apply(
1124 CompositionCommand::SetWindowComposition {
1125 window_id,
1126 composition: WindowCompositionSpec {
1127 items: vec![region_item(
1128 1,
1129 SurfaceTarget::BrowsingContext(BrowsingContextId::new(10)),
1130 )],
1131 },
1132 },
1133 |_| {},
1134 )
1135 .unwrap();
1136
1137 compositor
1138 .apply(
1139 CompositionCommand::SetItemHitTestRegions {
1140 window_id,
1141 item_id: CompositionItemId::new(1),
1142 snapshot_id: 3,
1143 coordinate_space: HitTestCoordinateSpace::ItemLocalCssPx,
1144 mode: HitTestRegionMode::ConsumeListedRegions,
1145 regions: vec![HitTestRegion::new(10.0, 20.0, 30.0, 40.0)],
1146 },
1147 |_| {},
1148 )
1149 .unwrap();
1150
1151 let scene = scene_log.borrow();
1152 let snapshot = scene
1153 .first()
1154 .and_then(|item| item.hit_test_snapshot.as_ref())
1155 .expect("snapshot should be synced");
1156 assert_eq!(snapshot.snapshot_id, 3);
1157 assert_eq!(snapshot.mode, HitTestRegionMode::ConsumeListedRegions);
1158 assert_eq!(
1159 snapshot.regions,
1160 vec![HitTestRegion::new(10.0, 20.0, 30.0, 40.0)]
1161 );
1162 }
1163
1164 #[test]
1165 fn set_item_hit_test_regions_rejects_bounds_item() {
1166 let mut compositor = Compositor::new();
1167 let window_id = CompositorWindowId::new(1);
1168 compositor.attach_test_window(window_id, Box::<TestPlatformHost>::default());
1169
1170 compositor
1171 .apply(
1172 CompositionCommand::SetWindowComposition {
1173 window_id,
1174 composition: WindowCompositionSpec {
1175 items: vec![item(
1176 1,
1177 SurfaceTarget::BrowsingContext(BrowsingContextId::new(10)),
1178 )],
1179 },
1180 },
1181 |_| {},
1182 )
1183 .unwrap();
1184
1185 let err = compositor
1186 .apply(
1187 CompositionCommand::SetItemHitTestRegions {
1188 window_id,
1189 item_id: CompositionItemId::new(1),
1190 snapshot_id: 1,
1191 coordinate_space: HitTestCoordinateSpace::ItemLocalCssPx,
1192 mode: HitTestRegionMode::ConsumeListedRegions,
1193 regions: vec![HitTestRegion::new(0.0, 0.0, 10.0, 10.0)],
1194 },
1195 |_| {},
1196 )
1197 .unwrap_err();
1198
1199 assert!(matches!(
1200 err,
1201 CompositorError::ItemDoesNotUseRegionHitTesting
1202 ));
1203 }
1204}