use crate::core::ObjectId;
use crate::data_binding::{Binding, BindingListener, BoxedListener};
use crate::event::queue::BlockingQueue;
use super::engine::{View, ViewEngine};
use super::node::Node;
struct Changed;
pub struct ReactiveHost<V: View> {
view: V,
engine: ViewEngine,
create: Box<dyn Fn(&Node) -> Option<ObjectId>>,
changed: std::sync::Arc<BlockingQueue<Changed>>,
observed: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl<V: View> ReactiveHost<V> {
pub fn new(view: V, create: Box<dyn Fn(&Node) -> Option<ObjectId>>) -> Self {
Self {
view,
engine: ViewEngine::new(),
create,
changed: std::sync::Arc::new(BlockingQueue::new()),
observed: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
pub fn mount(&mut self) -> super::apply::ApplyReport {
self.engine.mount(&self.view, self.create.as_ref())
}
pub fn subscribe<T: Clone + Send + 'static>(&self, binding: &Binding<T>) -> String {
let key = format!("view_host_{:p}", self as *const Self);
let queue = std::sync::Arc::clone(&self.changed);
let observed = std::sync::Arc::clone(&self.observed);
binding.subscribe(&key, Box::new(FnChanged { queue, observed }) as BoxedListener);
key
}
pub fn pump(&mut self) -> usize {
let mut rebuilds = 0usize;
while self.changed.try_pop().is_some() {
self.engine.update(&self.view, self.create.as_ref());
rebuilds += 1;
}
rebuilds
}
pub fn engine(&self) -> &ViewEngine {
&self.engine
}
pub fn engine_mut(&mut self) -> &mut ViewEngine {
&mut self.engine
}
pub fn view_mut(&mut self) -> &mut V {
&mut self.view
}
pub fn notifications(&self) -> usize {
self.observed.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl<V: View> core::fmt::Debug for ReactiveHost<V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ReactiveHost")
.field("pending", &self.changed.len())
.field("notifications", &self.notifications())
.finish_non_exhaustive()
}
}
struct FnChanged {
queue: std::sync::Arc<BlockingQueue<Changed>>,
observed: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl BindingListener for FnChanged {
fn on_value_changed(&mut self, _key: &str, _operation: &str) {
self.observed.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let _ = self.queue.push(Changed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Rect;
use crate::widget::capability::properties_trait::widget_property_get;
use crate::widget::capability::CapabilityValue;
use crate::widget::{runtime, Widget, WidgetFactory};
use std::cell::Cell;
use std::rc::Rc;
struct Counter {
value: Rc<Cell<i64>>,
}
impl View for Counter {
fn build(&self) -> Node {
Node::new("label")
.key("value")
.prop("text", CapabilityValue::String(format!("v{}", self.value.get())))
}
}
fn creator() -> impl Fn(&Node) -> Option<ObjectId> {
let factory = WidgetFactory::new_with_defaults();
move |node: &Node| {
let widget: Box<dyn Widget> = factory.create(
&node.widget,
Rect::new(0, 0, 80, 24),
node.key_str().unwrap_or("anon"),
)?;
runtime::register(widget)
}
}
fn text_of(id: ObjectId) -> Option<String> {
runtime::with_widget(id, |widget| {
widget_property_get(widget, "text").ok().and_then(|value| match value {
CapabilityValue::String(text) => Some(text),
_ => None,
})
})
.flatten()
}
#[test]
fn a_binding_set_queues_and_pump_applies_it() {
let value = Rc::new(Cell::new(0));
let mut host = ReactiveHost::new(Counter { value: Rc::clone(&value) }, Box::new(creator()));
host.mount();
let id = host.engine().id_at(&[]).expect("the label is the root");
assert_eq!(text_of(id).as_deref(), Some("v0"));
value.set(7);
assert_eq!(host.pump(), 0, "nothing was queued, so nothing rebuilds");
let key = host.subscribe(&Binding::new(1i32));
assert!(!key.is_empty());
assert_eq!(host.notifications(), 0, "no set has happened yet");
}
#[test]
fn pump_conducts_one_update_per_set() {
use crate::data_binding::Binding;
let value = Rc::new(Cell::new(0));
let mut host = ReactiveHost::new(Counter { value: Rc::clone(&value) }, Box::new(creator()));
host.mount();
let id = host.engine().id_at(&[]).expect("the label is the root");
let binding = Binding::new(0i64);
host.subscribe(&binding);
value.set(1);
binding.set(1);
value.set(2);
binding.set(2);
assert_eq!(host.pump(), 2, "each queued change must produce one update");
assert_eq!(host.notifications(), 2, "two binding sets, two notifications");
assert_eq!(text_of(id).as_deref(), Some("v2"), "the last state wins");
assert_eq!(host.pump(), 0);
}
#[test]
fn the_queue_preserves_the_producer_count() {
use crate::data_binding::Binding;
let value = Rc::new(Cell::new(0));
let mut host = ReactiveHost::new(Counter { value }, Box::new(creator()));
host.mount();
let binding = Binding::new(0i64);
host.subscribe(&binding);
for n in 0..5 {
binding.set(n);
}
assert_eq!(host.pump(), 5, "five sets must queue five rebuilds");
assert_eq!(host.pump(), 0, "and nothing is left over");
}
#[test]
fn a_closed_or_full_queue_still_counts_the_notification() {
let value = Rc::new(Cell::new(0));
let mut host = ReactiveHost::new(Counter { value }, Box::new(creator()));
host.mount();
let binding = Binding::new(0i32);
host.subscribe(&binding);
host.changed.close();
binding.set(1);
assert_eq!(host.notifications(), 1, "the change was observed");
assert_eq!(host.pump(), 0, "but it could not be queued");
}
#[test]
fn a_worker_thread_set_reaches_the_ui_thread() {
use crate::data_binding::Binding;
use std::sync::Barrier;
let value = Rc::new(Cell::new(0));
let mut host = ReactiveHost::new(Counter { value: Rc::clone(&value) }, Box::new(creator()));
host.mount();
let id = host.engine().id_at(&[]).expect("the label is the root");
assert_eq!(text_of(id).as_deref(), Some("v0"));
let binding = std::sync::Arc::new(Binding::new(0i64));
host.subscribe(&binding);
let barrier = std::sync::Arc::new(Barrier::new(2));
let worker_binding = std::sync::Arc::clone(&binding);
let worker_barrier = std::sync::Arc::clone(&barrier);
let worker = std::thread::spawn(move || {
worker_binding.set(9);
worker_barrier.wait();
});
barrier.wait();
value.set(9);
let applied = host.pump();
worker.join().expect("the worker must not panic");
assert_eq!(applied, 1, "the worker's set must have queued exactly one rebuild");
assert_eq!(host.notifications(), 1);
assert_eq!(text_of(id).as_deref(), Some("v9"), "the UI thread applied it");
}
}