use std::collections::HashMap;
use bevy::prelude::*;
use noesis_runtime::binding::ObservableCollection;
use noesis_runtime::classes::{
ClassBuilder, ClassInstance, ClassRegistration, Instance, PropertyChangeHandler, PropertyValue,
};
use noesis_runtime::collection_view::{CollectionView, CollectionViewSource, CurrentItem};
use noesis_runtime::ffi::{ClassBase, PropType};
use noesis_runtime::view::FrameworkElement;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Clone, Debug, PartialEq)]
pub enum ItemValue {
Str(String),
I32(i32),
F64(f64),
Bool(bool),
}
impl From<&str> for ItemValue {
fn from(v: &str) -> Self {
Self::Str(v.to_owned())
}
}
impl From<String> for ItemValue {
fn from(v: String) -> Self {
Self::Str(v)
}
}
impl From<&String> for ItemValue {
fn from(v: &String) -> Self {
Self::Str(v.clone())
}
}
impl From<i32> for ItemValue {
fn from(v: i32) -> Self {
Self::I32(v)
}
}
impl From<f64> for ItemValue {
fn from(v: f64) -> Self {
Self::F64(v)
}
}
impl From<bool> for ItemValue {
fn from(v: bool) -> Self {
Self::Bool(v)
}
}
impl ItemValue {
fn push_into(&self, coll: &mut ObservableCollection) {
match self {
Self::Str(v) => {
coll.push_string(v);
}
Self::I32(v) => {
coll.push_i32(*v);
}
Self::F64(v) => {
coll.push_f64(*v);
}
Self::Bool(v) => {
coll.push_bool(*v);
}
}
}
fn prop_type(&self) -> PropType {
match self {
Self::Str(_) => PropType::String,
Self::I32(_) => PropType::Int32,
Self::F64(_) => PropType::Double,
Self::Bool(_) => PropType::Bool,
}
}
fn set_on(&self, instance: Instance, index: u32) {
match self {
Self::Str(v) => instance.set_string(index, v),
Self::I32(v) => instance.set_int32(index, *v),
Self::F64(v) => instance.set_double(index, *v),
Self::Bool(v) => instance.set_bool(index, *v),
}
}
}
pub type ObjectRow = Vec<(String, ItemValue)>;
#[derive(Clone, Debug)]
pub struct ObjectSource {
pub class_name: String,
pub rows: Vec<ObjectRow>,
}
struct NoopChangeHandler;
impl PropertyChangeHandler for NoopChangeHandler {
fn on_changed(&self, _instance: Instance, _prop_index: u32, _value: PropertyValue<'_>) {}
}
fn current_item_value(item: &CurrentItem) -> Option<ItemValue> {
if let Some(s) = item.as_string() {
return Some(ItemValue::Str(s));
}
if let Some(b) = item.as_bool() {
return Some(ItemValue::Bool(b));
}
if let Some(i) = item.as_i32() {
return Some(ItemValue::I32(i));
}
if let Some(f) = item.as_f64() {
return Some(ItemValue::F64(f));
}
None
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CollectionViewOp {
First,
Last,
Next,
Previous,
To(i32),
}
impl CollectionViewOp {
fn apply(self, view: &CollectionView) -> bool {
match self {
Self::First => view.move_current_to_first(),
Self::Last => view.move_current_to_last(),
Self::Next => view.move_current_to_next(),
Self::Previous => view.move_current_to_previous(),
Self::To(pos) => view.move_current_to_position(pos),
}
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisItems {
pub sources: HashMap<String, Vec<ItemValue>>,
pub select: HashMap<String, i32>,
pub navigate: HashMap<String, CollectionViewOp>,
pub objects: HashMap<String, ObjectSource>,
}
impl NoesisItems {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(
mut self,
name: impl Into<String>,
items: impl IntoIterator<Item = impl Into<ItemValue>>,
) -> Self {
self.sources
.insert(name.into(), items.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn with_items(
mut self,
name: impl Into<String>,
items: impl IntoIterator<Item = ItemValue>,
) -> Self {
self.sources
.insert(name.into(), items.into_iter().collect());
self
}
#[must_use]
pub fn select(mut self, name: impl Into<String>, index: i32) -> Self {
self.select.insert(name.into(), index);
self
}
#[must_use]
pub fn navigate(mut self, name: impl Into<String>, op: CollectionViewOp) -> Self {
self.navigate.insert(name.into(), op);
self
}
#[must_use]
pub fn with_objects(
mut self,
name: impl Into<String>,
class_name: impl Into<String>,
rows: Vec<ObjectRow>,
) -> Self {
self.objects.insert(
name.into(),
ObjectSource {
class_name: class_name.into(),
rows,
},
);
self
}
pub fn set(
&mut self,
name: impl Into<String>,
items: impl IntoIterator<Item = impl Into<ItemValue>>,
) {
self.sources
.insert(name.into(), items.into_iter().map(Into::into).collect());
}
pub fn set_items(
&mut self,
name: impl Into<String>,
items: impl IntoIterator<Item = ItemValue>,
) {
self.sources
.insert(name.into(), items.into_iter().collect());
}
pub fn set_selection(&mut self, name: impl Into<String>, index: i32) {
self.select.insert(name.into(), index);
}
pub fn set_navigation(&mut self, name: impl Into<String>, op: CollectionViewOp) {
self.navigate.insert(name.into(), op);
}
pub fn set_objects(
&mut self,
name: impl Into<String>,
class_name: impl Into<String>,
rows: Vec<ObjectRow>,
) {
self.objects.insert(
name.into(),
ObjectSource {
class_name: class_name.into(),
rows,
},
);
}
}
pub struct ItemsBinding {
coll: ObservableCollection,
cvs: CollectionViewSource,
view: Option<CollectionView>,
bound_for_uri: Option<String>,
desired_select: Option<i32>,
applied_select: Option<i32>,
desired_nav: Option<CollectionViewOp>,
nav_pending: bool,
last_readback: Option<(usize, i32, i32, Option<ItemValue>)>,
obj_instances: Vec<ClassInstance>,
obj_schema: Vec<String>,
obj_registration: Option<ClassRegistration>,
}
impl Default for ItemsBinding {
fn default() -> Self {
Self::new()
}
}
impl ItemsBinding {
#[must_use]
pub fn new() -> Self {
let coll = ObservableCollection::new();
let mut cvs = CollectionViewSource::new();
cvs.set_source(&coll);
let view = cvs.view();
Self {
coll,
cvs,
view,
bound_for_uri: None,
desired_select: None,
applied_select: None,
desired_nav: None,
nav_pending: false,
last_readback: None,
obj_instances: Vec::new(),
obj_schema: Vec::new(),
obj_registration: None,
}
}
pub fn set<I, S>(&mut self, items: I)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.coll.clear();
for item in items {
self.coll.push_string(item.as_ref());
}
self.applied_select = None;
}
pub fn set_typed(&mut self, items: &[ItemValue]) {
self.coll.clear();
for item in items {
item.push_into(&mut self.coll);
}
self.applied_select = None;
}
pub(crate) fn set_objects(&mut self, src: &ObjectSource) {
if self.obj_registration.is_none() {
let Some(first) = src.rows.first() else {
self.coll.clear();
self.obj_instances.clear();
self.applied_select = None;
return;
};
let mut builder =
ClassBuilder::new(&src.class_name, ClassBase::Freezable, NoopChangeHandler);
let mut schema = Vec::with_capacity(first.len());
for (name, value) in first {
builder.add_property(name, value.prop_type());
schema.push(name.clone());
}
match builder.register() {
Some(reg) => {
self.obj_registration = Some(reg);
self.obj_schema = schema;
}
None => {
warn!(
"NoesisItems: failed to register item class {:?} (duplicate name?)",
src.class_name,
);
return;
}
}
}
let Some(reg) = self.obj_registration.as_ref() else {
return;
};
self.coll.clear();
self.obj_instances.clear();
for row in &src.rows {
let Some(instance) = reg.create_instance() else {
continue;
};
let handle = instance.handle();
for (name, value) in row {
if let Some(index) = self.obj_schema.iter().position(|n| n == name) {
value.set_on(handle, index as u32);
}
}
self.coll.push_object(&instance);
self.obj_instances.push(instance);
}
self.applied_select = None;
}
pub fn push(&mut self, item: &str) {
self.coll.push_string(item);
}
pub fn push_value(&mut self, item: &ItemValue) {
item.push_into(&mut self.coll);
}
pub fn remove_at(&mut self, index: usize) {
self.coll.remove_at(index);
}
pub fn clear(&mut self) {
self.coll.clear();
}
#[must_use]
pub fn collection(&self) -> &ObservableCollection {
&self.coll
}
pub(crate) fn set_desired_select(&mut self, index: Option<i32>) {
if self.desired_select != index {
self.desired_select = index;
self.applied_select = None;
}
}
pub(crate) fn set_desired_nav(&mut self, op: Option<CollectionViewOp>) {
self.desired_nav = op;
if op.is_some() {
self.nav_pending = true;
}
}
pub(crate) fn needs_bind(&self, uri: &str) -> bool {
self.bound_for_uri.as_deref() != Some(uri)
}
pub(crate) fn mark_bound(&mut self, uri: &str) {
self.bound_for_uri = Some(uri.to_owned());
}
pub(crate) fn reset_bind(&mut self) {
self.bound_for_uri = None;
self.applied_select = None;
}
fn view(&mut self) -> Option<&CollectionView> {
if self.view.is_none() {
self.view = self.cvs.view();
}
self.view.as_ref()
}
pub(crate) fn drive_selection(&mut self, element: &mut FrameworkElement) {
let Some(index) = self.desired_select else {
return;
};
if self.applied_select == Some(index) {
return;
}
let ok = element.set_selected_index(index);
if let Some(view) = self.view() {
view.move_current_to_position(index);
}
if ok {
self.applied_select = Some(index);
}
}
pub(crate) fn drive_navigation(&mut self) {
if !self.nav_pending {
return;
}
let Some(op) = self.desired_nav else {
self.nav_pending = false;
return;
};
if let Some(view) = self.view() {
op.apply(view);
self.nav_pending = false;
}
}
pub fn navigate(&mut self, op: CollectionViewOp) -> bool {
self.view().is_some_and(|view| op.apply(view))
}
#[must_use]
pub fn current_position(&mut self) -> i32 {
self.view().map_or(-1, CollectionView::current_position)
}
#[must_use]
pub fn current_item_value(&mut self) -> Option<ItemValue> {
self.view()
.and_then(CollectionView::current_item)
.and_then(|item| current_item_value(&item))
}
pub(crate) fn read_changed(
&mut self,
element: &FrameworkElement,
) -> Option<(usize, i32, i32, Option<ItemValue>)> {
let count = element.items_count().unwrap_or(0);
let selected_index = element.selected_index().unwrap_or(-1);
let current_position = self.current_position();
let current = self.current_item_value();
let snap = (count, selected_index, current_position, current);
if self.last_readback.as_ref() == Some(&snap) {
return None;
}
self.last_readback = Some(snap.clone());
Some(snap)
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisItemsCurrent {
pub view: Entity,
pub name: String,
pub count: usize,
pub selected_index: i32,
pub current_position: i32,
pub current: Option<ItemValue>,
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_items_bridge(
views: Query<(Entity, Ref<NoesisItems>)>,
state: Option<NonSendMut<NoesisRenderState>>,
mut current: MessageWriter<NoesisItemsCurrent>,
) {
let Some(mut state) = state else {
return;
};
for (entity, items) in &views {
state.apply_items_for(
entity,
&items.sources,
&items.objects,
&items.select,
&items.navigate,
items.is_changed(),
);
for (name, count, selected_index, current_position, value) in state.poll_items_for(entity) {
current.write(NoesisItemsCurrent {
view: entity,
name,
count,
selected_index,
current_position,
current: value,
});
}
}
}
pub struct NoesisItemsPlugin;
impl Plugin for NoesisItemsPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisItemsCurrent>()
.add_systems(PostUpdate, sync_items_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_sources() {
let i = NoesisItems::new()
.with("Combo", ["a", "b"])
.with("List", vec!["x".to_string()])
.with("Ports", [80, 443])
.select("Combo", 1)
.navigate("Combo", CollectionViewOp::Next);
assert_eq!(
i.sources["Combo"],
vec![ItemValue::Str("a".into()), ItemValue::Str("b".into())],
);
assert_eq!(i.sources["List"], vec![ItemValue::Str("x".into())]);
assert_eq!(
i.sources["Ports"],
vec![ItemValue::I32(80), ItemValue::I32(443)],
);
assert_eq!(i.select["Combo"], 1);
assert_eq!(i.navigate["Combo"], CollectionViewOp::Next);
}
#[test]
fn item_value_conversions() {
assert_eq!(ItemValue::from("s"), ItemValue::Str("s".into()));
assert_eq!(ItemValue::from(3i32), ItemValue::I32(3));
assert_eq!(ItemValue::from(2.5f64), ItemValue::F64(2.5));
assert_eq!(ItemValue::from(true), ItemValue::Bool(true));
}
}