use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use bevy::prelude::*;
use noesis_runtime::classes::{
ClassBuilder, ClassInstance, ClassRegistration, Instance, PropertyChangeHandler, PropertyValue,
};
use noesis_runtime::commands::{Command, CommandHandler, CommandParameterValue};
use noesis_runtime::ffi::{ClassBase, PropType};
use crate::render::{NoesisRenderState, NoesisSet};
use crate::viewmodel::AttachTarget;
#[derive(Debug, Clone)]
pub struct CommandsDef {
class_name: String,
commands: Vec<String>,
target: AttachTarget,
}
impl CommandsDef {
#[must_use]
pub fn new(class_name: impl Into<String>) -> Self {
Self {
class_name: class_name.into(),
commands: Vec::new(),
target: AttachTarget::Root,
}
}
#[must_use]
pub fn command(mut self, name: impl Into<String>) -> Self {
self.commands.push(name.into());
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 NoesisCommands {
def: CommandsDef,
pending_enables: Vec<(String, bool)>,
}
impl NoesisCommands {
#[must_use]
pub fn new(def: CommandsDef) -> Self {
Self {
def,
pending_enables: Vec::new(),
}
}
pub fn set_enabled(&mut self, name: impl Into<String>, enabled: bool) {
self.pending_enables.push((name.into(), enabled));
}
pub fn enable(&mut self, name: impl Into<String>) {
self.set_enabled(name, true);
}
pub fn disable(&mut self, name: impl Into<String>) {
self.set_enabled(name, false);
}
pub(crate) fn def(&self) -> &CommandsDef {
&self.def
}
pub(crate) fn take_pending_enables(&mut self) -> Vec<(String, bool)> {
std::mem::take(&mut self.pending_enables)
}
}
#[derive(Resource, Clone, Default)]
pub struct SharedCommandQueue(Arc<Mutex<Vec<(Entity, String, Option<String>)>>>);
impl SharedCommandQueue {
pub(crate) fn push(&self, view: Entity, name: String, parameter: Option<String>) {
self.0
.lock()
.expect("SharedCommandQueue poisoned")
.push((view, name, parameter));
}
#[must_use]
pub fn drain(&self) -> Vec<(Entity, String, Option<String>)> {
let mut guard = self.0.lock().expect("SharedCommandQueue poisoned");
if guard.is_empty() {
Vec::new()
} else {
std::mem::take(&mut *guard)
}
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisCommandInvoked {
pub view: Entity,
pub name: String,
pub parameter: Option<String>,
}
pub struct CommandForwarder {
view: Entity,
name: String,
queue: SharedCommandQueue,
enabled: Arc<AtomicBool>,
}
impl CommandForwarder {
#[must_use]
pub fn new(
view: Entity,
name: String,
queue: SharedCommandQueue,
enabled: Arc<AtomicBool>,
) -> Self {
Self {
view,
name,
queue,
enabled,
}
}
}
impl CommandHandler for CommandForwarder {
fn can_execute(&self, _param: CommandParameterValue) -> bool {
self.enabled.load(Ordering::Relaxed)
}
fn execute(&self, param: CommandParameterValue) {
self.queue
.push(self.view, self.name.clone(), decode_command_param(¶m));
}
}
fn decode_command_param(param: &CommandParameterValue) -> Option<String> {
if param.is_none() {
return None;
}
if let Some(s) = param.as_str() {
return Some(s.to_owned());
}
if let Some(i) = param.as_i32() {
return Some(i.to_string());
}
if let Some(f) = param.as_f64() {
return Some(f.to_string());
}
if let Some(b) = param.as_bool() {
return Some(b.to_string());
}
None
}
struct NoCommandChanges;
impl PropertyChangeHandler for NoCommandChanges {
fn on_changed(&self, _instance: Instance, _prop_index: u32, _value: PropertyValue<'_>) {}
}
pub(crate) struct CommandEntry {
instance: ClassInstance,
_registration: ClassRegistration,
commands: Vec<Command>,
enabled: Vec<Arc<AtomicBool>>,
names: Vec<String>,
target: AttachTarget,
attached_for_uri: Option<String>,
}
impl CommandEntry {
pub(crate) fn build(
view: Entity,
def: &CommandsDef,
queue: &SharedCommandQueue,
) -> Option<Self> {
let names: Vec<String> = def.commands.clone();
let mut builder =
ClassBuilder::new(&def.class_name, ClassBase::ContentControl, NoCommandChanges);
for name in &names {
builder.add_property(name, PropType::BaseComponent);
}
let registration = builder.register()?;
let instance = registration.create_instance()?;
let mut commands = Vec::with_capacity(names.len());
let mut enabled = Vec::with_capacity(names.len());
for (idx, name) in names.iter().enumerate() {
let flag = Arc::new(AtomicBool::new(true));
let forwarder =
CommandForwarder::new(view, name.clone(), queue.clone(), Arc::clone(&flag));
let command = Command::new(forwarder);
instance.handle().set_command(idx as u32, &command);
commands.push(command);
enabled.push(flag);
}
Some(Self {
instance,
_registration: registration,
commands,
enabled,
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 set_enabled(&self, name: &str, value: bool) -> bool {
let Some(idx) = self.names.iter().position(|n| n == name) else {
return false;
};
self.enabled[idx].store(value, Ordering::Relaxed);
self.commands[idx].raise_can_execute_changed();
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_commands(
mut views: Query<(Entity, &mut NoesisCommands)>,
queue: Res<SharedCommandQueue>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, mut cmds) in &mut views {
state.ensure_commands(entity, cmds.def(), &queue);
let enables = cmds.take_pending_enables();
if !enables.is_empty() {
state.apply_command_enables_for(entity, &enables);
}
}
state.attach_commands();
}
#[allow(clippy::needless_pass_by_value)]
pub fn drain_command_queue(
queue: Res<SharedCommandQueue>,
mut messages: MessageWriter<NoesisCommandInvoked>,
) {
for (view, name, parameter) in queue.drain() {
messages.write(NoesisCommandInvoked {
view,
name,
parameter,
});
}
}
pub struct NoesisCommandsPlugin;
impl Plugin for NoesisCommandsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(SharedCommandQueue::default())
.add_message::<NoesisCommandInvoked>()
.add_systems(PreUpdate, drain_command_queue)
.add_systems(PostUpdate, sync_commands.in_set(NoesisSet::Apply));
}
}