use crate::core::ObjectId;
use crate::data_binding::{Binding, BindingListener, BoxedListener};
use crate::event::queue::BlockingQueue;
use crate::event::Event;
use super::engine::{View, ViewEngine};
use super::node::Node;
use super::row_signal::RowSignal;
pub const REACTIVE_PUMP_EVENT: &str = "__reactive_pump";
pub fn reactive_pump_event() -> Event {
Event::Custom { name: REACTIVE_PUMP_EVENT.into(), payload: crate::compat::Vec::new() }
}
struct Changed;
pub struct ReactiveHost<V: View> {
view: V,
engine: ViewEngine,
create: Box<dyn Fn(&Node) -> Option<ObjectId>>,
changed: std::sync::Arc<BlockingQueue<Changed>>,
rows: std::sync::Arc<BlockingQueue<RowSignal>>,
observed: std::sync::Arc<std::sync::atomic::AtomicUsize>,
idle_pending: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
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()),
rows: std::sync::Arc::new(BlockingQueue::new()),
observed: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
idle_pending: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
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);
let idle_pending = std::sync::Arc::clone(&self.idle_pending);
binding.subscribe(
&key,
Box::new(FnChanged { queue, observed, idle_pending }) 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;
}
if rebuilds > 0 {
self.idle_pending.store(false, std::sync::atomic::Ordering::SeqCst);
}
rebuilds
}
pub fn pump_idle(&mut self) -> bool {
let mut applied = false;
while self.changed.try_pop().is_some() {
applied = true;
}
if applied {
self.engine.update(&self.view, self.create.as_ref());
}
self.idle_pending.store(false, std::sync::atomic::Ordering::SeqCst);
applied
}
pub fn pump_if(&mut self, event: &Event) -> bool {
match event {
Event::Custom { name, .. } if name.as_ref() == REACTIVE_PUMP_EVENT => {
self.pump_idle();
true
}
_ => false,
}
}
pub fn publish_rows(&self, signal: RowSignal) -> Result<(), crate::event::queue::QueueError> {
let result = self.rows.push(signal);
if result.is_ok() {
self.idle_pending.store(true, std::sync::atomic::Ordering::SeqCst);
}
result
}
pub fn idle_flag(&self) -> std::sync::Arc<std::sync::atomic::AtomicBool> {
std::sync::Arc::clone(&self.idle_pending)
}
pub fn pump_rows(&mut self) -> usize {
let mut updates = 0usize;
while let Some(signal) = self.rows.try_pop() {
self.engine.update_rows(&self.view, self.create.as_ref(), &signal);
updates += 1;
}
if !self.changed.is_empty() || !self.rows.is_empty() {
self.idle_pending.store(true, std::sync::atomic::Ordering::SeqCst);
} else {
self.idle_pending.store(false, std::sync::atomic::Ordering::SeqCst);
}
updates
}
pub fn has_pending(&self) -> bool {
!self.changed.is_empty() || !self.rows.is_empty()
}
pub fn resize(&mut self, size: crate::core::Size) -> bool {
if !self.engine.set_viewport(size) {
return false;
}
self.engine.update(&self.view, self.create.as_ref());
true
}
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>,
idle_pending: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl BindingListener for FnChanged {
fn on_value_changed(&mut self, _key: &str, _operation: &str) {
self.observed.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.idle_pending.store(true, 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");
}
struct Rows {
count: Rc<Cell<usize>>,
}
impl View for Rows {
fn build(&self) -> Node {
let mut rows = Node::new("panel").key("rows");
for index in 0..self.count.get() {
rows = rows.child(
Node::new("label")
.key(format!("row{index}"))
.prop("text", CapabilityValue::String(format!("r{index}"))),
);
}
Node::new("panel").key("root").child(rows)
}
}
#[test]
fn the_idle_pump_coalesces_a_burst_into_one_rebuild() {
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(5);
for n in 0..5 {
binding.set(n);
}
assert_eq!(host.notifications(), 5, "every set was observed");
assert!(host.pump_idle(), "the queued work is applied");
assert_eq!(host.notifications(), 5, "coalescing does not drop observations");
assert_eq!(text_of(id).as_deref(), Some("v5"), "the latest state wins");
assert!(!host.pump_idle(), "an empty idle phase does nothing");
}
#[test]
fn the_idle_event_is_recognised_and_other_events_are_not() {
let value = Rc::new(Cell::new(0));
let mut host = ReactiveHost::new(Counter { value }, Box::new(creator()));
host.mount();
assert!(!host.pump_if(&crate::event::Event::Paint));
assert!(host.pump_if(&reactive_pump_event()));
}
#[test]
fn a_row_signal_updates_only_its_container() {
let count = Rc::new(Cell::new(2usize));
let mut host = ReactiveHost::new(Rows { count: Rc::clone(&count) }, Box::new(creator()));
host.mount();
assert!(host.engine().id_at(&[0, 0]).is_some(), "the first row mounted");
count.set(3);
host.publish_rows(
RowSignal::new("rows").push(crate::view::RowChange::Insert { at: 2, count: 1 }),
)
.expect("the queue accepts the signal");
assert_eq!(host.pump_rows(), 1);
assert!(
host.engine().id_at(&[0, 2]).is_some(),
"the appended row was inserted by the scoped diff"
);
assert!(!host.has_pending());
}
#[test]
fn a_resize_rebuilds_only_on_a_tier_change() {
let count = Rc::new(Cell::new(1usize));
let mut host = ReactiveHost::new(Rows { count }, Box::new(creator()));
host.mount();
assert!(host.resize(crate::core::Size::new(700, 800)), "compact is a new tier");
assert!(!host.resize(crate::core::Size::new(690, 800)), "same tier, no rebuild");
assert!(host.resize(crate::core::Size::new(900, 800)), "medium is a new tier");
}
#[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");
}
}