{"id":"fission-tabs-1","tabs":[{"index":0,"label":"Dioxus","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-1","index":0,"label":"Dioxus","value":"source"}
#[component]
fn TodoApp() -> Element {
let todos = vec![
Todo::new(1, "Write release notes", false),
Todo::new(2, "Test Android package", true),
];
rsx! {
section {
h1 { "Ship checklist" }
for todo in todos {
{
let status = if todo.done { "Done" } else { "Open" };
rsx! {
div {
span { "{status}" }
span { "{todo.title}" }
}
}
}
}
}
}
}
{"id":"fission-tabs-1","index":1,"label":"Fission","value":"fission"}
use fission::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Todo {
pub id: u64,
pub title: String,
pub done: bool,
}
pub struct TodoApp;
impl From<TodoApp> for Widget {
fn from(_app: TodoApp) -> Widget {
let todos = vec![
Todo { id: 1, title: "Write release notes".into(), done: false },
Todo { id: 2, title: "Test Android package".into(), done: true },
];
let rows = todos.into_iter().map(|todo| {
let status = if todo.done { "Done" } else { "Open" };
Row {
gap: Some(12.0),
children: widgets![Text::new(status), Text::new(todo.title)],
..Default::default()
}
.into()
});
Container::new(Column {
gap: Some(12.0),
children: std::iter::once(Text::new("Ship checklist").size(28.0).weight(800).into())
.chain(rows)
.collect(),
..Default::default()
})
.padding_all(24.0)
.into()
}
}
{"id":"fission-tabs-1"}
{"id":"fission-tabs-2","tabs":[{"index":0,"label":"Dioxus","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-2","index":0,"label":"Dioxus","value":"source"}
#[component]
fn TodoApp() -> Element {
let mut draft = use_signal(String::new);
let mut todos = use_signal(Vec::<Todo>::new);
let add = move |_| {
let title = draft.read().trim().to_string();
if title.is_empty() { return; }
todos.write().push(Todo::new(title));
draft.set(String::new());
};
rsx! { TodoContent { draft, todos, onadd: add } }
}
{"id":"fission-tabs-2","index":1,"label":"Fission","value":"fission"}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TodoState {
pub draft: String,
pub todos: Vec<Todo>,
pub next_id: u64,
pub current_path: String,
}
impl Default for TodoState {
fn default() -> Self {
Self {
draft: String::new(),
todos: Vec::new(),
next_id: 1,
current_path: "/".into(),
}
}
}
impl GlobalState for TodoState {}
#[fission_reducer(SetDraft)]
fn set_draft(state: &mut TodoState, draft: String) {
state.draft = draft;
}
#[fission_reducer(AddDraftTodo)]
fn add_draft_todo(state: &mut TodoState) {
let title = state.draft.trim();
if title.is_empty() {
return;
}
state.todos.push(Todo {
id: state.next_id,
title: title.to_string(),
done: false,
});
state.next_id += 1;
state.draft.clear();
}
#[fission_reducer(ToggleTodo)]
fn toggle_todo(state: &mut TodoState, id: u64) {
if let Some(todo) = state.todos.iter_mut().find(|todo| todo.id == id) {
todo.done = !todo.done;
}
}
pub struct TodoScreen;
impl From<TodoScreen> for Widget {
fn from(_screen: TodoScreen) -> Widget {
let (ctx, view) = fission::build::current::<TodoState>();
let update_draft = with_reducer!(ctx, SetDraft(String::new()), set_draft);
let add_todo = with_reducer!(ctx, AddDraftTodo, add_draft_todo);
let rows = view.state().todos.iter().map(|todo| {
let toggle = with_reducer!(ctx, ToggleTodo(todo.id), toggle_todo);
Row {
gap: Some(12.0),
children: widgets![
Checkbox {
checked: todo.done,
on_toggle: Some(toggle),
..Default::default()
}
.semantics_identifier(format!("todo.toggle.{}", todo.id)),
Text::new(todo.title.clone()),
],
..Default::default()
}
.semantics_identifier(format!("todo.row.{}", todo.id))
.into()
});
Column {
gap: Some(16.0),
children: widgets![
Text::new("Ship checklist").size(28.0).weight(800),
Row {
gap: Some(12.0),
children: widgets![
TextInput {
id: Some(WidgetId::explicit("todo-title")),
value: view.state().draft.clone(),
label: Some("New task".into()),
placeholder: Some("Add a task".into()),
on_change: Some(update_draft),
..Default::default()
}
.semantics_identifier("todo.title"),
Button {
child: Some(Text::new("Add task").into()),
on_press: Some(add_todo),
..Default::default()
}
.semantics_identifier("todo.add"),
],
..Default::default()
},
Column { gap: Some(10.0), children: rows.collect(), ..Default::default() },
],
..Default::default()
}
.into()
}
}
{"id":"fission-tabs-2"}
{"id":"fission-tabs-3","tabs":[{"index":0,"label":"Dioxus","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-3","index":0,"label":"Dioxus","value":"source"}
let todos = use_resource(move || async move {
api::load_todos().await
});
match &*todos.read_unchecked() {
Some(Ok(items)) => rsx! { TodoList { todos: items.clone() } },
Some(Err(_)) => rsx! { "Unable to load" },
None => rsx! { "Loading" },
}
{"id":"fission-tabs-3","index":1,"label":"Fission","value":"fission"}
use fission::core::{JobRef, JobSpec};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoadTodosRequest;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApiError {
pub message: String,
}
pub struct LoadTodosJob;
impl JobSpec for LoadTodosJob {
type Request = LoadTodosRequest;
type Ok = Vec<Todo>;
type Err = ApiError;
const NAME: &'static str = "todo.load";
}
pub const LOAD_TODOS_JOB: JobRef<LoadTodosJob> = JobRef::new(LoadTodosJob::NAME);
#[fission_reducer(LoadTodos)]
fn load_todos(state: &mut TodoState, ctx: &mut ReducerContext<TodoState>) {
state.loading = true;
ctx.effects
.app(LOAD_TODOS_JOB, LoadTodosRequest)
.on_ok(ctx.effects.bind(TodosLoaded, reduce_with!(todos_loaded)))
.on_err(ctx.effects.bind(TodosFailed, reduce_with!(todos_failed)));
}
{"id":"fission-tabs-3"}
{"id":"fission-tabs-4","tabs":[{"index":0,"label":"Dioxus","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-4","index":0,"label":"Dioxus","value":"source"}
rsx! {
button { class: "rounded-xl border bg-white px-4 py-3 shadow-sm",
input { r#type: "checkbox", checked: todo.done }
span { "{todo.title}" }
}
}
{"id":"fission-tabs-4","index":1,"label":"Fission","value":"fission"}
pub struct TodoRow {
pub todo: Todo,
}
impl From<TodoRow> for Widget {
fn from(row: TodoRow) -> Widget {
let (ctx, view) = fission::build::current::<TodoState>();
let tokens = &view.env().theme.tokens;
let toggle = with_reducer!(ctx, ToggleTodo(row.todo.id), toggle_todo);
Container::new(Row {
gap: Some(tokens.spacing.s),
children: widgets![
Checkbox {
checked: row.todo.done,
on_toggle: Some(toggle),
..Default::default()
}
.semantics_identifier(format!("todo.toggle.{}", row.todo.id)),
Text::new(row.todo.title)
.size(tokens.typography.font_size_base)
.color(tokens.colors.text_primary),
],
..Default::default()
})
.bg(tokens.colors.surface)
.border(tokens.colors.border, 1.0)
.border_radius(tokens.radii.large)
.padding([tokens.spacing.m, tokens.spacing.m, tokens.spacing.s, tokens.spacing.s])
.into()
}
}
{"id":"fission-tabs-4"}
{"id":"fission-tabs-5","tabs":[{"index":0,"label":"Dioxus","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-5","index":0,"label":"Dioxus","value":"source"}
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[route("/")]
Todos {},
#[route("/todos/:id")]
TodoDetail { id: u64 },
}
rsx! { Router::<Route> {} }
{"id":"fission-tabs-5","index":1,"label":"Fission","value":"fission"}
use fission::widgets::{Route, Router};
use std::sync::Arc;
#[fission_reducer(NavigateTo)]
fn navigate_to(state: &mut TodoState, path: String) {
state.current_path = path;
}
pub struct TodoRoutes;
impl From<TodoRoutes> for Widget {
fn from(_routes: TodoRoutes) -> Widget {
let (_, view) = fission::build::current::<TodoState>();
Router::<TodoState> {
current_path: view.state().current_path.clone(),
routes: vec![
Route {
path: "/".into(),
builder: Arc::new(|_, _, _| TodoScreen.into()),
},
Route {
path: "/todos/:id".into(),
builder: Arc::new(|_, _, params| TodoDetail {
id: params["id"].parse().unwrap_or(0),
}
.into()),
},
],
not_found: Some(Arc::new(|_, _, _| Text::new("Not found").into())),
}
.into()
}
}
{"id":"fission-tabs-5"}
{"id":"fission-tabs-6","tabs":[{"index":0,"label":"Dioxus","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-6","index":0,"label":"Dioxus","value":"source"}
// Dioxus tests often target the rendered app or platform runner.
// Assert user-visible behavior rather than internal component structure.
app.find("New task").type_text("Publish release");
app.find("Add task").click();
app.find("Publish release").assert_exists();
{"id":"fission-tabs-6","index":1,"label":"Fission","value":"fission"}
use fission_test_driver::{LiveTestClient, SelectorQuery};
#[test]
#[ignore]
fn user_can_add_a_todo_in_a_real_shell() {
let control_port = reserve_control_port();
let mut child = launch_todo_app(control_port);
let client = LiveTestClient::connect(control_port);
client.wait_for_ready(15_000).expect("app ready");
client
.fill_text_semantic_identifier("todo.title", "Publish release")
.expect("fill draft");
client
.tap_semantic_identifier("todo.add")
.expect("add todo");
client
.wait_for_visible(SelectorQuery::label("Publish release"), 5_000)
.expect("todo visible");
client
.screenshot(".artifacts/screenshots/todos/added.png")
.expect("screenshot");
client.quit().expect("quit app");
let _ = child.wait();
}
{"id":"fission-tabs-6"}
Dioxus concept | Closest Fission concept | Important difference |
|---|---|---|
RSX component | Rust struct plus impl From<T> for Widget | Fission is widget-lowering, not virtual-DOM rendering. |
Signal | GlobalState or #[local_state] | State lives where ownership says it belongs. |
Resource | Resource/job/service/effect | Outside work is explicit runtime work. |
Dioxus router | Fission Router | Fission routes are shared app state. |
Desktop/mobile runner | Fission shell and target | Shells adapt one app model to each platform. |