graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: add-node palette is a cce-cloud dmenu popup
open_node_palette spawns cce-cloud --dmenu (the status bar's
CloudPopup/CloudPopupTracker pattern): positioned at the pointer,
parented to the designer surface, toggle-tracked, template list
filtered for utility dirs. The pick returns through the event loop as a
fire-and-forget AddNode at the grid cursor (CloudSpawned/CloudClosed
CustomEvents adopt/clear the tracker pid). RunAction failures now
surface in the status bar instead of stderr. The in-app NodePalette
widget no longer opens from this path.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/app.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++----
src/window.rs | 9 ++++++++-
2 files changed, 59 insertions(+), 5 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 7530e73..14bf5cb 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -351,6 +351,11 @@ pub enum CustomEvent {
/// A fire-and-forget action from an app-internal thread (the cce-files
/// choosers deliver their picked path this way).
RunAction(McpAction),
+ /// A cce-cloud popup thread announced its process (CloudPopupTracker
+ /// adoption — the add-node palette).
+ CloudSpawned { pid: u32, source: String },
+ /// A cce-cloud popup thread reported its popup closed.
+ CloudClosed { pid: u32, source: String },
/// App-requested exit (menu File > Exit, MCP menu_action): the engine's
/// update hook is the only place with exit access, so input handlers that
/// see `exit_requested` route it here.
@@ -940,6 +945,9 @@ pub struct State {
pub node_palette_query: String,
pub node_palette_filtered: Vec<usize>,
pub node_palette_selected: usize,
+ /// The add-node palette's cce-cloud popup (single active popup, toggle
+ /// semantics — the status bar's tracker pattern).
+ pub cloud_popups: cce_ui::process::CloudPopupTracker,
pub drag_widget: Option<usize>,
pub focused_widget: Option<usize>,
@@ -2024,11 +2032,49 @@ impl State {
});
}
+ /// The add-node palette, as a `cce-cloud --dmenu` popup (the in-app
+ /// NodePalette widget is retired from this path): toggle-tracked like the
+ /// status bar's popups, positioned at the pointer and parented to the
+ /// designer surface. The picked template comes back through the event
+ /// loop as a fire-and-forget `AddNode` at the grid cursor.
pub fn open_node_palette(&mut self) {
- self.node_palette_visible = true;
- self.node_palette_query = String::new();
- self.node_palette_selected = 0;
- self.refresh_node_palette();
+ const SOURCE: &str = "node-palette";
+ if self.cloud_popups.click(SOURCE) == cce_ui::process::CloudPopupClick::ToggledOff {
+ return;
+ }
+ let Some(sender) = self.event_sender.clone() else { return };
+ // In a utility dir geometry templates are rejected at placement —
+ // don't offer them.
+ let in_utility = !self.current_path.is_empty()
+ && self.fs_root.children[self.current_path[0]].node_type == "utility";
+ let items: String = self
+ .node_templates
+ .iter()
+ .filter(|t| !in_utility || !crate::geometry::is_geometry_node_type(&t.node.node_type))
+ .map(|t| t.label.as_str())
+ .collect::<Vec<_>>()
+ .join("\n");
+ let (px, py) = (self.cursor_x as i32, self.cursor_y as i32);
+ let (gx, gy) = (self.grid_cursor_col as f32, self.grid_cursor_row as f32);
+ std::thread::spawn(move || {
+ let popup = cce_ui::process::CloudPopup::at(px, py).parent_app_id("cce-designer");
+ let mut spawned_pid = 0;
+ let result = popup.run_dmenu("Add Node:", &items, |pid| {
+ spawned_pid = pid;
+ let _ = sender.send(CustomEvent::CloudSpawned { pid, source: SOURCE.to_string() });
+ });
+ if let Ok(Some(selected)) = &result {
+ if !selected.is_empty() {
+ let _ = sender.send(CustomEvent::RunAction(McpAction::AddNode {
+ template_name: selected.clone(),
+ name: None,
+ x: gx,
+ y: gy,
+ }));
+ }
+ }
+ let _ = sender.send(CustomEvent::CloudClosed { pid: spawned_pid, source: SOURCE.to_string() });
+ });
}
pub fn close_node_palette(&mut self) {
@@ -2640,6 +2686,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
node_palette_query: String::new(),
node_palette_filtered: Vec::new(),
node_palette_selected: 0,
+ cloud_popups: cce_ui::process::CloudPopupTracker::new(),
drag_widget: None,
focused_widget: None,
cursor_x: 0.0,
diff --git a/src/window.rs b/src/window.rs
index cc0e96d..215cadf 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -587,9 +587,16 @@ impl State {
}
CustomEvent::RunAction(action) => {
if let Err(e) = state.apply_action(action, &mut needs_redraw) {
- eprintln!("Action failed: {e}");
+ state.update_status_text(&e);
+ needs_redraw = true;
}
}
+ CustomEvent::CloudSpawned { pid, source } => {
+ state.cloud_popups.on_spawned(pid, &source);
+ }
+ CustomEvent::CloudClosed { pid, source } => {
+ let _ = state.cloud_popups.on_closed(pid, &source);
+ }
// Exit is handled by the Application::update wrapper
// (autosave + engine exit) before this is reached.
CustomEvent::Exit => {}