node-based graph editor
git clone https://git.lucas.co/cce-graph.git
CLAUDE.md (6K)
1 # CLAUDE.md
2
3 This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
5 ## What this is
6
7 `cce-graph` is a node-graph / mood-board editor client for the CCE Wayland desktop
8 environment: a grid-aligned canvas of connected nodes plus free-floating images, saved
9 as KDL project directories. It is one crate of the multi-repo `cce` workspace (its own
10 git repo side-by-side with its siblings; `origin` is the local *bare* repo
11 `~/git/cce-graph.git`, so **committing is not publishing — `git push origin main` is**,
12 after which the gitsite system mirrors it to `https://git.lucas.co/cce-graph.git`, the
13 old fetch-only static mirror now kept as the `published` remote). Read the workspace-level
14 `../cce-compositor/WORKSPACE.md` first — workspace layout, the `cce-ui` toolkit, config
15 conventions, and the multi-repo rules (each crate is its own git repo; commit here, not at
16 the workspace root) all live there.
17
18 The entire app is **one file, `src/main.rs`**: a `GraphApp` struct implementing
19 `cce-ui`'s `Application` trait, run via `cce_ui::engine::run::<GraphApp>()`. There are
20 no tests. The actual node-canvas widget (`Graph`, `GraphNode`) lives in `cce-ui`, not
21 here — this crate is orchestration: menu bar + File/Edit/View dropdowns, the `Graph`
22 widget, an image overlay, and a floating "control panel" showing the selected
23 node/image.
24
25 ## Build and run
26
27 ```sh
28 cargo build -p cce-graph # from the workspace root (shared ../target/)
29 cargo run -p cce-graph # optionally pass a project path as the first arg
30 make install # release build, then `ccebuild install --no-build cce-graph`
31 ```
32
33 Building from inside this directory also works (standalone clone case). `cargo run`
34 needs a running Wayland session — ideally the `cce` compositor.
35
36 ## Persistence model
37
38 - **A "project" is a directory** containing `state.kdl` plus `assets/` and `code/`
39 subdirs. `save_project_to_path` creates all three; images added while a project is
40 loaded are copied into `assets/` and referenced by relative path. Legacy
41 `state.json` projects still load; saving writes `state.kdl` and deletes the old
42 JSON.
43 - The KDL schema is hand-rolled in `load_project_from_kdl_path` /
44 `save_project_to_kdl_path` (top-level `name`/`show_grid`/`uniform_background`/
45 `opacity`, then `node` and `image` blocks). Keep both functions in sync when
46 changing it.
47 - With no CLI arg, the app loads (creating if missing)
48 `~/.config/cce/cce-graph/default.kdl` — a bare KDL state file, not a project dir.
49 - View settings persist to the **shared** `~/.config/cce/config.kdl` under
50 `layout` (`graph_show_grid`, `graph_snap_enabled`, `graph_network_opacity`,
51 `graph_gap_width`) and `style.surface.graph.uniform_background` — see
52 `load_config()` / `write_config_value()`.
53 - The delete-node keybinding resolves through `input.kdl`'s `cce-graph.delete_node`
54 (via `cce_ui::input::app_chord`), falling back to the legacy config.kdl value.
55 - Recent files are shared toolkit state (`cce_ui::config::load_recent_files`),
56 capped at 10, surfaced inside the File dropdown's options list.
57
58 ## Architecture: the "dissolved" Phase-6 style
59
60 This app is the reference for cce-ui's post-Phase-6 shape — no container widgets, one
61 paint path. When editing, preserve these invariants (the inline comments citing phase
62 numbers, e.g. "6l pattern", "6m recipe", document them deliberately):
63
64 - **Single paint path**: everything renders in `display_list()` — relayout when
65 `needs_rebuild`/resize, then the window plate is emitted as raw prims, top-level
66 widgets are walked with `paint_root_into` (shared borrows), and finally the control
67 panel and images are drawn on top. There is no `view()`; text renders from the
68 paint walk (`display_list_text()` returns true).
69 - **No root-plate/Plate containers**: top-level widgets register **parentless** in
70 `UiContext` (one-time `register_widget` block guarded by `widgets_registered`,
71 using raw pointers — the widgets must stay owned fields of `GraphApp` so those
72 pointers stay valid). The former control-panel Plate is "dissolved": its rect,
73 drag state, and visual are app fields (`panel_*`, `panel_visual()`), its plate is
74 emitted as prims, and only its `Label` is a real walked widget.
75 - **Popovers are ui_context-only**: open dropdowns call
76 `ui_context.register_popover` each frame. Do NOT also register them globally —
77 that spawns a render-only xdg popup that swallows clicks on the open menu.
78 - **Routed events**: input goes through `ui_context.propagate_event(&event, root_id)`
79 with `WidgetId` roots (dropdowns get priority when `over_menu`; otherwise the
80 graph). Two drags are deliberately app-owned rather than widget-routed: the control
81 panel and loaded images (`dragging_image_idx`). The router owns node drags —
82 `is_dragging` forces rebuilds mid-drag, and DragEnd commits before the release
83 reaches `Graph`.
84 - **Dropdown selection protocol**: menu dropdowns use sentinel `selected = 999`
85 ("nothing chosen"); on `take_change()` the app maps the selected option to an
86 `AppMessage` and resets to 999. File-menu entries are matched by option **text**
87 (recent-file paths are pushed straight into `options`), so renaming an entry means
88 updating the match arm.
89 - **Rebuild flags are dual**: handlers set both the `*needs_rebuild` out-param (frame
90 redraw) and `self.needs_rebuild` (relayout in `display_list`). Set both.
91
92 ## Quirks worth knowing
93
94 - Images are decoded, downscaled to max 96px on the long edge, and drawn as
95 **per-pixel quads** clipped to the graph rect — image size on the canvas is in grid
96 cells (width drives height via aspect ratio). Positions are (column, row) floats;
97 snap rounds to half-cells.
98 - Blocking file dialogs run on spawned threads and send results back through the
99 calloop message channel (`AppMessage::OpenRecent` / `SaveToPath` /
100 `AddImageFromPath`); don't call `cce_ui::file_dialog` on the UI thread.
101 - `main()` creates a tokio runtime and enters it before `engine::run` — cce-ui
102 (e.g. its MCP server) expects an ambient runtime.