use async_channel::Receiver;
use bevy::prelude::*;
use bevy_cef_core::prelude::{TitleChangedMessage, TitleChangedSenderInner};
use serde::{Deserialize, Serialize};
pub(super) struct TitlePlugin;
impl Plugin for TitlePlugin {
fn build(&self, app: &mut App) {
let (tx, rx) = async_channel::unbounded();
app.insert_resource(TitleChangedSender(tx))
.insert_resource(TitleChangedReceiver(rx))
.register_type::<TitleChanged>()
.register_type::<WebviewTitle>()
.add_systems(PreUpdate, drain_title_changed);
}
}
#[derive(Debug, EntityEvent, Clone, Reflect, Serialize, Deserialize)]
pub struct TitleChanged {
#[event_target]
pub webview: Entity,
pub title: String,
}
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component, Debug, Default)]
pub struct WebviewTitle(pub String);
#[derive(Resource, Debug, Deref)]
pub(crate) struct TitleChangedSender(pub(crate) TitleChangedSenderInner);
#[derive(Resource, Debug)]
struct TitleChangedReceiver(Receiver<TitleChangedMessage>);
fn drain_title_changed(
mut commands: Commands,
mut titles: Query<&mut WebviewTitle>,
receiver: Res<TitleChangedReceiver>,
) {
while let Ok(msg) = receiver.0.try_recv() {
let Ok(mut title) = titles.get_mut(msg.webview) else {
continue;
};
if msg.title == title.0 {
continue;
}
title.0 = msg.title.clone();
commands.trigger(TitleChanged {
webview: msg.webview,
title: msg.title,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Resource, Default)]
struct FiredTitles(Vec<String>);
fn record_title(on: On<TitleChanged>, mut fired: ResMut<FiredTitles>) {
fired.0.push(on.title.clone());
}
fn setup() -> (World, Schedule, async_channel::Sender<TitleChangedMessage>) {
let (tx, rx) = async_channel::unbounded::<TitleChangedMessage>();
let mut world = World::new();
world.insert_resource(TitleChangedReceiver(rx));
world.init_resource::<FiredTitles>();
world.add_observer(record_title);
let mut schedule = Schedule::default();
schedule.add_systems(drain_title_changed);
(world, schedule, tx)
}
#[test]
fn updates_component_and_fires_event() {
let (mut world, mut schedule, tx) = setup();
let e = world.spawn(WebviewTitle::default()).id();
tx.send_blocking(TitleChangedMessage {
webview: e,
title: "Hello".into(),
})
.unwrap();
schedule.run(&mut world);
assert_eq!(
world.get::<WebviewTitle>(e).map(|t| t.0.as_str()),
Some("Hello")
);
assert_eq!(world.resource::<FiredTitles>().0, vec!["Hello".to_string()]);
}
#[test]
fn dedups_identical_titles_in_same_frame() {
let (mut world, mut schedule, tx) = setup();
let e = world.spawn(WebviewTitle::default()).id();
for _ in 0..2 {
tx.send_blocking(TitleChangedMessage {
webview: e,
title: "A".into(),
})
.unwrap();
}
schedule.run(&mut world);
assert_eq!(world.resource::<FiredTitles>().0, vec!["A".to_string()]);
}
#[test]
fn fires_for_distinct_titles() {
let (mut world, mut schedule, tx) = setup();
let e = world.spawn(WebviewTitle::default()).id();
for t in ["A", "B"] {
tx.send_blocking(TitleChangedMessage {
webview: e,
title: t.into(),
})
.unwrap();
}
schedule.run(&mut world);
assert_eq!(
world.resource::<FiredTitles>().0,
vec!["A".to_string(), "B".to_string()]
);
assert_eq!(
world.get::<WebviewTitle>(e).map(|t| t.0.clone()),
Some("B".to_string())
);
}
#[test]
fn dedups_identical_titles_across_frames() {
let (mut world, mut schedule, tx) = setup();
let e = world.spawn(WebviewTitle::default()).id();
tx.send_blocking(TitleChangedMessage {
webview: e,
title: "A".into(),
})
.unwrap();
schedule.run(&mut world);
tx.send_blocking(TitleChangedMessage {
webview: e,
title: "A".into(),
})
.unwrap();
schedule.run(&mut world);
assert_eq!(world.resource::<FiredTitles>().0, vec!["A".to_string()]);
}
}