use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use crate::XamlEnv;
use crate::XamlScene;
use crate::binding::DataContext;
use crate::components::{PfFrame, PfLogicalParent};
#[derive(Resource, Default)]
pub struct PfPages(pub HashMap<String, XamlScene>);
#[derive(Message, Debug, Clone)]
pub struct PfNavigated {
pub frame: Entity,
pub source: String,
pub title: Option<String>,
}
pub trait PfNavigationAppExt {
fn register_page(&mut self, route: impl Into<String>, scene: XamlScene) -> &mut Self;
}
impl PfNavigationAppExt for App {
fn register_page(&mut self, route: impl Into<String>, scene: XamlScene) -> &mut Self {
self.world_mut()
.get_resource_or_insert_with::<PfPages>(Default::default)
.0
.insert(route.into(), scene);
self
}
}
fn resolve(world: &World, route: &str) -> Option<bevy_pf_xaml::XamlDocument> {
if let Some(pages) = world.get_resource::<PfPages>()
&& let Some(scene) = pages.0.get(route)
{
return Some(scene.document());
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = std::path::Path::new("assets").join(route);
if let Ok(source) = std::fs::read_to_string(path) {
return bevy_pf_xaml::parse(&source).ok();
}
}
None
}
pub fn navigate(world: &mut World, frame: Entity, route: &str) -> bool {
let Some(current) = world.get::<PfFrame>(frame).map(|f| f.current.clone()) else {
warn!("bevy_pf: navigate() target is not a Frame");
return false;
};
if !show_page(world, frame, route) {
return false;
}
if let Some(mut f) = world.get_mut::<PfFrame>(frame) {
if let Some(prev) = current {
f.back.push(prev);
}
f.forward.clear();
}
sync_chrome(world, frame);
true
}
pub fn go_back(world: &mut World, frame: Entity) -> bool {
let Some((prev, current)) = world.get_mut::<PfFrame>(frame).and_then(|mut f| {
let prev = f.back.pop()?;
let current = f.current.clone();
Some((prev, current))
}) else {
return false;
};
if !show_page(world, frame, &prev) {
return false;
}
if let Some(mut f) = world.get_mut::<PfFrame>(frame)
&& let Some(current) = current
{
f.forward.push(current);
}
sync_chrome(world, frame);
true
}
pub fn go_forward(world: &mut World, frame: Entity) -> bool {
let Some((next, current)) = world.get_mut::<PfFrame>(frame).and_then(|mut f| {
let next = f.forward.pop()?;
let current = f.current.clone();
Some((next, current))
}) else {
return false;
};
if !show_page(world, frame, &next) {
return false;
}
if let Some(mut f) = world.get_mut::<PfFrame>(frame)
&& let Some(current) = current
{
f.back.push(current);
}
sync_chrome(world, frame);
true
}
pub fn can_go_back(world: &World, frame: Entity) -> bool {
world
.get::<PfFrame>(frame)
.is_some_and(|f| !f.back.is_empty())
}
pub fn can_go_forward(world: &World, frame: Entity) -> bool {
world
.get::<PfFrame>(frame)
.is_some_and(|f| !f.forward.is_empty())
}
fn show_page(world: &mut World, frame: Entity, route: &str) -> bool {
let Some(doc) = resolve(world, route) else {
warn!("bevy_pf: page `{route}` is not registered and no file was found");
return false;
};
let Some(content) = world.get::<PfFrame>(frame).map(|f| f.content) else {
return false;
};
world.entity_mut(content).despawn_children();
let page_root = world.spawn(PfLogicalParent(frame)).id();
let _ = world.get::<DataContext>(frame); match crate::instantiate_document_env(world, page_root, &doc, &XamlEnv::default()) {
Ok(result) => {
for w in &result.warnings {
warn!("bevy_pf: page `{route}`: {w}");
}
}
Err(e) => {
warn!("bevy_pf: page `{route}` failed to instantiate: {e}");
world.entity_mut(page_root).despawn();
return false;
}
}
world.entity_mut(content).add_children(&[page_root]);
let title = match doc.root.attribute("Title") {
Some(bevy_pf_xaml::XamlValue::Str(s)) => Some(s.clone()),
_ => None,
};
if let Some(mut f) = world.get_mut::<PfFrame>(frame) {
f.current = Some(route.to_string());
f.current_title = title.clone();
}
world.write_message(PfNavigated {
frame,
source: route.to_string(),
title,
});
true
}
pub fn follow_hyperlink(world: &mut World, link: Entity, uri: &str) {
let external =
uri.starts_with("http://") || uri.starts_with("https://") || uri.starts_with("mailto:");
if external {
crate::util::open_url(uri);
return;
}
let mut current = link;
loop {
if world.get::<PfFrame>(current).is_some() {
navigate(world, current, uri);
return;
}
current = match world.get::<ChildOf>(current) {
Some(c) => c.parent(),
None => match world.get::<PfLogicalParent>(current) {
Some(l) => l.0,
None => break,
},
};
}
warn!("bevy_pf: Hyperlink `{uri}` has no enclosing Frame; opening externally");
crate::util::open_url(uri);
}
fn sync_chrome(world: &mut World, frame: Entity) {
let Some((back_btn, fwd_btn)) = world
.get::<PfFrame>(frame)
.and_then(|f| f.chrome.map(|c| (c.back_button, c.forward_button)))
else {
return;
};
let back_ok = can_go_back(world, frame);
let fwd_ok = can_go_forward(world, frame);
for (button, enabled) in [(back_btn, back_ok), (fwd_btn, fwd_ok)] {
if let Some(mut bg) = world.get_mut::<BackgroundColor>(button) {
bg.0 = if enabled {
Color::srgb_u8(0xE6, 0xE6, 0xE6)
} else {
Color::srgb_u8(0xF4, 0xF4, 0xF4)
};
}
if let Some(mut text_color) = world
.get::<Children>(button)
.and_then(|c| c.iter().next())
.and_then(|t| world.get_mut::<bevy::text::TextColor>(t))
{
text_color.0 = if enabled {
Color::srgb_u8(0x1A, 0x1A, 0x1A)
} else {
Color::srgb_u8(0xAF, 0xAF, 0xAF)
};
}
}
}
pub(crate) fn init_pending_frames(world: &mut World) {
let mut pending: Vec<(Entity, String)> = Vec::new();
let mut frames = world.query::<(Entity, &PfFrame)>();
for (entity, frame) in frames.iter(world) {
if let Some(source) = &frame.pending_source {
pending.push((entity, source.clone()));
}
}
for (entity, source) in pending {
let resolvable = resolve(world, &source).is_some();
if resolvable {
if let Some(mut f) = world.get_mut::<PfFrame>(entity) {
f.pending_source = None;
}
show_page(world, entity, &source);
sync_chrome(world, entity);
}
}
}