use anyhow::Result;
use fission_core::action::GlobalState as CoreGlobalState;
use fission_core::env::Env;
use fission_core::internal::BuildCtx;
use fission_core::internal::InternalLoweringCx;
use fission_core::ui::{TextInput, Widget};
use fission_core::{reduce_with, ReducerContext, Runtime, View, WidgetIdExt};
use fission_layout::{LayoutEngine, LayoutSize, LineMetric, TextMeasurer};
use fission_render::{DisplayList, RenderScene, Renderer};
use fission_widgets::{Checkbox, Portal};
use std::sync::{Arc, Mutex};
use fission_shell_winit::Pipeline;
#[derive(Debug, Default, Clone)]
struct GlobalState {
text: String,
checked: bool,
show_portal: bool,
}
impl CoreGlobalState for GlobalState {}
#[fission_macros::fission_action]
struct Toggle;
fn on_toggle(state: &mut GlobalState, _a: Toggle, _ctx: &mut ReducerContext<GlobalState>) {
state.checked = !state.checked;
}
#[fission_macros::fission_action]
struct UpdateText(String);
fn on_update(state: &mut GlobalState, a: UpdateText, _ctx: &mut ReducerContext<GlobalState>) {
state.text = a.0;
}
#[derive(Clone, Default)]
struct MockRenderer(pub Arc<Mutex<Option<DisplayList>>>);
impl Renderer for MockRenderer {
fn render_scene(&mut self, scene: &RenderScene) -> Result<()> {
*self.0.lock().unwrap() = Some(scene.flatten());
Ok(())
}
}
struct MockMeasurer;
impl TextMeasurer for MockMeasurer {
fn measure(&self, _text: &str, _font_size: f32, _available_width: Option<f32>) -> (f32, f32) {
(0.0, 0.0)
}
fn hit_test(
&self,
_text: &str,
_font_size: f32,
_available_width: Option<f32>,
_x: f32,
_y: f32,
) -> usize {
0
}
fn get_line_metrics(
&self,
_text: &str,
_font_size: f32,
_available_width: Option<f32>,
) -> Vec<LineMetric> {
vec![]
}
}
#[derive(Clone)]
struct Root;
impl From<Root> for Widget {
fn from(_component: Root) -> Self {
let (ctx, view) = fission_core::build::current::<GlobalState>();
use fission_core::ui::{Column, Row};
let mut children: Vec<Widget> = vec![
Checkbox {
checked: view.state().checked,
on_toggle: Some(ctx.bind(Toggle, reduce_with!(on_toggle))),
label: Some("check".into()),
..Default::default()
}
.into(),
TextInput {
value: view.state().text.clone(),
placeholder: Some("type".into()),
on_change: Some(ctx.bind(UpdateText("".into()), reduce_with!(on_update))),
width: Some(200.0),
height: Some(40.0),
..Default::default()
}
.into(),
];
if view.state().show_portal {
use fission_core::ui::{Overlay, ZStack};
let overlay = Overlay {
id: None,
content: Row::default().into(),
overlay: ZStack {
id: None,
children: vec![
Portal {
child: Row::default().into(),
}
.into(),
],
..Default::default()
}
.into(),
};
children.push(
Portal {
child: overlay.into(),
}
.into(),
);
}
children.push(Row::default().into());
Column {
children,
..Default::default()
}
.into()
}
}
fn pump<W>(
runtime: &mut Runtime,
layout: &mut LayoutEngine,
pipe: &mut Pipeline,
env: &Env,
root: &W,
) -> (fission_ir::CoreIR, fission_layout::LayoutSnapshot)
where
W: Clone + Into<Widget>,
{
let node_tree = {
let state = runtime.get_global_state::<GlobalState>().unwrap();
let view = View::new(
state,
&runtime.runtime_state,
env,
pipe.last_snapshot.as_ref(),
);
let mut ctx = BuildCtx::new();
let mut tree = fission_core::build::enter(&mut ctx, &view, || (*root).clone().into());
runtime.clear_reducers();
let anim = ctx.take_animation_requests();
for (t, r) in anim {
runtime.enqueue_animation(t, r);
}
let vids = ctx.take_video_registrations();
runtime.sync_video_nodes(&vids);
let portals_with_ids = ctx.take_portals();
let portals = portals_with_ids
.into_iter()
.map(|(id, node)| {
if let Some(id) = id {
fission_core::ui::Container::new(node).id(id).into()
} else {
node
}
})
.collect::<Vec<_>>();
if !portals.is_empty() {
use fission_core::ui::{Overlay, Row, ZStack};
let mut children = Vec::with_capacity(1 + portals.len());
children.push(tree);
for p in portals {
children.push(
Overlay {
id: None,
content: Row::default().into(),
overlay: p,
}
.into(),
);
}
tree = ZStack {
id: None,
children,
..Default::default()
}
.into();
}
tree
};
let mut cx = InternalLoweringCx::new(
env,
&runtime.runtime_state,
None,
pipe.last_snapshot.as_ref(),
);
let root_id = fission_core::internal::lower_widget(&node_tree, &mut cx);
cx.ir.root = Some(root_id);
let ir = cx.ir;
let viewport = LayoutSize {
width: 800.0,
height: 600.0,
};
let mut renderer = MockRenderer::default();
let env = Env::default();
let _ = pipe
.render(
ir.clone(),
viewport,
layout,
&runtime.runtime_state.scroll,
&mut renderer,
&runtime.runtime_state.video,
&runtime.runtime_state.web,
&env,
)
.expect("render ok");
let snap = pipe.last_snapshot.clone().expect("snapshot");
(ir, snap)
}
#[test]
fn test_composition_cycles_and_portals() -> Result<()> {
let env = Env::default();
let mut runtime = Runtime::default();
runtime.add_global_state(Box::new(GlobalState::default()))?;
let mut layout = LayoutEngine::new().with_measurer(Arc::new(MockMeasurer));
let mut pipe = Pipeline::new();
let root = Root;
let (ir1, _snap1) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
{
let state = runtime.get_global_state_mut::<GlobalState>().unwrap();
state.show_portal = true;
}
let (ir2, _snap2) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
{
let state = runtime.get_global_state_mut::<GlobalState>().unwrap();
state.show_portal = false;
}
let (ir3, _snap3) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
assert_ne!(ir1.nodes.len(), ir2.nodes.len());
assert_eq!(ir1.nodes.len(), ir3.nodes.len());
Ok(())
}