git.lucas.co / cce-graph
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.