Skip to content

Commit fd5248b

Browse files
Merge pull request #259 from mitchmindtree/feat/palette-node-pointer-pos
feat(palette): place new nodes under the pointer and select them
2 parents c92bcf5 + 082d62d commit fd5248b

6 files changed

Lines changed: 90 additions & 12 deletions

File tree

crates/bevy_gantz_egui/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@ pub fn on_create_node<N>(
703703
registry: Res<Registry<N>>,
704704
builtins: Res<BuiltinNodes<N>>,
705705
demos: Res<Demos>,
706+
mut gui_state: ResMut<GuiState>,
706707
mut vms: NonSendMut<head::HeadVms>,
707708
mut heads: Query<head::OpenHeadData<N>, With<head::OpenHead>>,
708709
mut views_query: Query<&mut GraphView, With<head::OpenHead>>,
@@ -731,6 +732,10 @@ pub fn on_create_node<N>(
731732
log::error!("CreateNode: VM not found for entity {:?}", event.head);
732733
return;
733734
};
735+
let Some(head_state) = gui_state.open_heads.get_mut(&**data.head_ref) else {
736+
log::error!("CreateNode: GUI state not found for head");
737+
return;
738+
};
734739

735740
let node_reg = registry_ref(&registry, &builtins, &demos);
736741
let get_node = |ca: &ca::ContentAddr| node_reg.node(ca);
@@ -741,6 +746,7 @@ pub fn on_create_node<N>(
741746
|node_type| node_reg.create_node(node_type),
742747
&mut data.working_graph,
743748
&mut views,
749+
head_state,
744750
vm,
745751
event.data.clone(),
746752
);
@@ -780,6 +786,7 @@ pub fn on_branch_node<N>(
780786
pub fn on_create_nested_graph<N>(
781787
trigger: On<ForHead<gantz_egui::CreateNestedGraph>>,
782788
mut registry: ResMut<Registry<N>>,
789+
mut gui_state: ResMut<GuiState>,
783790
mut heads: Query<head::OpenHeadData<N>, With<head::OpenHead>>,
784791
mut views_query: Query<&mut GraphView, With<head::OpenHead>>,
785792
) where
@@ -804,11 +811,17 @@ pub fn on_create_nested_graph<N>(
804811
);
805812
return;
806813
};
814+
let Some(head_state) = gui_state.open_heads.get_mut(&**data.head_ref) else {
815+
log::error!("CreateNestedGraph: GUI state not found for head");
816+
return;
817+
};
807818
gantz_egui::ops::create_nested_graph(
808819
&mut registry,
809820
bevy_gantz::reg::timestamp(),
810821
&mut data.working_graph,
811822
&mut views,
823+
head_state,
824+
event.data.pos,
812825
&parent,
813826
);
814827
}

