use std::sync::{Arc, Mutex};
use bevy::prelude::*;
use noesis_runtime::classes::{
ClassBuilder, ClassInstance, ClassRegistration, Instance, PropertyChangeHandler, PropertyValue,
};
use noesis_runtime::ffi::{ClassBase, PropType};
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Debug, Clone, PartialEq)]
pub enum VmValue {
Double(f64),
Bool(bool),
Int32(i32),
Str(String),
}
impl VmValue {
fn from_property(value: &PropertyValue<'_>) -> Option<Self> {
match *value {
PropertyValue::Double(d) => Some(Self::Double(d)),
PropertyValue::Float(f) => Some(Self::Double(f64::from(f))),
PropertyValue::Bool(b) => Some(Self::Bool(b)),
PropertyValue::Int32(i) => Some(Self::Int32(i)),
PropertyValue::String(s) => Some(Self::Str(s.unwrap_or_default().to_owned())),
_ => None,
}
}
fn apply_to(&self, instance: Instance, index: u32) {
match self {
Self::Double(v) => instance.set_double(index, *v),
Self::Bool(v) => instance.set_bool(index, *v),
Self::Int32(v) => instance.set_int32(index, *v),
Self::Str(v) => instance.set_string(index, v),
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum AttachTarget {
Root,
Named(String),
}
#[derive(Debug, Clone)]
pub struct ViewModelDef {
class_name: String,
props: Vec<(String, PropType)>,
target: AttachTarget,
}
impl ViewModelDef {
#[must_use]
pub fn new(class_name: impl Into<String>) -> Self {
Self {
class_name: class_name.into(),
props: Vec::new(),
target: AttachTarget::Root,
}
}
#[must_use]
pub fn property(mut self, name: impl Into<String>, kind: PropType) -> Self {
self.props.push((name.into(), kind));
self
}
#[must_use]
pub fn attach_to_root(mut self) -> Self {
self.target = AttachTarget::Root;
self
}
#[must_use]
pub fn attach_to(mut self, x_name: impl Into<String>) -> Self {
self.target = AttachTarget::Named(x_name.into());
self
}
pub(crate) fn class_name(&self) -> &str {
&self.class_name
}
}
#[derive(Component)]
pub struct NoesisVm {
def: ViewModelDef,
pending: Vec<(String, VmValue)>,
}
impl NoesisVm {
#[must_use]
pub fn new(def: ViewModelDef) -> Self {
Self {
def,
pending: Vec::new(),
}
}
pub fn set_f64(&mut self, prop: impl Into<String>, value: f64) {
self.pending.push((prop.into(), VmValue::Double(value)));
}
pub fn set_bool(&mut self, prop: impl Into<String>, value: bool) {
self.pending.push((prop.into(), VmValue::Bool(value)));
}
pub fn set_i32(&mut self, prop: impl Into<String>, value: i32) {
self.pending.push((prop.into(), VmValue::Int32(value)));
}
pub fn set_string(&mut self, prop: impl Into<String>, value: impl Into<String>) {
self.pending.push((prop.into(), VmValue::Str(value.into())));
}
pub(crate) fn def(&self) -> &ViewModelDef {
&self.def
}
pub(crate) fn take_pending(&mut self) -> Vec<(String, VmValue)> {
std::mem::take(&mut self.pending)
}
}
#[derive(Resource, Clone, Default)]
pub struct SharedVmChangedQueue(Arc<Mutex<Vec<(Entity, String, VmValue)>>>);
impl SharedVmChangedQueue {
pub(crate) fn push(&self, view: Entity, prop: String, value: VmValue) {
self.0
.lock()
.expect("SharedVmChangedQueue poisoned")
.push((view, prop, value));
}
#[must_use]
pub fn drain(&self) -> Vec<(Entity, String, VmValue)> {
let mut guard = self.0.lock().expect("SharedVmChangedQueue poisoned");
if guard.is_empty() {
Vec::new()
} else {
std::mem::take(&mut *guard)
}
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisViewModelChanged {
pub view: Entity,
pub prop: String,
pub value: VmValue,
}
pub struct ViewModelChangeForwarder {
view: Entity,
prop_names: Arc<Vec<String>>,
queue: SharedVmChangedQueue,
}
impl ViewModelChangeForwarder {
#[must_use]
pub fn new(view: Entity, prop_names: Arc<Vec<String>>, queue: SharedVmChangedQueue) -> Self {
Self {
view,
prop_names,
queue,
}
}
}
impl PropertyChangeHandler for ViewModelChangeForwarder {
fn on_changed(&self, _instance: Instance, prop_index: u32, value: PropertyValue<'_>) {
let Some(name) = self.prop_names.get(prop_index as usize) else {
return;
};
let Some(value) = VmValue::from_property(&value) else {
return;
};
self.queue.push(self.view, name.clone(), value);
}
}
pub(crate) struct VmEntry {
instance: ClassInstance,
_registration: ClassRegistration,
prop_names: Vec<String>,
target: AttachTarget,
attached_for_uri: Option<String>,
}
impl VmEntry {
pub(crate) fn build(
view: Entity,
def: &ViewModelDef,
changed: &SharedVmChangedQueue,
) -> Option<Self> {
let prop_names: Vec<String> = def.props.iter().map(|(n, _)| n.clone()).collect();
let forwarder =
ViewModelChangeForwarder::new(view, Arc::new(prop_names.clone()), changed.clone());
let mut builder = ClassBuilder::new(&def.class_name, ClassBase::ContentControl, forwarder);
for (name, kind) in &def.props {
builder.add_property(name, *kind);
}
let registration = builder.register()?;
let instance = registration.create_instance()?;
Some(Self {
instance,
_registration: registration,
prop_names,
target: def.target.clone(),
attached_for_uri: None,
})
}
pub(crate) fn target(&self) -> &AttachTarget {
&self.target
}
pub(crate) fn instance(&self) -> &ClassInstance {
&self.instance
}
pub(crate) fn write(&self, prop: &str, value: &VmValue) -> bool {
let Some(index) = self.prop_names.iter().position(|n| n == prop) else {
return false;
};
value.apply_to(self.instance.handle(), index as u32);
true
}
pub(crate) fn needs_attach(&self, uri: &str) -> bool {
self.attached_for_uri.as_deref() != Some(uri)
}
pub(crate) fn mark_attached(&mut self, uri: &str) {
self.attached_for_uri = Some(uri.to_owned());
}
pub(crate) fn reset_attach(&mut self) {
self.attached_for_uri = None;
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_view_models(
mut views: Query<(Entity, &mut NoesisVm)>,
changed: Res<SharedVmChangedQueue>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, mut vm) in &mut views {
state.ensure_view_model(entity, vm.def(), &changed);
let writes = vm.take_pending();
if !writes.is_empty() {
state.apply_view_model_writes_for(entity, &writes);
}
}
state.attach_view_models();
}
#[allow(clippy::needless_pass_by_value)]
pub fn drain_vm_changed_queue(
queue: Res<SharedVmChangedQueue>,
mut messages: MessageWriter<NoesisViewModelChanged>,
) {
for (view, prop, value) in queue.drain() {
messages.write(NoesisViewModelChanged { view, prop, value });
}
}
pub struct NoesisViewModelPlugin;
impl Plugin for NoesisViewModelPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(SharedVmChangedQueue::default())
.add_message::<NoesisViewModelChanged>()
.add_systems(PreUpdate, drain_vm_changed_queue)
.add_systems(PostUpdate, sync_view_models.in_set(NoesisSet::Apply));
}
}