use std::any::Any;
use std::collections::HashMap;
use plushie_core::protocol::TreeNode;
use plushie_core::tree_walk::{TreeTransform, WalkCtx};
use plushie_core::types::FromNode;
use serde_json::Value;
use crate::View;
use crate::event::Event;
use crate::subscription::Subscription;
pub trait Widget: Send + Sync + 'static {
type State: Default + Send + 'static;
type Props: FromNode;
fn view(id: &str, props: &Self::Props, state: &Self::State) -> View;
fn handle_event(_event: &Event, _state: &mut Self::State) -> EventResult {
EventResult::Ignored
}
fn subscribe(_props: &Self::Props, _state: &Self::State) -> Vec<Subscription> {
vec![]
}
fn cache_key(_props: &Self::Props, _state: &Self::State) -> Option<u64> {
None
}
}
pub fn hash_cache_key<T: std::hash::Hash + ?Sized>(value: &T) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
#[derive(Debug)]
#[non_exhaustive]
pub enum EventResult {
Emit {
family: String,
value: Value,
},
Consumed,
Ignored,
}
impl EventResult {
pub fn emit(family: &str, value: impl Into<Value>) -> Self {
plushie_core::EventType::assert_custom_family(family);
Self::Emit {
family: family.to_string(),
value: value.into(),
}
}
pub fn emit_event(event: impl plushie_core::types::WidgetEventEncode) -> Self {
let (family, value) = event.to_wire();
Self::Emit {
family: family.to_string(),
value: serde_json::Value::from(value),
}
}
}
#[derive(Debug)]
pub struct Interception {
pub result: EventResult,
pub widget_id: String,
pub outer_scope: Vec<String>,
pub window_id: String,
}
pub struct WidgetView<W: Widget> {
id: String,
props: plushie_core::protocol::PropMap,
_marker: std::marker::PhantomData<W>,
}
impl<W: Widget> WidgetView<W> {
pub fn new(id: &str) -> Self {
Self {
id: id.to_string(),
props: plushie_core::protocol::PropMap::new(),
_marker: std::marker::PhantomData,
}
}
pub fn from_builder(builder: plushie_core::WidgetBuilder) -> Self {
Self {
id: builder.id,
props: builder.props,
_marker: std::marker::PhantomData,
}
}
pub fn prop(mut self, key: &str, value: impl Into<plushie_core::protocol::PropValue>) -> Self {
self.props.insert(key, value.into());
self
}
}
impl<W: Widget> WidgetView<W> {
pub fn placeholder(self) -> View {
let mut props = self.props;
props.insert("__widget__", plushie_core::protocol::PropValue::Bool(true));
View::new(
self.id,
"__widget__",
plushie_core::protocol::Props::from(props),
vec![],
)
}
pub fn register(self, registrar: &mut WidgetRegistrar) -> View {
let expander: Box<dyn DynWidgetExpander> =
Box::new(WidgetExpander::<W>(std::marker::PhantomData));
registrar.register(self.id.clone(), expander);
self.placeholder()
}
}
#[allow(dead_code)] pub(crate) trait DynWidgetExpander: Send {
fn expand(&self, id: &str, node: &TreeNode, state: &dyn Any) -> TreeNode;
fn handle_event(&self, event: &Event, state: &mut dyn Any) -> EventResult;
fn default_state(&self) -> Box<dyn Any + Send>;
fn subscribe(&self, node: &TreeNode, state: &dyn Any) -> Vec<Subscription>;
fn state_type_id(&self) -> std::any::TypeId;
fn widget_type_name(&self) -> &'static str;
fn cache_key(&self, node: &TreeNode, state: &dyn Any) -> Option<u64>;
}
struct WidgetExpander<W: Widget>(std::marker::PhantomData<W>);
impl<W: Widget> DynWidgetExpander for WidgetExpander<W> {
fn expand(&self, id: &str, node: &TreeNode, state: &dyn Any) -> TreeNode {
let state = state.downcast_ref::<W::State>().unwrap_or_else(|| {
widget_type_mismatch_panic::<W>(id);
});
let props = W::Props::from_node(node);
W::view(id, &props, state).into_tree_node()
}
fn handle_event(&self, event: &Event, state: &mut dyn Any) -> EventResult {
let state = state.downcast_mut::<W::State>().unwrap_or_else(|| {
widget_type_mismatch_panic::<W>("<event dispatch>");
});
W::handle_event(event, state)
}
fn default_state(&self) -> Box<dyn Any + Send> {
Box::new(W::State::default())
}
fn subscribe(&self, node: &TreeNode, state: &dyn Any) -> Vec<Subscription> {
let state = state.downcast_ref::<W::State>().unwrap_or_else(|| {
widget_type_mismatch_panic::<W>(&node.id);
});
let props = W::Props::from_node(node);
W::subscribe(&props, state)
}
fn state_type_id(&self) -> std::any::TypeId {
std::any::TypeId::of::<W::State>()
}
fn widget_type_name(&self) -> &'static str {
std::any::type_name::<W>()
}
fn cache_key(&self, node: &TreeNode, state: &dyn Any) -> Option<u64> {
let state = state.downcast_ref::<W::State>().unwrap_or_else(|| {
widget_type_mismatch_panic::<W>(&node.id);
});
let props = W::Props::from_node(node);
W::cache_key(&props, state)
}
}
fn widget_type_mismatch_panic<W: Widget>(id: &str) -> ! {
panic!(
"widget state type mismatch: expander for `{}` (id={id:?}) \
received state of the wrong type. This should have been \
caught at registration with a `widget_id_type_collision` \
diagnostic; treat this panic as a bug in WidgetStateStore.",
std::any::type_name::<W>(),
)
}
#[derive(Default)]
pub struct WidgetRegistrar {
expanders: HashMap<String, Box<dyn DynWidgetExpander>>,
}
impl WidgetRegistrar {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn register(&mut self, id: String, expander: Box<dyn DynWidgetExpander>) {
self.expanders.insert(id, expander);
}
pub(crate) fn take_all(self) -> HashMap<String, Box<dyn DynWidgetExpander>> {
self.expanders
}
}
pub(crate) struct WidgetStateStore {
states: HashMap<String, (std::any::TypeId, Box<dyn Any + Send>)>,
expanders: HashMap<String, Box<dyn DynWidgetExpander>>,
}
impl WidgetStateStore {
pub fn new() -> Self {
Self {
states: HashMap::new(),
expanders: HashMap::new(),
}
}
pub(crate) fn register_expander(&mut self, id: String, expander: Box<dyn DynWidgetExpander>) {
let incoming_type = expander.state_type_id();
let incoming_name = expander.widget_type_name();
if let Some((existing_type, _)) = self.states.get(&id) {
if *existing_type != incoming_type {
let existing_name = self
.expanders
.get(&id)
.map(|e| e.widget_type_name())
.unwrap_or("<unknown>");
let diag = plushie_core::Diagnostic::WidgetIdTypeCollision {
id: id.clone(),
existing_type: existing_name.to_string(),
incoming_type: incoming_name.to_string(),
};
log::error!("{diag}");
panic!(
"widget_id_type_collision: ID {id:?} was previously registered as \
`{existing_name}`; cannot reuse it for `{incoming_name}`. Pick a \
unique ID per composite widget type."
);
}
} else {
self.states
.insert(id.clone(), (incoming_type, expander.default_state()));
}
self.expanders.insert(id, expander);
}
pub(crate) fn expand_in_place(
&self,
node: &mut TreeNode,
cache: Option<&mut crate::runtime::widget_view_cache::WidgetViewCache>,
) {
let mut cache = cache;
while node.type_name == "__widget__" {
if let Some(expander) = self.expanders.get(&node.id) {
let (_type_id, state) = self.states.get(&node.id).expect("widget state missing");
if let Some(cache_ref) = cache.as_deref_mut()
&& let Some(key_hash) = expander.cache_key(node, state.as_ref())
{
let widget_id = node.id.clone();
cache_ref.mark_live(&widget_id);
if let Some(cached_view) = cache_ref.get(&widget_id, key_hash) {
*node = cached_view.clone();
continue;
}
let expanded = expander.expand(&widget_id, node, state.as_ref());
cache_ref.insert(widget_id, key_hash, expanded.clone());
*node = expanded;
} else {
let expanded = expander.expand(&node.id, node, state.as_ref());
*node = expanded;
}
} else {
Self::rewrite_unrecognized_placeholder(node);
break;
}
}
}
fn rewrite_unrecognized_placeholder(node: &mut TreeNode) {
let id = std::mem::take(&mut node.id);
plushie_core::diagnostics::warn(plushie_core::Diagnostic::UnrecognizedWidgetPlaceholder {
id: id.clone(),
});
let mut props = plushie_core::protocol::PropMap::new();
props.insert(
"a11y",
plushie_core::protocol::PropValue::from(serde_json::json!({
"label": format!("unregistered widget: {id}"),
"role": "alert",
})),
);
*node = TreeNode {
id,
type_name: "container".to_string(),
props: plushie_core::protocol::Props::from(props),
children: Vec::new(),
};
}
pub fn intercept_event(&mut self, event: &Event) -> Option<Interception> {
let scoped_id = match event {
Event::Widget(w) => &w.scoped_id,
_ => return None,
};
let scope = &scoped_id.scope;
let window_id = scoped_id.window_id.clone().unwrap_or_default();
for (i, ancestor_id) in scope.iter().enumerate() {
if let Some(expander) = self.expanders.get(ancestor_id) {
let (_type_id, state) = self.states.get_mut(ancestor_id).expect(
"widget state invariant: expander registered without matching state entry",
);
let result = expander.handle_event(event, state.as_mut());
match result {
EventResult::Ignored => continue,
other => {
return Some(Interception {
result: other,
widget_id: ancestor_id.clone(),
outer_scope: scope[i + 1..].to_vec(),
window_id: window_id.clone(),
});
}
}
}
}
None
}
}
pub(crate) struct ExpandWidgetsTransform<'a> {
store: &'a WidgetStateStore,
cache: Option<&'a mut crate::runtime::widget_view_cache::WidgetViewCache>,
}
impl<'a> ExpandWidgetsTransform<'a> {
pub(crate) fn with_cache(
store: &'a WidgetStateStore,
cache: Option<&'a mut crate::runtime::widget_view_cache::WidgetViewCache>,
) -> Self {
Self { store, cache }
}
}
impl TreeTransform for ExpandWidgetsTransform<'_> {
fn enter(&mut self, node: &mut TreeNode, _ctx: &mut WalkCtx) {
if self.store.expanders.is_empty() {
return;
}
self.store.expand_in_place(node, self.cache.as_deref_mut());
}
}
pub(crate) fn register_expanders(store: &mut WidgetStateStore, registrar: WidgetRegistrar) {
for (id, expander) in registrar.take_all() {
store.register_expander(id, expander);
}
}