crates/gantz_egui/examples/demo.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -993,10 +993,11 @@ fn process_responses(ctx: &egui::Context, state: &mut State, mut responses: gant
993993
let Some((head, ix)) = tagged_head(state, head) else {
994994
continue;
995995
};
996-
let editing = match head {
997-
gantz_ca::Head::Branch(name) => Some(name),
996+
let editing = match &head {
997+
gantz_ca::Head::Branch(name) => Some(name.clone()),
998998
gantz_ca::Head::Commit(_) => None,
999999
};
1000+
let head_state = state.gantz.open_heads.entry(head).or_default();
10001001
let env = &state.env;
10011002
let get_node = |ca: &gantz_ca::ContentAddr| env.node(ca);
10021003
let (_, graph, view) = &mut state.heads[ix];
@@ -1007,25 +1008,33 @@ fn process_responses(ctx: &egui::Context, state: &mut State, mut responses: gant
10071008
|node_type| env.new_node(node_type),
10081009
graph,
10091010
view,
1011+
head_state,
10101012
&mut state.vms[ix],
10111013
create,
10121014
);
10131015
}
10141016

1015-
for (head, _) in responses.take::<gantz_egui::CreateNestedGraph>() {
1017+
for (head, create) in responses.take::<gantz_egui::CreateNestedGraph>() {
10161018
let Some((head, ix)) = tagged_head(state, head) else {
10171019
continue;
10181020
};
10191021
let gantz_ca::Head::Branch(parent) = head else {
10201022
log::warn!("CreateNestedGraph: name the graph before adding a nested graph");
10211023
continue;
10221024
};
1025+
let head_state = state
1026+
.gantz
1027+
.open_heads
1028+
.entry(gantz_ca::Head::Branch(parent.clone()))
1029+
.or_default();
10231030
let (_, graph, view) = &mut state.heads[ix];
10241031
gantz_egui::ops::create_nested_graph(
10251032
&mut state.env.registry,
10261033
timestamp(),
10271034
graph,
10281035
view,
1036+
head_state,
1037+
create.pos,
10291038
&parent,
10301039
);
10311040
}

crates/gantz_egui/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ pub struct CopyNodes(pub std::collections::HashSet<widget::graph_scene::NodeInde
318318
pub struct CreateNode {
319319
/// The type name of the node to create.
320320
pub node_type: String,
321+
/// Where to place the new node, in graph coordinates. When `None`, the node
322+
/// is placed at the center of the current view.
323+
pub pos: Option<egui::Pos2>,
321324
}
322325

323326
/// Create a new nested graph in the emitting head's graph.
@@ -327,7 +330,11 @@ pub struct CreateNode {
327330
/// [`node::NamedRef`] to it. Behaves like creating any
328331
/// other node, but is registry-aware.
329332
#[derive(Clone, Copy, Debug)]
330-
pub struct CreateNestedGraph;
333+
pub struct CreateNestedGraph {
334+
/// Where to place the new node, in graph coordinates. When `None`, the node
335+
/// is placed at the center of the current view.
336+
pub pos: Option<egui::Pos2>,
337+
}
331338

332339
/// Evaluate an entrypoint (push or pull).
333340
#[derive(Clone, Debug)]

crates/gantz_egui/src/ops.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,14 @@ pub fn create_node<N>(
9696
new_node: impl FnOnce(&str) -> Option<N>,
9797
graph: &mut Graph<N>,
9898
view: &mut egui_graph::View,
99+
head_state: &mut OpenHeadState,
99100
vm: &mut Engine,
100101
cmd: CreateNode,
101102
) -> Option<NodeIndex>
102103
where
103104
N: gantz_core::Node + crate::sync::AsNamedRef,
104105
{
105-
let CreateNode { node_type } = cmd;
106+
let CreateNode { node_type, pos } = cmd;
106107
// Refuse references that would form a cycle back to the editing graph; with
107108
// sync on such a cycle recommits endlessly (see `crate::cycle`). A nameless
108109
// (detached commit) head can't be the target of a name-based cycle.
@@ -121,9 +122,18 @@ where
121122
let reg_ctx = node::RegCtx::new(get_node, &node_path, vm);
122123
graph[node_ix].register(reg_ctx);
123124

124-
// Position the new node at the scene center (or use layout default).
125+
// Position the new node under the pointer, falling back to the center of the
126+
// current view. `insert` (not `entry`) so a reused stable-graph index can't
127+
// inherit a removed node's stale position.
128+
let pos = pos.unwrap_or_else(|| view.scene_rect.center());
125129
let egui_id = egui_graph::NodeId::from_u64(node_ix.index() as u64);
126-
view.layout.entry(egui_id).or_insert(egui::Pos2::ZERO);
130+
view.layout.insert(egui_id, pos);
131+
132+
// Make the new node the sole selection (clearing the previous one).
133+
let sel = &mut head_state.scene.interaction.selection;
134+
sel.nodes.clear();
135+
sel.edges.clear();
136+
sel.nodes.insert(node_ix);
127137

128138
Some(node_ix)
129139
}
@@ -139,6 +149,8 @@ pub fn create_nested_graph<N>(
139149
timestamp: std::time::Duration,
140150
graph: &mut Graph<N>,
141151
view: &mut egui_graph::View,
152+
head_state: &mut OpenHeadState,
153+
pos: Option<egui::Pos2>,
142154
parent: &str,
143155
) -> Option<NodeIndex>
144156
where
@@ -166,9 +178,18 @@ where
166178
let named_ref = NamedRef::with_sync(name, node::Ref::new(commit_ca.into()));
167179
let node_ix = graph.add_node(N::from(named_ref));
168180

169-
// Position the new node at the scene center (or use layout default).
181+
// Position the new node under the pointer, falling back to the center of the
182+
// current view. `insert` (not `entry`) so a reused stable-graph index can't
183+
// inherit a removed node's stale position.
184+
let pos = pos.unwrap_or_else(|| view.scene_rect.center());
170185
let egui_id = egui_graph::NodeId::from_u64(node_ix.index() as u64);
171-
view.layout.entry(egui_id).or_insert(egui::Pos2::ZERO);
186+
view.layout.insert(egui_id, pos);
187+
188+
// Make the new node the sole selection (clearing the previous one).
189+
let sel = &mut head_state.scene.interaction.selection;
190+
sel.nodes.clear();
191+
sel.edges.clear();
192+
sel.nodes.insert(node_ix);
172193

173194
Some(node_ix)
174195
}

crates/gantz_egui/src/widget/gantz.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -869,13 +869,19 @@ where
869869
gantz_ca::Head::Branch(name) => Some(name.as_str()),
870870
_ => None,
871871
};
872+
// The pointer position over the focused head's scene
873+
// (graph coords) recorded this frame; new nodes are placed
874+
// here. `Copy`, so no borrow is held across the call.
875+
let pointer_pos = head_state.scene.interaction.last_pointer_pos;
872876
let created =
873877
command_palette(gantz.env, editing, &mut state.command_palette, ui);
874878
match created {
875-
Some(PaletteChoice::Node(create)) => {
879+
Some(PaletteChoice::Node(mut create)) => {
880+
create.pos = pointer_pos;
876881
gantz_response.responses.push(Some(fh), create);
877882
}
878-
Some(PaletteChoice::NestedGraph(create)) => {
883+
Some(PaletteChoice::NestedGraph(mut create)) => {
884+
create.pos = pointer_pos;
879885
gantz_response.responses.push(Some(fh), create);
880886
}
881887
None => {}
@@ -2062,11 +2068,14 @@ fn command_palette(
20622068
// The chosen node type becomes a creation payload. The reserved
20632069
// `NESTED_GRAPH_TYPE` routes to the registry-aware nested-graph op.
20642070
cmd_palette.show(ui.ctx(), cmds).map(|cmd| {
2071+
// The placement position is filled in by the caller, which has access to
2072+
// the focused head's last pointer position.
20652073
if cmd.name == NESTED_GRAPH_TYPE {
2066-
PaletteChoice::NestedGraph(CreateNestedGraph)
2074+
PaletteChoice::NestedGraph(CreateNestedGraph { pos: None })
20672075
} else {
20682076
PaletteChoice::Node(CreateNode {
20692077
node_type: cmd.name.to_string(),
2078+
pos: None,
20702079
})
20712080
}
20722081
})

crates/gantz_egui/src/widget/graph_scene.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ pub struct Interaction {
7676
/// Position where an edge context menu was opened (in graph coordinates).
7777
#[serde(default, skip)]
7878
pub edge_context_menu_pos: Option<egui::Pos2>,
79+
/// Latest pointer position over the scene, in graph coordinates. Updated
80+
/// each frame the scene is hovered; used to place palette-created nodes
81+
/// under the pointer.
82+
#[serde(default, skip)]
83+
pub last_pointer_pos: Option<egui::Pos2>,
7984
}
8085

8186
#[derive(Default, serde::Deserialize, serde::Serialize)]
@@ -214,6 +219,20 @@ where
214219
.collect();
215220
}
216221

222+
// Track the latest pointer position over the scene (in graph space) so a
223+
// node added via the command palette lands under the pointer. While the
224+
// palette window covers the scene, `contains_pointer` is false, so this
225+
// retains the pre-open position.
226+
if graph_response.response.contains_pointer() {
227+
let layer_id = graph_response.response.layer_id;
228+
let ptr = ui
229+
.ctx()
230+
.input(|i| i.pointer.interact_pos().or(i.pointer.hover_pos()));
231+
if let (Some(ptr), Some(t)) = (ptr, ui.ctx().layer_transform_from_global(layer_id)) {
232+
state.interaction.last_pointer_pos = Some(t.mul_pos(ptr));
233+
}
234+
}
235+
217236
// Background context menu: graph actions (when mutable) plus a "panes"
218237
// submenu for toggling pane visibility (available even when immutable).
219238
let immutable = self.immutable;

0 commit comments

Comments
 (0)