use std::any::TypeId;
use std::collections::HashMap;
use bevy::ecs::component::Mutable;
use bevy::prelude::*;
use crate::plain_vm::{NoesisViewModel, PlainType, PlainValue};
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Component, Clone, Debug)]
#[require(PanelAggregate)]
pub struct UiPanel {
uri: String,
host: Entity,
host_name: String,
static_data: bool,
deferred: bool,
}
impl UiPanel {
#[must_use]
pub fn new(uri: impl Into<String>) -> Self {
Self {
uri: uri.into(),
host: Entity::PLACEHOLDER,
host_name: String::new(),
static_data: false,
deferred: false,
}
}
#[must_use]
pub fn mount_into(mut self, host: Entity, host_name: impl Into<String>) -> Self {
self.host = host;
self.host_name = host_name.into();
self
}
#[must_use]
pub fn static_context(mut self) -> Self {
self.static_data = true;
self
}
#[must_use]
pub fn deferred_seal(mut self) -> Self {
self.deferred = true;
self
}
#[must_use]
pub fn uri(&self) -> &str {
&self.uri
}
#[must_use]
pub fn host(&self) -> Entity {
self.host
}
#[must_use]
pub fn host_name(&self) -> &str {
&self.host_name
}
}
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct SealPanel;
struct FieldContribution {
reg_index: u32,
props: &'static [(&'static str, PlainType)],
}
#[derive(Component, Default)]
pub(crate) struct PanelAggregate {
present: HashMap<TypeId, FieldContribution>,
pending: HashMap<TypeId, Vec<PlainValue>>,
layout: Vec<(String, PlainType)>,
slots: HashMap<TypeId, (u32, u32)>,
writebacks: Vec<(u32, PlainValue)>,
built: bool,
}
impl PanelAggregate {
fn note_present(
&mut self,
tid: TypeId,
reg_index: u32,
props: &'static [(&'static str, PlainType)],
) {
self.present
.insert(tid, FieldContribution { reg_index, props });
}
fn set_pending(&mut self, tid: TypeId, snapshot: Vec<PlainValue>) {
self.pending.insert(tid, snapshot);
}
fn freeze(&mut self) {
let mut fields: Vec<(&TypeId, &FieldContribution)> = self.present.iter().collect();
fields.sort_by_key(|(_, c)| c.reg_index);
let mut offset: u32 = 0;
for (tid, contrib) in fields {
let len = contrib.props.len() as u32;
self.slots.insert(*tid, (offset, len));
for (name, kind) in contrib.props {
self.layout.push(((*name).to_owned(), *kind));
}
offset += len;
}
self.built = true;
}
fn take_pushes(&mut self) -> Vec<(u32, PlainValue)> {
let mut out = Vec::new();
for (tid, snapshot) in self.pending.drain() {
let Some((offset, len)) = self.slots.get(&tid).copied() else {
continue;
};
for (i, value) in snapshot.into_iter().enumerate() {
if (i as u32) < len {
out.push((offset + i as u32, value));
}
}
}
out
}
fn slot(&self, tid: TypeId) -> Option<(u32, u32)> {
self.slots.get(&tid).copied()
}
}
#[derive(Resource, Default)]
pub(crate) struct PanelFieldOrder {
indices: HashMap<TypeId, u32>,
next: u32,
}
impl PanelFieldOrder {
fn register(&mut self, tid: TypeId) -> u32 {
if let Some(i) = self.indices.get(&tid) {
return *i;
}
let i = self.next;
self.next += 1;
self.indices.insert(tid, i);
i
}
fn index_of(&self, tid: TypeId) -> u32 {
self.indices.get(&tid).copied().unwrap_or(u32::MAX)
}
}
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub enum NoesisPanelSet {
Collect,
Writeback,
}
#[allow(clippy::needless_pass_by_value)]
fn collect_panel_field<T: NoesisViewModel + Component>(
order: Res<PanelFieldOrder>,
mut panels: Query<(&mut PanelAggregate, Ref<T>), With<UiPanel>>,
) {
let tid = TypeId::of::<T>();
let reg = order.index_of(tid);
for (mut agg, field) in &mut panels {
if !agg.built {
agg.note_present(tid, reg, T::noesis_properties());
} else if !agg.slots.contains_key(&tid) {
bevy::log::warn_once!(
"NoesisPanel: bound component `{}` was inserted after the panel's \
DataContext froze on its first reconcile; its fields are not bound. \
Insert every bound component before the panel's first frame (e.g. in \
the same spawn bundle).",
std::any::type_name::<T>(),
);
}
if field.is_changed() {
agg.set_pending(tid, field.noesis_snapshot());
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_panels(
mut commands: Commands,
mut panels: Query<(Entity, &UiPanel, &mut PanelAggregate, Has<SealPanel>)>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, panel, mut agg, sealed) in &mut panels {
if !agg.built {
let freeze = if panel.deferred {
sealed
} else {
panel.static_data || !agg.present.is_empty()
};
if !freeze {
continue;
}
agg.freeze();
if panel.deferred {
commands.entity(entity).remove::<SealPanel>();
}
}
let pushes = agg.take_pushes();
let writebacks = state.sync_panel(
entity,
&panel.uri,
panel.host,
&panel.host_name,
&agg.layout,
&pushes,
);
agg.writebacks = writebacks;
}
}
#[allow(clippy::needless_pass_by_value)]
fn apply_panel_writeback<T: NoesisViewModel + Component<Mutability = Mutable>>(
mut panels: Query<(&PanelAggregate, &mut T), With<UiPanel>>,
) {
let tid = TypeId::of::<T>();
for (agg, mut field) in &mut panels {
let Some((offset, len)) = agg.slot(tid) else {
continue;
};
for (gi, value) in &agg.writebacks {
if *gi >= offset && *gi < offset + len {
field.noesis_apply(*gi - offset, value);
}
}
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisPanelText {
pub watch: Vec<String>,
}
impl NoesisPanelText {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn watching(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.watch.extend(names.into_iter().map(Into::into));
self
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisPanelTextChanged {
pub panel: Entity,
pub name: String,
pub text: String,
}
#[allow(clippy::needless_pass_by_value)]
fn poll_panel_text(
panels: Query<(Entity, &NoesisPanelText), With<UiPanel>>,
state: Option<NonSendMut<NoesisRenderState>>,
mut changed: MessageWriter<NoesisPanelTextChanged>,
) {
let Some(mut state) = state else {
return;
};
for (entity, watch) in &panels {
for (name, text) in state.poll_panel_text_for(entity, &watch.watch) {
changed.write(NoesisPanelTextChanged {
panel: entity,
name,
text,
});
}
}
}
pub trait NoesisPanelAppExt {
fn add_noesis_panel_field<T: NoesisViewModel + Component<Mutability = Mutable>>(
&mut self,
) -> &mut Self;
}
impl NoesisPanelAppExt for App {
fn add_noesis_panel_field<T: NoesisViewModel + Component<Mutability = Mutable>>(
&mut self,
) -> &mut Self {
{
let mut order = self
.world_mut()
.get_resource_or_insert_with(PanelFieldOrder::default);
order.register(TypeId::of::<T>());
}
self.add_systems(
PostUpdate,
collect_panel_field::<T>.in_set(NoesisPanelSet::Collect),
);
self.add_systems(
PostUpdate,
apply_panel_writeback::<T>.in_set(NoesisPanelSet::Writeback),
);
self
}
}
#[derive(Default)]
pub struct NoesisPanelPlugin;
impl Plugin for NoesisPanelPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<PanelFieldOrder>();
app.configure_sets(
PostUpdate,
(
NoesisPanelSet::Collect.before(NoesisSet::Apply),
NoesisPanelSet::Writeback.after(NoesisSet::Apply),
),
);
app.add_message::<NoesisPanelTextChanged>();
app.add_systems(
PostUpdate,
(sync_panels, poll_panel_text.after(sync_panels)).in_set(NoesisSet::Apply),
);
}
}