Skip to main content

cbf_compositor/core/
compositor.rs

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
35/// Scene compositor that attaches to native host windows and routes browser
36/// surfaces into a declarative composition tree.
37pub 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
46/// Host callback used to accept or consume a routed input event before dispatch.
47pub type EventRouter = Arc<dyn Fn(&RoutedEventContext) -> EventRoutingDecision + Send + Sync>;
48
49/// Options for attaching a host window to the compositor.
50#[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/// Host decision applied after compositor hit-testing and before backend dispatch.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum EventRoutingDecision {
69    Dispatch,
70    Consume,
71}
72
73/// Routed event kinds supported by the host event router.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum RoutedEventKind {
76    PointerDown,
77    PointerUp,
78    Wheel,
79    KeyDown,
80}
81
82/// Context describing one compositor-routed input event.
83#[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    /// Create an empty compositor with no attached windows.
105    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    /// Attach a host-native window and return its compositor window id.
117    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    /// Detach a previously attached compositor window.
145    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    /// Apply a declarative scene update to one attached window.
162    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    /// Feed browser-generic backend events into the compositor state machine.
237    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    /// Feed Chrome-specific events into the compositor.
316    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    /// No-op Chrome event hook when the Chrome backend feature is disabled.
322    pub fn update_chrome_event(&mut self, _event: &()) -> Result<(), CompositorError> {
323        Ok(())
324    }
325
326    /// Return the scene target currently displayed by an item.
327    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    /// Return every item currently showing the given target.
332    pub fn item_ids_for_target(&self, target: SurfaceTarget) -> Vec<CompositionItemId> {
333        self.composition_state.item_ids_for_target(target)
334    }
335
336    /// Return the compositor window that owns the given item.
337    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    /// Programmatically move the active input target to one visible input-receiving item.
342    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    /// Return the last preferred size hint reported for a transient popup.
364    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    /// Present a host-owned context menu for the given surface target.
376    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    /// Present a host-owned choice menu for the given surface target.
393    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    /// Start a host-owned native drag session from a browsing-context target.
409    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}