use crate::node_id::NodeId;
use std::any::Any;
use std::cell::RefCell;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReactiveFlags {
pub repaint: bool,
pub layout: bool,
pub init: bool,
pub always_update: bool,
}
impl Default for ReactiveFlags {
fn default() -> Self {
Self {
repaint: true,
layout: false,
init: true,
always_update: false,
}
}
}
impl ReactiveFlags {
pub const fn reactive() -> Self {
Self {
repaint: true,
layout: false,
init: true,
always_update: false,
}
}
pub const fn reactive_layout() -> Self {
Self {
repaint: true,
layout: true,
init: true,
always_update: false,
}
}
pub const fn reactive_no_init() -> Self {
Self {
repaint: true,
layout: false,
init: false,
always_update: false,
}
}
pub const fn reactive_layout_no_init() -> Self {
Self {
repaint: true,
layout: true,
init: false,
always_update: false,
}
}
pub const fn var() -> Self {
Self {
repaint: false,
layout: false,
init: false,
always_update: false,
}
}
pub const fn reactive_always_update() -> Self {
Self {
repaint: true,
layout: false,
init: true,
always_update: true,
}
}
}
pub struct ReactiveChange {
pub field_name: &'static str,
pub flags: ReactiveFlags,
pub old_value: Box<dyn Any + Send>,
pub new_value: Box<dyn Any + Send>,
}
impl std::fmt::Debug for ReactiveChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReactiveChange")
.field("field_name", &self.field_name)
.field("flags", &self.flags)
.field("old_value", &"<type-erased>")
.field("new_value", &"<type-erased>")
.finish()
}
}
pub struct ReactiveCtx {
node_id: NodeId,
changes: Vec<ReactiveChange>,
repaint_requested: bool,
layout_requested: bool,
}
impl ReactiveCtx {
pub fn new(node_id: NodeId) -> Self {
Self {
node_id,
changes: Vec::new(),
repaint_requested: false,
layout_requested: false,
}
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
pub fn changes(&self) -> &[ReactiveChange] {
&self.changes
}
pub fn take_changes(&mut self) -> Vec<ReactiveChange> {
std::mem::take(&mut self.changes)
}
pub fn record_change(
&mut self,
field_name: &'static str,
flags: ReactiveFlags,
old_value: Box<dyn Any + Send>,
new_value: Box<dyn Any + Send>,
) {
if flags.repaint {
self.repaint_requested = true;
}
if flags.layout {
self.layout_requested = true;
}
self.changes.push(ReactiveChange {
field_name,
flags,
old_value,
new_value,
});
}
pub fn needs_repaint(&self) -> bool {
self.repaint_requested
}
pub fn needs_layout(&self) -> bool {
self.layout_requested
}
pub fn has_changes(&self) -> bool {
!self.changes.is_empty()
}
pub fn reset_flags(&mut self) {
self.repaint_requested = false;
self.layout_requested = false;
}
pub fn clear_flags(&mut self) {
self.repaint_requested = false;
self.layout_requested = false;
}
}
impl std::fmt::Debug for ReactiveCtx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReactiveCtx")
.field("node_id", &self.node_id)
.field("changes", &self.changes.len())
.field("repaint_requested", &self.repaint_requested)
.field("layout_requested", &self.layout_requested)
.finish()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ReactiveFieldDescriptor {
pub name: &'static str,
pub flags: ReactiveFlags,
}
pub trait ReactiveWidget {
fn reactive_dispatch(&mut self, _changes: &[ReactiveChange], _ctx: &mut ReactiveCtx) {
}
fn reactive_field_descriptors(&self) -> &'static [ReactiveFieldDescriptor] {
&[]
}
}
pub const MAX_REACTIVE_ITERATIONS: usize = 100;
#[derive(Debug, Default)]
pub struct ReactivePhaseResult {
pub had_changes: bool,
pub needs_repaint: bool,
pub needs_layout: bool,
pub iterations: usize,
pub cycle_detected: bool,
}
pub fn run_reactive_phase(
widget: &mut dyn ReactiveWidget,
ctx: &mut ReactiveCtx,
) -> ReactivePhaseResult {
run_reactive_phase_with_dispatch(ctx, |changes, dispatch_ctx| {
widget.reactive_dispatch(changes, dispatch_ctx);
})
}
pub fn run_reactive_phase_with_dispatch(
ctx: &mut ReactiveCtx,
mut dispatch: impl FnMut(&[ReactiveChange], &mut ReactiveCtx),
) -> ReactivePhaseResult {
let mut result = ReactivePhaseResult::default();
for iteration in 0..MAX_REACTIVE_ITERATIONS {
if !ctx.has_changes() {
break;
}
result.had_changes = true;
result.iterations = iteration + 1;
if ctx.needs_repaint() {
result.needs_repaint = true;
}
if ctx.needs_layout() {
result.needs_layout = true;
}
let changes = ctx.take_changes();
ctx.clear_flags();
dispatch(&changes, ctx);
}
if ctx.has_changes() {
result.cycle_detected = true;
result.iterations = MAX_REACTIVE_ITERATIONS;
crate::debug::debug_render(&format!(
"[reactive] cycle detected: {} iterations exceeded for node {:?}",
MAX_REACTIVE_ITERATIONS,
ctx.node_id()
));
let _ = ctx.take_changes();
ctx.clear_flags();
}
if ctx.needs_repaint() {
result.needs_repaint = true;
}
if ctx.needs_layout() {
result.needs_layout = true;
}
result
}
pub struct RuntimeReactiveEntry {
node_id: NodeId,
ctx: ReactiveCtx,
}
impl RuntimeReactiveEntry {
pub fn new(node_id: NodeId, ctx: ReactiveCtx) -> Self {
Self { node_id, ctx }
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
pub fn run_with_dispatch(
&mut self,
dispatch: impl FnMut(&[ReactiveChange], &mut ReactiveCtx),
) -> ReactivePhaseResult {
run_reactive_phase_with_dispatch(&mut self.ctx, dispatch)
}
pub fn run_without_dispatch(&mut self) -> ReactivePhaseResult {
run_reactive_phase_with_dispatch(&mut self.ctx, |_changes, _ctx| {})
}
}
thread_local! {
static RUNTIME_REACTIVE_QUEUE: RefCell<Vec<RuntimeReactiveEntry>> = const { RefCell::new(Vec::new()) };
}
pub fn enqueue_runtime_reactive_entry(entry: RuntimeReactiveEntry) {
RUNTIME_REACTIVE_QUEUE.with(|queue| queue.borrow_mut().push(entry));
}
pub fn take_runtime_reactive_entries() -> Vec<RuntimeReactiveEntry> {
RUNTIME_REACTIVE_QUEUE.with(|queue| std::mem::take(&mut *queue.borrow_mut()))
}
#[cfg(test)]
mod tests {
use super::*;
use slotmap::SlotMap;
fn make_node_id() -> NodeId {
let mut sm: SlotMap<NodeId, ()> = SlotMap::new();
sm.insert(())
}
#[test]
fn reactive_flags_defaults() {
let flags = ReactiveFlags::default();
assert!(flags.repaint);
assert!(!flags.layout);
assert!(flags.init);
}
#[test]
fn reactive_flags_reactive() {
let flags = ReactiveFlags::reactive();
assert!(flags.repaint);
assert!(!flags.layout);
assert!(flags.init);
}
#[test]
fn reactive_flags_reactive_layout() {
let flags = ReactiveFlags::reactive_layout();
assert!(flags.repaint);
assert!(flags.layout);
assert!(flags.init);
}
#[test]
fn reactive_flags_var() {
let flags = ReactiveFlags::var();
assert!(!flags.repaint);
assert!(!flags.layout);
assert!(!flags.init);
}
#[test]
fn ctx_new_is_empty() {
let id = make_node_id();
let ctx = ReactiveCtx::new(id);
assert_eq!(ctx.node_id(), id);
assert!(ctx.changes().is_empty());
assert!(!ctx.needs_repaint());
assert!(!ctx.needs_layout());
assert!(!ctx.has_changes());
}
#[test]
fn ctx_record_change_sets_repaint() {
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
ctx.record_change(
"label",
ReactiveFlags::reactive(),
Box::new("old".to_string()),
Box::new("new".to_string()),
);
assert!(ctx.needs_repaint());
assert!(!ctx.needs_layout());
assert!(ctx.has_changes());
assert_eq!(ctx.changes().len(), 1);
assert_eq!(ctx.changes()[0].field_name, "label");
}
#[test]
fn ctx_record_change_sets_layout() {
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
ctx.record_change(
"size",
ReactiveFlags::reactive_layout(),
Box::new(10_usize),
Box::new(20_usize),
);
assert!(ctx.needs_repaint());
assert!(ctx.needs_layout());
}
#[test]
fn ctx_var_change_no_repaint() {
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
ctx.record_change(
"counter",
ReactiveFlags::var(),
Box::new(0_u32),
Box::new(1_u32),
);
assert!(!ctx.needs_repaint());
assert!(!ctx.needs_layout());
assert!(ctx.has_changes());
}
#[test]
fn ctx_take_changes_drains() {
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
ctx.record_change(
"a",
ReactiveFlags::reactive(),
Box::new(0_i32),
Box::new(1_i32),
);
ctx.record_change(
"b",
ReactiveFlags::reactive(),
Box::new(false),
Box::new(true),
);
let changes = ctx.take_changes();
assert_eq!(changes.len(), 2);
assert!(ctx.changes().is_empty());
assert!(ctx.needs_repaint());
}
#[test]
fn ctx_clear_flags() {
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
ctx.record_change(
"x",
ReactiveFlags::reactive_layout(),
Box::new(0_i32),
Box::new(1_i32),
);
assert!(ctx.needs_repaint());
assert!(ctx.needs_layout());
ctx.clear_flags();
assert!(!ctx.needs_repaint());
assert!(!ctx.needs_layout());
}
#[test]
fn ctx_multiple_changes_accumulate() {
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
ctx.record_change(
"counter",
ReactiveFlags::var(),
Box::new(0_u32),
Box::new(1_u32),
);
assert!(!ctx.needs_repaint());
ctx.record_change(
"label",
ReactiveFlags::reactive(),
Box::new("a".to_string()),
Box::new("b".to_string()),
);
assert!(ctx.needs_repaint());
assert_eq!(ctx.changes().len(), 2);
}
#[test]
fn change_old_new_downcast() {
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
ctx.record_change(
"value",
ReactiveFlags::reactive(),
Box::new(42_i32),
Box::new(99_i32),
);
let change = &ctx.changes()[0];
assert_eq!(*change.old_value.downcast_ref::<i32>().unwrap(), 42);
assert_eq!(*change.new_value.downcast_ref::<i32>().unwrap(), 99);
}
#[test]
fn reactive_widget_default_is_noop() {
struct Dummy;
impl ReactiveWidget for Dummy {}
let mut dummy = Dummy;
let id = make_node_id();
let mut ctx = ReactiveCtx::new(id);
dummy.reactive_dispatch(&[], &mut ctx);
}
#[test]
fn change_debug_impl() {
let change = ReactiveChange {
field_name: "test",
flags: ReactiveFlags::reactive(),
old_value: Box::new(1_i32),
new_value: Box::new(2_i32),
};
let debug_str = format!("{:?}", change);
assert!(debug_str.contains("test"));
assert!(debug_str.contains("type-erased"));
}
#[test]
fn reactive_flags_always_update() {
let flags = ReactiveFlags::reactive_always_update();
assert!(flags.repaint);
assert!(!flags.layout);
assert!(flags.init);
assert!(flags.always_update);
}
#[test]
fn reactive_flags_default_not_always_update() {
assert!(!ReactiveFlags::reactive().always_update);
assert!(!ReactiveFlags::var().always_update);
assert!(!ReactiveFlags::reactive_layout().always_update);
}
}