use std::any::Any;
use guinea_app::feature::FeatureInitContext;
use guinea_core::actor::UiThreadToken;
use guinea_core::scope::Reducer;
use guinea_router::router::{Router, SegmentEntry};
use crate::{
Element, Envelope, Iced, Layout, LayoutCx, Nodes, Observing, Page, PageCx, UpdateCx, deliver_page,
envelope, layout, layout_entry, page, segment_entry,
};
#[derive(Clone, Default, Debug)]
struct Domain(Vec<u32>);
#[derive(Clone)]
struct Replaced(Vec<u32>);
impl Reducer for Domain {
type Update = Replaced;
fn reduce(&mut self, update: Replaced) {
self.0 = update.0;
}
}
#[derive(Default)]
struct Shell {
hits: usize,
}
#[derive(Clone)]
struct ShellMsg;
#[layout]
impl Layout for Shell {
type Params = ();
type Message = ShellMsg;
fn update(&mut self, _message: ShellMsg, _cx: &mut UpdateCx<'_, Self>) {
self.hits += 1;
}
fn view<'a>(&'a self, cx: &LayoutCx<'a, Self>) -> Element<'a, Envelope> {
cx.outlet()
}
}
#[derive(Default)]
struct Leaf {
picked: usize,
log: Vec<&'static str>,
}
#[derive(Clone)]
enum LeafMsg {
Pick(usize),
Clamp(usize),
}
fn list_replaced(update: &Replaced) -> Option<LeafMsg> {
Some(LeafMsg::Clamp(update.0.len()))
}
#[page]
impl Page for Leaf {
const CACHE_STATE_IN_MEMORY: bool = true;
type Params = ();
type Message = LeafMsg;
fn install(ctx: &FeatureInitContext, _params: &()) -> anyhow::Result<()> {
ctx.state::<Domain>().seed(Domain(vec![1, 2, 3])).plain();
Ok(())
}
fn observes(cx: &Observing<'_, LeafMsg>) {
cx.on::<Domain>(list_replaced);
}
fn update(&mut self, message: LeafMsg, _cx: &mut UpdateCx<'_, Self>) {
match message {
LeafMsg::Pick(row) => {
self.picked = row;
self.log.push("pick");
}
LeafMsg::Clamp(len) => {
self.picked = self.picked.min(len.saturating_sub(1));
self.log.push("clamp");
}
}
}
fn view(&self, _cx: &PageCx<'_, Self>) -> Element<'_, LeafMsg> {
iced::widget::text("").into()
}
}
#[derive(Default)]
struct Other;
#[page]
impl Page for Other {
fn view(&self, _cx: &PageCx<'_, Self>) -> Element<'_, Self::Message> {
iced::widget::text("").into()
}
}
const WITH_LEAF: [SegmentEntry<Iced>; 2] = [layout_entry::<Shell>(), segment_entry::<Leaf>()];
const WITH_OTHER: [SegmentEntry<Iced>; 2] = [layout_entry::<Shell>(), segment_entry::<Other>()];
fn params() -> Vec<Box<dyn Any>> {
vec![Box::new(()), Box::new(())]
}
struct Mounted {
router: std::rc::Rc<Router<Iced>>,
nodes: Nodes,
}
impl Mounted {
fn at(chain: &'static [SegmentEntry<Iced>]) -> Self {
let token = UiThreadToken::dangerously_create_token_unchecked();
let router = std::rc::Rc::new(Router::<Iced>::new(token));
let mut mounted = Mounted {
router,
nodes: Nodes::default(),
};
mounted.activate(chain);
mounted
}
fn activate(&mut self, chain: &'static [SegmentEntry<Iced>]) {
self.router.activate(chain, params()).expect("activate");
self.nodes.sync(chain);
}
fn send(&mut self, envelope: Envelope) {
envelope::settle(&self.router, &mut self.nodes, envelope);
}
fn leaf(&self) -> &Leaf {
self.nodes.get::<Leaf>(1).expect("a leaf is mounted")
}
}
fn to_leaf(message: LeafMsg) -> Envelope {
Envelope::new(1, deliver_page::<Leaf>, Box::new(message))
}
#[test]
fn a_message_reaches_its_own_node_and_no_other() {
let mut mounted = Mounted::at(&WITH_LEAF);
mounted.send(to_leaf(LeafMsg::Pick(2)));
assert_eq!(mounted.leaf().picked, 2);
assert_eq!(
mounted
.nodes
.get::<Shell>(0)
.expect("a layout is mounted")
.hits,
0,
"the layout above never sees its child's messages - which is why \
adding a page costs it no edit"
);
}
#[test]
fn an_observer_translates_rather_than_mutates() {
let mut mounted = Mounted::at(&WITH_LEAF);
mounted.send(to_leaf(LeafMsg::Pick(2)));
mounted
.router
.scope_at(1)
.expect("a leaf is mounted")
.push::<Domain>(Replaced(vec![9]));
assert_eq!(
mounted.leaf().picked,
2,
"observing produced a message, and a message is not a mutation"
);
mounted.send(Envelope::settled());
assert_eq!(
mounted.leaf().picked,
0,
"the node clamped itself, through update"
);
assert_eq!(
mounted.leaf().log,
["pick", "clamp"],
"both changes went through the same update - there is one mutation point"
);
}
#[test]
fn a_cached_page_comes_back_to_the_state_it_left() {
let mut mounted = Mounted::at(&WITH_LEAF);
mounted.send(to_leaf(LeafMsg::Pick(2)));
mounted.activate(&WITH_OTHER);
mounted.activate(&WITH_LEAF);
assert_eq!(
mounted.leaf().picked,
2,
"the node the shell kept came back, rather than the one init built"
);
}
#[test]
fn a_page_that_did_not_ask_starts_fresh() {
let mut mounted = Mounted::at(&WITH_LEAF);
mounted.send(Envelope::new(0, crate::deliver_layout::<Shell>, Box::new(ShellMsg)));
assert_eq!(mounted.nodes.get::<Shell>(0).expect("mounted").hits, 1);
mounted.activate(&WITH_OTHER);
mounted.activate(&WITH_LEAF);
assert_eq!(
mounted.nodes.get::<Shell>(0).expect("mounted").hits,
0,
"keeping state across navigation is opt-in, and this layout did not"
);
}
#[test]
fn a_message_for_a_node_that_left_the_chain_is_dropped() {
let mut mounted = Mounted::at(&WITH_LEAF);
mounted.activate(&WITH_OTHER);
mounted.send(to_leaf(LeafMsg::Pick(9)));
assert!(
mounted.nodes.get::<Leaf>(1).is_none(),
"delivering it would have made a node for a page that is not there"
);
}