use std::collections::BTreeMap;
use std::ops::Range;
use std::sync::Arc;
use frust_core::RenderRoot;
use frust_core::view::View;
use frust_core::widget::PlatformViewFrame;
use frust_scene::Command;
use frust_shell_common::platform_view::{PlatformViewState, ViewCommand};
use kurbo::Rect;
use winit::window::Window;
use crate::extensions::DesktopExtensions;
pub(crate) type OverlayCutSpec = (u64, Range<usize>);
#[derive(Debug, Default)]
struct OverlayRanges {
ranges: BTreeMap<u64, Range<usize>>,
folded_generation: u64,
}
impl OverlayRanges {
fn fold(&mut self, state: &PlatformViewState) {
let (generation, backlog) = state.commands();
if generation < self.folded_generation {
*self = Self::default();
}
if generation == self.folded_generation {
return;
}
let (_, seen) = state.commands_up_to(self.folded_generation);
for command in &backlog[seen.len()..] {
match command {
ViewCommand::Overlay {
overlay_id,
command_range,
..
} => {
self.ranges.insert(*overlay_id, command_range.clone());
}
ViewCommand::DisposeOverlay { overlay_id } => {
self.ranges.remove(overlay_id);
}
_ => {}
}
}
self.folded_generation = generation;
}
fn cuts(&self) -> Vec<OverlayCutSpec> {
let mut cuts: Vec<OverlayCutSpec> = self
.ranges
.iter()
.map(|(&id, range)| (id, range.clone()))
.collect();
cuts.sort_by_key(|(id, range)| (range.start, range.end, *id));
cuts
}
}
#[derive(Debug, Default)]
pub(crate) struct DesktopPlatformViews {
state: PlatformViewState,
overlays: OverlayRanges,
}
impl DesktopPlatformViews {
pub(crate) fn new() -> Self {
Self {
state: PlatformViewState::new(),
overlays: OverlayRanges::default(),
}
}
pub(crate) fn after_paint<State, V>(
&mut self,
root: &mut RenderRoot<State, V>,
commands: &[Command],
viewport: Rect,
) -> bool
where
State: 'static,
V: View<State>,
{
let retired = root.take_retired_platform_views();
if crate::extensions::overlays_enabled() {
static ANNOUNCED: std::sync::Once = std::sync::Once::new();
ANNOUNCED.call_once(|| {
log::info!("frust-overlay: overlay surfaces on (one per differ cut, Mode A)");
});
self.ingest_scene(&retired, root.platform_view_frames(), commands, viewport)
} else {
self.ingest(&retired, root.platform_view_frames())
}
}
fn ingest_scene(
&mut self,
retired: &[u64],
frames: &[PlatformViewFrame],
commands: &[Command],
viewport: Rect,
) -> bool {
let mut produced = false;
for &slot_id in retired {
produced |= self.state.retire(slot_id);
}
produced |= self.state.ingest_scene(frames, &[], commands, viewport);
self.overlays.fold(&self.state);
produced
}
pub(crate) fn overlay_cuts(&self) -> Vec<OverlayCutSpec> {
self.overlays.cuts()
}
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.overlays.fold(&self.state);
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(),
command_index: 0,
}
}
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(),
z_index: 0,
}
);
}
#[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(),
z_index: 0,
}]
);
}
#[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(),
z_index: 0,
}],
"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(),
z_index: 0,
}]
);
}
#[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(),
z_index: 0,
}
);
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(),
z_index: 1,
}
);
}
#[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(),
z_index: 0,
}
);
}
#[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);
}
fn scene_with_slot(drawing: usize) -> frust_scene::Scene {
let mut scene = frust_scene::Scene::new();
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
for _ in 0..drawing {
builder.fill_rect(
Rect::new(0.0, 0.0, 10.0, 10.0),
peniko::Brush::Solid(peniko::Color::BLACK),
);
}
scene
}
fn slot_at(slot_id: u64, command_index: usize) -> PlatformViewFrame {
PlatformViewFrame {
command_index,
..frame(slot_id, Rect::new(0.0, 0.0, 100.0, 100.0))
}
}
#[test]
fn an_overlay_aware_ingest_announces_the_overlay_and_folds_its_range() {
let mut host = DesktopPlatformViews::new();
let scene = scene_with_slot(5);
let viewport = Rect::new(0.0, 0.0, 800.0, 600.0);
assert!(host.ingest_scene(&[], &[slot_at(1, 2)], scene.commands(), viewport));
assert!(
pending(&host)
.iter()
.any(|command| matches!(command, ViewCommand::Overlay { overlay_id: 1, .. })),
"the batch the host receives carries the overlay"
);
assert_eq!(host.overlay_cuts(), vec![(1, 2..5)]);
drain(&mut host);
assert!(!host.ingest_scene(&[], &[slot_at(1, 2)], scene.commands(), viewport));
assert_eq!(host.overlay_cuts(), vec![(1, 2..5)]);
assert!(host.ingest_scene(&[1], &[], scene.commands(), viewport));
assert!(host.overlay_cuts().is_empty());
assert!(matches!(
pending(&host).first(),
Some(ViewCommand::DisposeOverlay { overlay_id: 1 })
));
}
#[test]
fn the_plain_ingest_folds_no_overlay() {
let mut host = DesktopPlatformViews::new();
host.ingest(&[], &[slot_at(1, 2)]);
assert!(host.overlay_cuts().is_empty());
assert!(
!pending(&host)
.iter()
.any(|command| matches!(command, ViewCommand::Overlay { .. }))
);
}
}