# compose-rt ![Rust](https://github.com/cksac/compose-rt/workflows/Rust/badge.svg) [![Docs Status](https://docs.rs/compose-rt/badge.svg)](https://docs.rs/compose-rt) [![Latest Version](https://img.shields.io/crates/v/compose-rt.svg)](https://crates.io/crates/compose-rt) A positional memoization runtime similar to Jetpack Compose Runtime. >What this means is that Compose is, at its core, a general-purpose tool for managing a tree of nodes of any type. Well a “tree of nodes” describes just about anything, and as a result Compose can target just about anything. – [jakewharton](https://jakewharton.com/a-jetpack-compose-by-any-other-name/) # use cases - Declarative GUI - https://github.com/cksac/oxui, an experiment GUI framework similar to Flutter - Automatic differentiation - Others... # example Below example show how to build a declarative GUI similar to Jetpack Compose UI ```toml [dependencies] compose-rt = "0.12" downcast-rs = "1.2" log = "0.4" env_logger = "0.6" fake = "2.4" ``` ```rust #![allow(non_snake_case)] use compose_rt::{compose, ComposeNode, Composer, Recomposer}; use downcast_rs::impl_downcast; use fake::{Fake, Faker}; use std::{cell::RefCell, fmt::Debug, rc::Rc}; //////////////////////////////////////////////////////////////////////////// // User application //////////////////////////////////////////////////////////////////////////// pub struct Movie { id: usize, name: String, img_url: String, } impl Movie { pub fn new(id: usize, name: impl Into, img_url: impl Into) -> Self { Movie { id, name: name.into(), img_url: img_url.into(), } } } #[compose] pub fn MoviesScreen(movies: &Vec) { Column(cx, |cx| { for movie in movies { cx.tag(movie.id, |cx| MovieOverview(cx, &movie)); } }); } #[compose] pub fn MovieOverview(movie: &Movie) { Column(cx, |cx| { Text(cx, &movie.name); Image(cx, &movie.img_url); RandomRenderObject(cx, &movie.name); let count = cx.remember(|| Rc::new(RefCell::new(0usize))); Text(cx, format!("compose count {}", count.borrow())); *count.borrow_mut() += 1; }); } fn main() { // Setup logging env_logger::Builder::from_default_env() .filter_level(log::LevelFilter::Trace) .init(); // define root compose let root_fn = |cx: &mut Composer, movies| MoviesScreen(cx, movies); let mut recomposer = Recomposer::new(20); // first run let movies = vec![Movie::new(1, "A", "IMG_A"), Movie::new(2, "B", "IMG_B")]; recomposer.compose(|cx| { root_fn(cx, &movies); }); if let Some(root) = recomposer.root::>>() { // call paint of render tree let mut context = PaintContext::new(); root.borrow().paint(&mut context); } // rerun with new input let movies = vec![ Movie::new(1, "AA", "IMG_AA"), Movie::new(3, "C", "IMG_C"), Movie::new(2, "B", "IMG_B"), ]; recomposer.compose(|cx| { root_fn(cx, &movies); }); // end compose, Recomposer allow you to access root if let Some(root) = recomposer.root::>>() { // call paint of render tree let mut context = PaintContext::new(); root.borrow().paint(&mut context); } } //////////////////////////////////////////////////////////////////////////// // Components - Usage of compose-rt //////////////////////////////////////////////////////////////////////////// #[compose(skip_inject_cx = true)] pub fn Column(cx: &mut Composer, content: C) where C: Fn(&mut Composer), { cx.group_use_children( |_| Rc::new(RefCell::new(RenderFlex::new())), |_| false, content, |node, children| { let mut flex = node.borrow_mut(); flex.children.clear(); for child in children.iter() { if let Some(c) = child.cast_ref::>>().cloned() { flex.children.push(c); } else if let Some(c) = child.cast_ref::>>().cloned() { flex.children.push(c); } else if let Some(c) = child.cast_ref::>>().cloned() { flex.children.push(c); } else if let Some(c) = child.cast_ref::>>().cloned() { flex.children.push(c); } } }, |_, _| {}, |_| {}, ); } #[compose(skip_inject_cx = true)] pub fn Text(cx: &mut Composer, text: impl AsRef) { let text = text.as_ref(); cx.memo( |_| Rc::new(RefCell::new(RenderLabel(text.to_string()))), |n| n.borrow().0 == text, |n| { let mut n = n.borrow_mut(); n.0 = text.to_string(); }, |_| {}, ); } #[compose(skip_inject_cx = true)] pub fn Image(cx: &mut Composer, url: impl AsRef) { let url = url.as_ref(); cx.memo( |_| Rc::new(RefCell::new(RenderImage(url.to_string()))), |n| n.borrow().0 == url, |n| { let mut n = n.borrow_mut(); n.0 = url.to_string(); }, |_| {}, ); } #[compose(skip_inject_cx = true)] pub fn RandomRenderObject(cx: &mut Composer, text: impl AsRef) { let t = text.as_ref(); cx.memo( |_| { let obj: Rc> = if Faker.fake::() { let url = format!("http://image.com/{}.png", t); Rc::new(RefCell::new(RenderImage(url))) } else { Rc::new(RefCell::new(RenderLabel(t.to_string()))) }; obj }, |_| false, |n| { if let Some(label) = n.borrow_mut().downcast_mut::() { label.0 = t.to_string(); } if let Some(img) = n.borrow_mut().downcast_mut::() { let url = format!("http://image.com/{}.png", t); img.0 = url; } }, |_| {}, ); } //////////////////////////////////////////////////////////////////////////// // Rendering backend - Not scope of compose-rt //////////////////////////////////////////////////////////////////////////// pub struct PaintContext { depth: usize, } impl PaintContext { pub fn new() -> Self { Self { depth: 0 } } } pub trait RenderObject: Debug + ComposeNode { fn paint(&self, context: &mut PaintContext); } impl_downcast!(RenderObject); #[derive(Debug)] pub struct RenderFlex { children: Vec>>, } impl RenderFlex { pub fn new() -> Self { RenderFlex { children: Vec::new(), } } } impl RenderObject for RenderFlex { fn paint(&self, context: &mut PaintContext) { println!( "{}", "\t".repeat(context.depth), self.children.len() ); context.depth += 1; for child in &self.children { child.borrow().paint(context); } context.depth -= 1; println!("{}", "\t".repeat(context.depth)); } } #[derive(Debug)] pub struct RenderLabel(String); impl RenderObject for RenderLabel { fn paint(&self, context: &mut PaintContext) { println!("{}", "\t".repeat(context.depth), self.0); } } #[derive(Debug)] pub struct RenderImage(String); impl RenderObject for RenderImage { fn paint(&self, context: &mut PaintContext) { println!("{}", "\t".repeat(context.depth), self.0); } } ```