use std::sync::Arc;
use frust_core::RenderRoot;
use frust_core::view::View;
use frust_core::widget::PlatformViewFrame;
use frust_shell_common::platform_view::{PlatformViewState, ViewCommand};
use winit::window::Window;
use crate::extensions::DesktopExtensions;
#[derive(Debug, Default)]
pub(crate) struct DesktopPlatformViews {
state: PlatformViewState,
}
impl DesktopPlatformViews {
pub(crate) fn new() -> Self {
Self {
state: PlatformViewState::new(),
}
}
pub(crate) fn after_paint<State, V>(&mut self, root: &mut RenderRoot<State, V>) -> bool
where
State: 'static,
V: View<State>,
{
let retired = root.take_retired_platform_views();
self.ingest(&retired, root.platform_view_frames())
}
fn ingest(&mut self, retired: &[u64], frames: &[PlatformViewFrame]) -> bool {
let mut produced = false;
for &slot_id in retired {
produced |= self.state.retire(slot_id);
}
produced |= self.state.ingest(frames, &[]);
produced
}
pub(crate) fn drain<E: DesktopExtensions>(
&mut self,
ext: &mut E,
window: &Arc<Window>,
scale: f64,
) {
let Some((generation, commands)) = self.pending() else {
return;
};
ext.on_platform_view_commands(window, scale, commands);
self.applied(generation);
}
fn pending(&self) -> Option<(u64, &[ViewCommand])> {
let (generation, commands) = self.state.commands();
(!commands.is_empty()).then_some((generation, commands))
}
fn applied(&mut self, generation: u64) {
self.state.acknowledge(generation);
}
pub(crate) fn on_surface_recreated(&mut self) {
self.state.reset_for_surface_recreate();
}
pub(crate) fn suspend<E: DesktopExtensions>(&mut self, ext: &mut E) {
self.state.suspend_all();
ext.on_platform_views_suspended();
let (generation, _) = self.state.commands();
self.applied(generation);
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust_shell_common::platform_view::DISPOSE_AFTER_MISSING_FRAMES;
use kurbo::Rect;
#[derive(Debug, Default)]
struct SpyExtensions {
suspends: usize,
}
impl DesktopExtensions for SpyExtensions {
fn on_platform_views_suspended(&mut self) {
self.suspends += 1;
}
}
fn frame(slot_id: u64, rect: Rect) -> PlatformViewFrame {
PlatformViewFrame {
slot_id,
view_type: "dev.frust.VideoPlayerFactory".to_string(),
params_json: String::new(),
params_generation: 0,
rect,
clip: None,
visible: true,
interactive: false,
shields: Vec::new(),
}
}
fn pending(host: &DesktopPlatformViews) -> Vec<ViewCommand> {
host.pending()
.map(|(_, commands)| commands.to_vec())
.unwrap_or_default()
}
fn drain(host: &mut DesktopPlatformViews) -> Vec<ViewCommand> {
let Some((generation, commands)) = host.pending() else {
return Vec::new();
};
let batch = commands.to_vec();
host.applied(generation);
batch
}
#[test]
fn a_new_slot_arrives_as_create_then_update_in_one_batch() {
let mut host = DesktopPlatformViews::new();
let rect = Rect::new(10.0, 20.0, 110.0, 140.0);
assert!(host.ingest(&[], &[frame(7, rect)]));
let batch = pending(&host);
assert_eq!(batch.len(), 2, "create and update ship together: {batch:?}");
assert!(matches!(
&batch[0],
ViewCommand::Create { slot_id: 7, view_type, interactive: false, .. }
if view_type == "dev.frust.VideoPlayerFactory"
));
assert_eq!(
batch[1],
ViewCommand::Update {
slot_id: 7,
rect,
clip: None,
visible: true,
shields: Vec::new(),
}
);
}
#[test]
fn an_unchanged_repaint_produces_nothing() {
let mut host = DesktopPlatformViews::new();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
host.ingest(&[], &[frame(1, rect)]);
drain(&mut host);
assert!(
!host.ingest(&[], &[frame(1, rect)]),
"a still frame must not cost the host a call"
);
assert!(host.pending().is_none());
}
#[test]
fn a_moved_slot_produces_exactly_one_update() {
let mut host = DesktopPlatformViews::new();
host.ingest(&[], &[frame(1, Rect::new(0.0, 0.0, 100.0, 100.0))]);
drain(&mut host);
let moved = Rect::new(0.0, 40.0, 100.0, 140.0);
assert!(host.ingest(&[], &[frame(1, moved)]));
assert_eq!(
pending(&host),
vec![ViewCommand::Update {
slot_id: 1,
rect: moved,
clip: None,
visible: true,
shields: Vec::new(),
}]
);
}
#[test]
fn a_slot_missing_from_two_passes_is_hidden_without_being_disposed() {
let mut host = DesktopPlatformViews::new();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
host.ingest(&[], &[frame(1, rect)]);
drain(&mut host);
assert!(!host.ingest(&[], &[]));
assert!(host.pending().is_none());
assert!(host.ingest(&[], &[]));
assert_eq!(
drain(&mut host),
vec![ViewCommand::Update {
slot_id: 1,
rect,
clip: None,
visible: false,
shields: Vec::new(),
}],
"the hide reuses the slot's last known geometry"
);
assert!(host.ingest(&[], &[frame(1, rect)]));
assert_eq!(
drain(&mut host),
vec![ViewCommand::Update {
slot_id: 1,
rect,
clip: None,
visible: true,
shields: Vec::new(),
}]
);
}
#[test]
fn a_slot_missing_for_the_whole_dispose_streak_is_disposed() {
let mut host = DesktopPlatformViews::new();
host.ingest(&[], &[frame(1, Rect::new(0.0, 0.0, 100.0, 100.0))]);
drain(&mut host);
for _ in 0..DISPOSE_AFTER_MISSING_FRAMES {
host.ingest(&[], &[]);
}
let batch = drain(&mut host);
assert!(
batch.contains(&ViewCommand::Dispose { slot_id: 1 }),
"the streak backstop must tear the view down: {batch:?}"
);
host.ingest(&[], &[frame(1, Rect::new(0.0, 0.0, 100.0, 100.0))]);
let batch = drain(&mut host);
assert!(matches!(batch.first(), Some(ViewCommand::Create { .. })));
}
#[test]
fn a_retired_slot_is_disposed_immediately() {
let mut host = DesktopPlatformViews::new();
host.ingest(&[], &[frame(1, Rect::new(0.0, 0.0, 100.0, 100.0))]);
drain(&mut host);
assert!(host.ingest(&[1], &[]));
assert_eq!(drain(&mut host), vec![ViewCommand::Dispose { slot_id: 1 }]);
}
#[test]
fn a_surface_recreate_replays_every_live_slot() {
let mut host = DesktopPlatformViews::new();
let first = Rect::new(0.0, 0.0, 100.0, 100.0);
let second = Rect::new(0.0, 200.0, 100.0, 300.0);
host.ingest(&[], &[frame(1, first), frame(2, second)]);
drain(&mut host);
host.on_surface_recreated();
let batch = drain(&mut host);
assert_eq!(batch.len(), 4, "create + update per live slot: {batch:?}");
assert!(matches!(&batch[0], ViewCommand::Create { slot_id: 1, .. }));
assert_eq!(
batch[1],
ViewCommand::Update {
slot_id: 1,
rect: first,
clip: None,
visible: true,
shields: Vec::new(),
}
);
assert!(matches!(&batch[2], ViewCommand::Create { slot_id: 2, .. }));
assert_eq!(
batch[3],
ViewCommand::Update {
slot_id: 2,
rect: second,
clip: None,
visible: true,
shields: Vec::new(),
}
);
}
#[test]
fn suspend_fires_the_hook_and_leaves_nothing_queued() {
let mut host = DesktopPlatformViews::new();
let mut ext = SpyExtensions::default();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
host.ingest(&[], &[frame(1, rect)]);
drain(&mut host);
host.suspend(&mut ext);
assert_eq!(ext.suspends, 1, "the host must be told to drop its views");
assert!(
host.pending().is_none(),
"queued commands describe views the host has just removed"
);
host.on_surface_recreated();
let batch = drain(&mut host);
assert!(matches!(&batch[0], ViewCommand::Create { slot_id: 1, .. }));
assert_eq!(
batch[1],
ViewCommand::Update {
slot_id: 1,
rect,
clip: None,
visible: false,
shields: Vec::new(),
}
);
}
#[test]
fn suspending_with_nothing_hosted_still_notifies_the_host() {
let mut host = DesktopPlatformViews::new();
let mut ext = SpyExtensions::default();
host.suspend(&mut ext);
assert_eq!(ext.suspends, 1);
assert!(host.pending().is_none());
}
#[test]
fn acknowledging_a_batch_stops_it_being_delivered_again() {
let mut host = DesktopPlatformViews::new();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
host.ingest(&[], &[frame(1, rect)]);
let first = drain(&mut host);
assert_eq!(first.len(), 2);
assert!(
host.pending().is_none(),
"an acknowledged batch is compacted out of the backlog"
);
host.ingest(&[], &[frame(1, Rect::new(0.0, 10.0, 100.0, 110.0))]);
assert_eq!(pending(&host), pending(&host));
assert_eq!(pending(&host).len(), 1);
}
}