use std::hash::Hash;
use pipe::PipeNode;
use smallvec::{SmallVec, smallvec};
use crate::{pipe::GenRange, prelude::*, ticker::FrameMsg, window::WindowId};
#[derive(Default, Clone)]
pub struct Classes {
pub(crate) store: ahash::HashMap<ClassName, ClassImpl>,
}
#[macro_export]
macro_rules! style_class {
($($field: ident: $value: expr),* $(,)?) => {
(move |widget: $crate::prelude::Widget| {
rdl! {
@FatObj {
$( $field: $value, )*
@ { widget }
}
}.into_widget()
}) as $crate::prelude::ClassImpl
};
}
#[macro_export]
macro_rules! named_style_impl {
($(#[$meta:meta])* $style_name:ident => {
$($field:ident: $value:expr),* $(,)?
}) => {
$(#[$meta])*
fn $style_name(widget: $crate::prelude::Widget) -> $crate::prelude::Widget {
rdl! {
@FatObj {
$( $field: $value, )*
@ { widget }
}.into_widget()
}
}
};
}
#[macro_export]
macro_rules! named_styles_impl {
($( $(#[$meta:meta])* $name:ident => { $($field:ident: $value:expr),* $(,)? } ),* $(,)? ) => {
$(
named_style_impl! {
$(#[$meta])*
$name => { $($field: $value),* }
}
)*
};
}
#[macro_export]
macro_rules! class_multi_impl {
($($class: expr),*) => {
move |mut w: Widget| {
$(w = $class(w);)*
w
}
};
}
#[macro_export]
macro_rules! class_array {
($($class:expr),* $(,)?) => {{
[
$(
$crate::prelude::PipeValue
::<Option<$crate::prelude::ClassName>>
::r_from($class)
),*
]
}};
}
pub fn empty_cls(w: Widget) -> Widget { w }
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct ClassName(&'static str);
pub type ClassImpl = fn(Widget) -> Widget;
#[derive(Default, Clone, PartialEq)]
pub struct Class {
pub class: Option<ClassName>,
}
#[macro_export]
macro_rules! class {
($($t: tt)*) => { fn_widget! { @Class { $($t)* } } };
}
#[macro_export]
macro_rules! class_names {
($(
$(#[$outer:meta])?
$name:ident
),* $(,)?) => {
$(
$(#[$outer])?
pub const $name: ClassName = ClassName::new(stringify!($name));
)*
};
}
impl ClassName {
pub const fn new(name: &'static str) -> Self { ClassName(name) }
fn type_info(&self) -> TypeInfo {
const LAYOUT: std::alloc::Layout = std::alloc::Layout::new::<ClassImpl>();
TypeInfo { name: std::any::type_name::<ClassName>(), pkg_version: self.0, layout: &LAYOUT }
}
fn from_info(info: &TypeInfo) -> Option<Self> {
if info.name == std::any::type_name::<ClassName>() {
Some(Self(info.pkg_version))
} else {
None
}
}
}
impl Classes {
#[inline]
pub fn insert(&mut self, cls: ClassName, f: ClassImpl) -> Option<ClassImpl> {
self.store.insert(cls, f)
}
pub(crate) fn reader_into_provider<R: StateReader<Value = Classes> + Query>(this: R) -> Provider {
Provider::Setup(Box::new(ClassesReaderSetup(this)))
}
fn remove_intersects_class(&self, map: &mut ProviderCtx) -> Vec<(TypeInfo, Box<dyn Query>)> {
map.remove_key_value_if(|info| {
ClassName::from_info(info).is_some_and(|name| self.store.contains_key(&name))
})
}
}
struct ClassesReaderSetup<T>(T);
struct ClassesRestore {
overrides: Vec<(TypeInfo, Box<dyn Query>)>,
classes: Box<dyn ProviderRestore>,
}
impl ProviderSetup for Classes {
fn setup(self: Box<Self>, map: &mut ProviderCtx) -> Box<dyn ProviderRestore> {
let overrides = self.remove_intersects_class(map);
let classes = Box::new(Setup::new(*self)).setup(map);
Box::new(ClassesRestore { overrides, classes })
}
}
impl<R: StateReader<Value = Classes> + Query> ProviderSetup for ClassesReaderSetup<R> {
fn setup(self: Box<Self>, map: &mut ProviderCtx) -> Box<dyn ProviderRestore> {
let classes = self.0;
let overrides = classes.read().remove_intersects_class(map);
let classes = Box::new(Setup::from_state(classes)).setup(map);
Box::new(ClassesRestore { overrides, classes })
}
}
impl ProviderRestore for ClassesRestore {
fn restore(self: Box<Self>, map: &mut ProviderCtx) -> Box<dyn ProviderSetup> {
let setup = self.classes.restore(map);
for (info, provider) in self.overrides {
let _old = map.set_raw_provider(info, provider);
debug_assert!(_old.is_none());
}
setup
}
}
impl Declare for Class {
type Builder = FatObj<()>;
#[inline]
fn declarer() -> Self::Builder { FatObj::new(()) }
}
impl<'c> ComposeChild<'c> for Class {
type Child = Widget<'c>;
fn compose_child(this: impl StateWriter<Value = Self>, child: Self::Child) -> Widget<'c> {
let f = move || match this.try_into_value() {
Ok(c) => c.apply_style(child),
Err(this) => {
let ctx = BuildCtx::get();
let this2 = this.clone_watcher();
let dummy = GenRange::Single(ctx.tree().dummy_id());
let cls_child = ClassNode::empty_node(dummy.clone());
let orig_child = ClassNode::empty_node(dummy);
let orig_child2 = orig_child.clone();
let child = child.on_build(move |orig_id| orig_child2.init_for_single(orig_id));
let cls_child2 = cls_child.clone();
let orig_child2 = orig_child.clone();
let wnd_id = ctx.window().id();
let sampler = ctx
.window()
.frame_tick_stream()
.filter(|msg| matches!(msg, FrameMsg::NewFrame(_)));
let u = this2
.raw_modifies()
.filter(|s| s.contains(ModifyEffect::FRAMEWORK))
.merge(Local::of(ModifyInfo::default()))
.map(move |_| this2.read().clone())
.distinct_until_changed()
.skip(1)
.sample(sampler)
.subscribe(move |class| class_update(&cls_child2, &orig_child2, &class, wnd_id))
.unsubscribe_when_dropped();
this
.read()
.apply_style(child)
.on_build(move |child_id| cls_child.init_for_single(child_id))
.attach_anonymous_data(u)
}
};
FnWidget::new(f).into_widget()
}
}
impl<'w, const M: usize> ComposeChild<'w> for [PipeValue<Option<ClassName>>; M] {
type Child = Widget<'w>;
fn compose_child(this: impl StateWriter<Value = Self>, mut widget: Self::Child) -> Widget<'w> {
let this = this
.try_into_value()
.unwrap_or_else(|_| panic!("Class array only supports stateless."));
for cls in this.into_iter().rev() {
widget = match cls {
PipeValue::Value(class) => Class { class }.with_child(widget).into_widget(),
cls @ PipeValue::Pipe { .. } => {
let mut widget = FatObj::new(widget);
widget.with_class(cls);
widget.into_widget()
}
};
}
widget
}
}
impl Class {
pub fn provider(name: ClassName, cls_impl: ClassImpl) -> Provider {
let setup = Setup::custom(name.type_info(), Box::new(Queryable(cls_impl)));
Provider::Setup(Box::new(setup))
}
fn apply_style<'a>(&self, w: Widget<'a>) -> Widget<'a> {
if let Some(cls_impl) = self.class_impl() { cls_impl(w) } else { w }
}
fn class_impl(&self) -> Option<ClassImpl> {
let cls = self.class?;
let ctx = BuildCtx::get();
let override_cls = ctx
.as_ref()
.get_raw_provider(&cls.type_info())
.and_then(|q| q.query(&QueryId::of::<ClassImpl>()))
.and_then(QueryHandle::into_ref::<ClassImpl>)
.map(|i| *i);
override_cls.or_else(|| {
Provider::of::<Classes>(ctx)?
.store
.get(&cls)
.copied()
})
}
}
type ClassNode = PipeNode;
fn class_update(node: &ClassNode, orig: &ClassNode, class: &Class, wnd_id: WindowId) {
let wnd =
AppCtx::get_window(wnd_id).expect("This handle is not valid because the window is closed");
let child_id = node.dyn_info().host_id();
let orig_id = orig.dyn_info().host_id();
if child_id.is_dropped(wnd.tree()) {
return;
}
let child_holder = child_id.place_holder(wnd.tree_mut());
let old_child_node = node.take_data();
let _guard = BuildCtx::init_for(child_id, wnd.tree);
let ctx = BuildCtx::get_mut();
let class_node =
std::mem::replace(child_id.get_node_mut(ctx.tree_mut()).unwrap(), old_child_node);
*orig_id.get_node_mut(ctx.tree_mut()).unwrap() = Box::new(orig.clone());
let new_id = ctx.build(class.apply_style(Widget::from_id(orig_id)));
let tree = ctx.tree_mut();
if child_id != new_id {
class_node.update_track_id(new_id);
}
new_id.wrap_node(tree, |render| {
node.replace_data(render);
class_node
});
if new_id != child_id {
new_id
.query_all_iter::<PipeNode>(tree)
.for_each(|node| node.dyn_info_mut().replace(child_id, new_id));
child_holder.replace(new_id, tree);
}
if orig_id != child_id {
child_id.dispose_subtree(tree);
}
let mut stack: SmallVec<[WidgetId; 1]> = smallvec![new_id];
while let Some(w) = stack.pop() {
if w != child_id {
w.on_mounted_subtree(tree);
stack.extend(w.children(tree).rev());
}
}
node.dyn_info_mut().gen_range = GenRange::Single(new_id);
let marker = tree.dirty_marker();
marker.mark(new_id, DirtyPhase::Layout);
if new_id != orig_id && new_id.ancestor_of(orig_id, tree) {
marker.mark(orig_id, DirtyPhase::Layout);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{reset_test_env, test_helper::*};
class_names!(MARGIN, BOX_200, CLAMP_50, EMPTY);
use smallvec::smallvec;
fn initd_classes() -> Classes {
let mut classes = Classes::default();
classes.insert(MARGIN, style_class!(margin: EdgeInsets::all(10.)));
classes.insert(BOX_200, |w| {
fn_widget! {
@MockBox {
size: Size::new(200., 200.),
on_mounted: |_| {println!("mounted");},
@ { w }
}
}
.into_widget()
});
classes.insert(
CLAMP_50,
style_class! {
clamp: BoxClamp::fixed_size(Size::new(50., 50.))
},
);
classes
}
impl Classes {
fn into_provider(self) -> Provider { Provider::new(self) }
}
#[test]
fn switch_class() {
reset_test_env!();
let (cls, w_cls) = split_value(MARGIN);
let wnd = TestWindow::from_widget(fn_widget! {
let cls = cls.clone_watcher();
@Providers {
providers: smallvec![initd_classes().into_provider()],
@Container {
size: Size::new(100., 100.),
class: pipe!(*$read(cls)),
}
}
});
wnd.draw_frame();
wnd.assert_root_size(Size::splat(120.));
*w_cls.write() = BOX_200;
wnd.draw_frame();
wnd.assert_root_size(Size::splat(200.));
*w_cls.write() = MARGIN;
wnd.draw_frame();
wnd.assert_root_size(Size::splat(120.));
}
#[test]
fn switch_class_many() {
for i in 0..10 {
println!("{i}");
switch_class();
}
}
#[test]
fn on_disposed_of_class_nodes() {
reset_test_env!();
class_names!(ON_DISPOSED);
let (cls, w_cls) = split_value(ON_DISPOSED);
static mut DISPOSED: bool = false;
let wnd = TestWindow::from_widget(fn_widget! {
let cls = cls.clone_watcher();
let mut classes = initd_classes();
classes.insert(ON_DISPOSED, move |w| {
fn_widget! {
@MockBox {
size: Size::zero(),
on_disposed: move |_| unsafe { DISPOSED = true },
@ { w }
}
}
.into_widget()
});
@Providers {
providers: smallvec![classes.into_provider()],
@Container {
size: Size::new(100., 100.),
class: pipe!(*$read(cls)),
}
}
});
wnd.draw_frame();
assert!(unsafe { !DISPOSED });
*w_cls.write() = MARGIN;
wnd.draw_frame();
assert!(unsafe { DISPOSED });
}
#[test]
fn class_array() {
reset_test_env!();
let wnd = TestWindow::from_widget(fn_widget! {
let margin_and_clamp = class_array![MARGIN, CLAMP_50];
@Providers {
providers: smallvec![initd_classes().into_provider()],
@(margin_and_clamp) {
@Container {
size: Size::new(100., 100.),
}
}
}
});
wnd.draw_frame();
wnd.assert_root_size(Size::new(70., 70.));
}
#[test]
fn fix_crash_for_class_impl_may_have_multi_child() {
reset_test_env!();
class_names!(MULTI);
let (cls, w_cls) = split_value(MARGIN);
let wnd = TestWindow::from_widget(fn_widget! {
let cls = cls.clone_watcher();
let mut classes = initd_classes();
classes.insert(MULTI, |w| {
fn_widget! {
@MockMulti {
@MockBox { size: Size::new(100., 100.) }
@MockBox { size: Size::new(100., 200.) }
@ { w }
}
}
.into_widget()
});
@Providers {
providers: smallvec![classes.into_provider()],
@Container {
size: Size::new(100., 100.),
class: pipe!(*$read(cls)),
}
}
});
wnd.draw_frame();
wnd.assert_root_size(Size::new(120., 120.));
*w_cls.write() = MULTI;
wnd.draw_frame();
wnd.assert_root_size(Size::new(300., 200.));
}
#[test]
fn fix_provider_in_pipe_class() {
reset_test_env!();
class_names!(PROVIDER_CLS);
let (r_val, w_val) = split_value(-1);
let wnd = TestWindow::from_widget(fn_widget! {
let trigger = Stateful::new(true);
let mut classes = Classes::default();
classes.insert(PROVIDER_CLS, |w| {
Providers::new([Provider::new(0i32)])
.with_child(fn_widget! { w })
});
@Providers {
providers: smallvec![classes.into_provider()],
@Container {
size: Size::new(100., 100.),
class: pipe!($read(trigger); PROVIDER_CLS),
on_performed_layout: move |e| {
*$write(w_val) = *Provider::of::<i32>(e).unwrap();
}
}
}
});
wnd.draw_frame();
assert_eq!(*r_val.read(), 0);
}
#[test]
fn fix_not_mounted_class_node() {
reset_test_env!();
let (cls, w_cls) = split_value(EMPTY);
let wnd = TestWindow::from_widget(fn_widget! {
let cls = cls.clone_watcher();
@Providers {
providers: smallvec![initd_classes().into_provider()],
@Container {
size: Size::new(100., 100.),
class: pipe!(*$read(cls)),
}
}
});
wnd.draw_frame();
wnd.assert_root_size(Size::splat(100.));
*w_cls.write() = BOX_200;
wnd.draw_frame();
wnd.assert_root_size(Size::splat(200.));
}
#[test]
fn fix_style_class_switch() {
reset_test_env!();
let (cls, w_cls) = split_value(EMPTY);
let wnd = TestWindow::from_widget(fn_widget! {
let cls = cls.clone_watcher();
@Providers {
providers: smallvec![initd_classes().into_provider()],
@Container {
size: Size::new(100., 100.),
class: pipe!(*$read(cls)),
}
}
});
wnd.draw_frame();
wnd.assert_root_size(Size::new(100., 100.));
*w_cls.write() = CLAMP_50;
wnd.draw_frame();
wnd.assert_root_size(Size::new(50., 50.));
}
#[test]
fn override_class() {
reset_test_env!();
let wnd = TestWindow::from_widget(fn_widget! {
@Providers {
providers: smallvec![
initd_classes().into_provider(),
Class::provider(MARGIN, style_class!{
clamp: BoxClamp::fixed_size(Size::new(66., 66.))
})
],
@Container {
size: Size::new(100., 100.),
class: MARGIN,
}
}
});
wnd.draw_frame();
wnd.assert_root_size(Size::new(66., 66.));
}
#[test]
fn fix_pipe_class_on_pipe_widget() {
reset_test_env!();
let (w_trigger, w) = split_value(true);
let (cls, w_cls) = split_value(EMPTY);
let wnd = TestWindow::from_widget(fn_widget! {
let w_trigger = w_trigger.clone_watcher();
let cls = cls.clone_watcher();
@Providers {
providers: smallvec![initd_classes().into_provider()],
@ {
let w = pipe!(*$read(w_trigger)).map(|_| fn_widget!{
@Container {size: Size::new(100., 100.) }
});
@Class {
class: pipe!(*$read(cls)),
@ { w }
}
}
}
});
wnd.draw_frame();
*w.write() = false;
wnd.draw_frame();
*w_cls.write() = MARGIN;
wnd.draw_frame();
wnd.assert_root_size(Size::splat(120.));
}
#[test]
fn fix_track_id_in_new_class() {
reset_test_env!();
class_names!(TRACK_ID);
let mut classes = initd_classes();
classes.insert(TRACK_ID, |w| {
let mut w = FatObj::new(w);
rdl! {
@Container {
size: Size::new(100., 100.),
@(w) {
on_performed_layout: move |e| {
let id = $clone(w.track_id()).get().unwrap();
assert!(!id.is_dropped(e.tree()));
}
}
}
}
.into_widget()
});
let (cls, w_cls) = split_value(EMPTY);
let wnd = TestWindow::from_widget(fn_widget! {
let cls = cls.clone_watcher();
@Providers {
providers: smallvec![classes.clone().into_provider()],
@Container {
size: Size::new(100., 100.),
class: pipe!(*$read(cls)),
}
}
});
wnd.draw_frame();
*w_cls.write() = TRACK_ID;
wnd.draw_frame();
}
#[test]
fn fix_pipe_class_in_pipe_class() {
reset_test_env!();
class_names! { PIPE_CLS, INNER_PIPE_A, INNER_PIPE_B };
let (cls, w_cls) = split_value(INNER_PIPE_A);
let (out, w_out) = split_value(EMPTY);
let wnd = TestWindow::from_widget(fn_widget! {
let mut classes = Classes::default();
classes.insert(PIPE_CLS, style_class!{
class: Variant::<ClassName>::new(BuildCtx::get()).unwrap()
});
let out = out.clone_watcher();
let cls = cls.clone_watcher();
providers!{
providers: smallvec![
classes.clone().into_provider(),
Provider::watcher(cls.clone_watcher())
],
@MockBox {
class: pipe!(*$read(out)),
size: Size::new(100., 100.),
}
}
});
*w_out.write() = PIPE_CLS;
wnd.draw_frame();
*w_cls.write() = INNER_PIPE_B;
wnd.draw_frame();
}
#[test]
fn fix_pipe_class_unsubscribed() {
reset_test_env!();
class_names! { OUT_PIPE_CLS, OUT_PIPE_CLS_2, INNER_PIPE, INNER_PIPE_2};
let inner_apply = Stateful::new(0usize);
let w_inner_apply = inner_apply.clone_writer();
let (inner, w_inner) = split_value(false);
let (out, w_out) = split_value(OUT_PIPE_CLS);
let wnd = TestWindow::from_widget(fn_widget! {
let out_cls = Class::provider(OUT_PIPE_CLS, style_class!{
class: Variant::<bool>::new(BuildCtx::get()).unwrap()
.map(|b| if *b { INNER_PIPE } else { INNER_PIPE_2 } )
});
let out_cls_2 = Class::provider(OUT_PIPE_CLS_2, style_class!{
class: Variant::<bool>::new(BuildCtx::get()).unwrap()
.map(|b| if *b { INNER_PIPE } else { INNER_PIPE_2 } )
});
let inner_cls = Class::provider(INNER_PIPE, |w| {
*Provider::write_of::<usize>(BuildCtx::get()).unwrap() += 1;
w
});
let inner_cls_2 = Class::provider(INNER_PIPE_2, |w| {
*Provider::write_of::<usize>(BuildCtx::get()).unwrap() += 1;
w
});
let out = out.clone_watcher();
let inner = inner.clone_watcher();
let w_inner_apply = w_inner_apply.clone_writer();
providers!{
providers: smallvec![
out_cls, out_cls_2, inner_cls, inner_cls_2,
Provider::watcher(inner.clone_watcher()),
Provider::writer(w_inner_apply.clone_writer(), None),
],
@MockBox {
class: pipe!(*$read(out)),
size: Size::new(100., 100.),
}
}
});
wnd.draw_frame();
assert_eq!(*inner_apply.read(), 1);
*w_out.write() = OUT_PIPE_CLS_2;
wnd.draw_frame();
assert_eq!(*inner_apply.read(), 2);
*w_inner.write() = true;
wnd.draw_frame();
assert_eq!(*inner_apply.read(), 3);
}
#[test]
fn fix_track_id_in_class_node() {
reset_test_env!();
class_names! { WRAP_CLS, IDENTITY_CLS };
let (r_cls, w_cls) = split_value(IDENTITY_CLS);
let (r_id, w_id) = split_value(None);
let w = fn_widget! {
let cls = Class::provider(WRAP_CLS, style_class!(
margin: EdgeInsets::all(2.),
));
let mut w = FatObj::new(
@Void {
class: pipe!(*$read(r_cls)),
}.into_widget()
);
*$write(w_id) = Some($clone(w.track_id()));
@Providers{
providers: smallvec![
cls,
],
@ { w }
}
};
let wnd = TestWindow::from_widget(w);
wnd.draw_frame();
let id1 = r_id.read().as_ref().and_then(|w| w.get());
*w_cls.write() = WRAP_CLS;
wnd.draw_frame();
let id2 = r_id.read().as_ref().and_then(|w| w.get());
assert!(id1 != id2);
assert!(!id2.unwrap().is_dropped(wnd.tree()));
}
}