{"id":"fission-tabs-1","tabs":[{"index":0,"label":"Electron","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-1","index":0,"label":"Electron","value":"source"}
const todos = [
{ id: 1, title: "Write release notes", done: false },
{ id: 2, title: "Test Android package", done: true },
];
return (
<section className="todo-screen">
<h1>Ship checklist</h1>
{todos.map((todo) => (
<div key={todo.id} className="todo-row">
<span>{todo.done ? "Done" : "Open"}</span>
<span>{todo.title}</span>
</div>
))}
</section>
);
}
{"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":"Electron","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-2","index":0,"label":"Electron","value":"source"}
const [draft, setDraft] = useState("");
const [todos, setTodos] = useState<Todo[]>([]);
function addDraftTodo() {
const title = draft.trim();
if (!title) return;
setTodos((items) => [...items, { id: crypto.randomUUID(), title, done: false }]);
setDraft("");
}
return <TodoContent draft={draft} todos={todos} onDraft={setDraft} onAdd={addDraftTodo} />;
}
{"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":"Electron","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-3","index":0,"label":"Electron","value":"source"}
// renderer
const todos = await window.electron.todos.load();
// preload
contextBridge.exposeInMainWorld("electron", {
todos: { load: () => ipcRenderer.invoke("todos:load") },
});
// main
ipcMain.handle("todos:load", async () => loadTodosFromDisk());
{"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":"Electron","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-4","index":0,"label":"Electron","value":"source"}
<button className="todo-row rounded-xl border bg-white px-4 py-3 shadow-sm">
<input type="checkbox" checked={todo.done} readOnly />
<span>{todo.title}</span>
</button>
{"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":"Electron","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-5","index":0,"label":"Electron","value":"source"}
createRoot(root).render(
<HashRouter>
<Routes>
<Route path="/" element={<TodoApp />} />
<Route path="/todos/:id" element={<TodoDetail />} />
</Routes>
</HashRouter>
);
{"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":"Electron","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-6","index":0,"label":"Electron","value":"source"}
await app.client.$('[aria-label="New task"]').setValue('Publish release');
await app.client.$('[data-testid="todo.add"]').click();
await expect(app.client.$('text=Publish release')).toBeDisplayed();
{"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"}
Electron concept | Closest Fission concept | Important difference |
|---|---|---|
Renderer component | Fission widget | Fission does not require a DOM/webview substrate. |
ipcRenderer.invoke | Capability, job, service, or resource | Host work is typed and reducer-driven. |
Main process | Shell/native provider boundary | Shells adapt platform behavior around shared app state. |
CSS | Design-system tokens and widget fields | Styling remains target-lowerable. |
Electron packaging | Fission targets and package manifests | Package handoff is manifest-backed. |
WebDriver/Playwright | LiveTestClient and /cmd | Tests can target semantic identifiers, not DOM selectors. |