use anyhow::Result;
use fission_core::action::AppState as CoreAppState;
use fission_core::env::Env;
use fission_core::lowering::LoweringContext;
use fission_core::ui::{Node, TextInput};
use fission_core::{reduce_with, BuildCtx, InputEvent, LayoutPoint, ReducerContext, Runtime, View};
use fission_ir::Role;
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 AppState {
text: String,
checked: bool,
show_portal: bool,
}
impl CoreAppState for AppState {}
#[fission_macros::fission_action]
struct Toggle;
fn on_toggle(state: &mut AppState, _a: Toggle, _ctx: &mut ReducerContext<AppState>) {
state.checked = !state.checked;
}
#[fission_macros::fission_action]
struct UpdateText(String);
fn on_update(state: &mut AppState, a: UpdateText, _ctx: &mut ReducerContext<AppState>) {
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![]
}
}
struct Root;
impl fission_core::view::Widget<AppState> for Root {
fn build(&self, ctx: &mut BuildCtx<AppState>, view: &View<AppState>) -> Node {
use fission_core::ui::Column;
let mut children: Vec<Node> = 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, Row as UIRow, ZStack};
let overlay = Overlay {
id: None,
content: Box::new(Node::Row(UIRow::default())),
overlay: Box::new(Node::ZStack(ZStack::default())),
};
children.push(
Portal {
child: Node::Overlay(overlay),
}
.build(ctx, view),
);
}
Node::Column(Column {
children,
..Default::default()
})
}
}
fn pump(
runtime: &mut Runtime,
layout: &mut LayoutEngine,
pipe: &mut Pipeline,
env: &Env,
root: &impl fission_core::view::Widget<AppState>,
) -> (fission_ir::CoreIR, fission_layout::LayoutSnapshot) {
let node_tree = {
let state = runtime.get_app_state::<AppState>().unwrap();
let view = View::new(
state,
&runtime.runtime_state,
env,
pipe.last_snapshot.as_ref(),
);
let mut ctx = BuildCtx::new();
let mut tree = root.build(&mut ctx, &view);
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())
.into_node()
} 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(Node::Overlay(Overlay {
id: None,
content: Box::new(Node::Row(Row::default())),
overlay: Box::new(p),
}));
}
tree = Node::ZStack(ZStack { id: None, children });
}
tree
};
let mut cx = LoweringContext::new(
env,
&runtime.runtime_state,
None,
pipe.last_snapshot.as_ref(),
);
let root_id = node_tree.lower(&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 strict_incremental_checkbox_textinput_checkbox_sequence() -> Result<()> {
std::env::set_var("FISSION_LAYOUT_STRICT", "1");
std::env::remove_var("FISSION_ALLOW_FULL_REBUILD");
let env = Env::default();
let mut runtime = Runtime::default();
runtime.add_app_state(Box::new(AppState::default()))?;
let mut layout = LayoutEngine::new().with_measurer(Arc::new(MockMeasurer));
let mut pipe = Pipeline::new();
let root = Root;
let (ir, snap) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
let mut cb = None;
for (id, node) in &ir.nodes {
if let fission_ir::Op::Semantics(s) = &node.op {
if s.role == Role::Checkbox {
cb = Some(*id);
break;
}
}
}
let cb = cb.expect("checkbox semantics");
let r = snap.get_node_rect(cb).unwrap();
let p = LayoutPoint::new(r.x() + r.width() / 2.0, r.y() + r.height() / 2.0);
runtime.handle_input(
InputEvent::Pointer(fission_core::event::PointerEvent::Down {
point: p,
button: fission_core::event::PointerButton::Primary,
modifiers: 0,
}),
&ir,
&snap,
)?;
let (_ir2, _sn2) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
runtime.handle_input(
InputEvent::Pointer(fission_core::event::PointerEvent::Up {
point: p,
button: fission_core::event::PointerButton::Primary,
modifiers: 0,
}),
&ir,
&snap,
)?;
let (_ir3, _sn3) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
let (ir4, sn4) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
let mut ti = None;
for (id, node) in &ir4.nodes {
if let fission_ir::Op::Semantics(s) = &node.op {
if s.role == Role::TextInput {
ti = Some(*id);
break;
}
}
}
let ti = ti.expect("text input semantics");
let tr = sn4.get_node_rect(ti).unwrap();
let tp = LayoutPoint::new(tr.x() + tr.width() / 2.0, tr.y() + tr.height() / 2.0);
runtime.handle_input(
InputEvent::Pointer(fission_core::event::PointerEvent::Down {
point: tp,
button: fission_core::event::PointerButton::Primary,
modifiers: 0,
}),
&ir4,
&sn4,
)?;
let (_ir5, _sn5) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
let (ir6, sn6) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
runtime.handle_input(
InputEvent::Pointer(fission_core::event::PointerEvent::Down {
point: p,
button: fission_core::event::PointerButton::Primary,
modifiers: 0,
}),
&ir6,
&sn6,
)?;
let (_ir7, _sn7) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
runtime.handle_input(
InputEvent::Pointer(fission_core::event::PointerEvent::Up {
point: p,
button: fission_core::event::PointerButton::Primary,
modifiers: 0,
}),
&ir6,
&sn6,
)?;
let (_ir8, _sn8) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);
Ok(())
}