WIP expose button type picker

This commit is contained in:
Keavon Chambers
2026-05-13 21:07:21 -07:00
parent 696b625a3e
commit 47f2936ef3
10 changed files with 225 additions and 37 deletions
@@ -308,7 +308,31 @@ impl LayoutMessageHandler {
Widget::ParameterExposeButton(parameter_expose_button) => {
let callback_message = match action {
WidgetValueAction::Commit => (parameter_expose_button.on_commit.callback)(&()),
WidgetValueAction::Update => (parameter_expose_button.on_update.callback)(parameter_expose_button),
WidgetValueAction::Update => {
let Some(value_path) = value.as_array() else {
error!("ParameterExposeButton update was not of type: array");
return;
};
// Process the bare button click, since no menu is involved if we're given an empty array.
if value_path.is_empty() {
(parameter_expose_button.on_update.callback)(parameter_expose_button)
}
// Process the menu list entry click, since we have a path to the value of the contained menu entry.
else {
let mut current_submenu = &parameter_expose_button.menu_list_children;
let mut final_entry: Option<&MenuListEntry> = None;
for value in value_path.iter().filter_map(|v| v.as_str().map(|s| s.to_string())) {
let Some(next_entry) = current_submenu.iter().flatten().find(|e| e.value == value) else { return };
current_submenu = &next_entry.children;
final_entry = Some(next_entry);
}
(final_entry.unwrap().on_commit.callback)(&())
}
}
};
responses.add(callback_message);
@@ -914,6 +914,10 @@ impl DiffUpdate {
apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children, action_input_mapping);
text_button.menu_list_children_hash = hash_menu_list_entry_sections(&text_button.menu_list_children);
}
Widget::ParameterExposeButton(parameter_expose_button) => {
apply_action_shortcut_to_menu_lists(&mut parameter_expose_button.menu_list_children, action_input_mapping);
parameter_expose_button.menu_list_children_hash = hash_menu_list_entry_sections(&parameter_expose_button.menu_list_children);
}
_ => {}
};
@@ -92,6 +92,13 @@ pub struct ParameterExposeButton {
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
// Children
#[serde(rename = "menuListChildren")]
pub menu_list_children: MenuListEntrySections,
#[serde(rename = "menuListChildrenHash")]
#[widget_builder(skip)]
pub menu_list_children_hash: u64,
// Tooltips
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
@@ -724,7 +724,7 @@ impl LayoutHolder for MenuBarMessageHandler {
.icon(if message_logging_verbosity_off {
#[cfg(not(target_os = "macos"))]
{
"SmallDot".to_string()
"DataSourceValue".to_string()
}
#[cfg(target_os = "macos")]
{
@@ -738,7 +738,7 @@ impl LayoutHolder for MenuBarMessageHandler {
.icon(if message_logging_verbosity_names {
#[cfg(not(target_os = "macos"))]
{
"SmallDot".to_string()
"DataSourceValue".to_string()
}
#[cfg(target_os = "macos")]
{
@@ -752,7 +752,7 @@ impl LayoutHolder for MenuBarMessageHandler {
.icon(if message_logging_verbosity_contents {
#[cfg(not(target_os = "macos"))]
{
"SmallDot".to_string()
"DataSourceValue".to_string()
}
#[cfg(target_os = "macos")]
{
@@ -53,7 +53,59 @@ pub fn commit_value<T>(_: &T) -> Message {
DocumentMessage::AddTransaction.into()
}
pub fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetInstance {
pub fn expose_widget(
node_id: NodeId,
index: usize,
data_type: FrontendGraphDataType,
resolved_type: String,
valid_types: Vec<Type>,
is_subnetwork: bool,
is_wired: bool,
exposed: bool,
) -> WidgetInstance {
let input_connector = InputConnector::node(node_id, index);
// Subnetwork inputs and wired inputs are read-only here: the type follows the subgraph or the upstream wire automatically.
// Only unconnected protonode inputs accept a manual choice that picks among multiple type implementations.
let entries_clickable = !is_subnetwork && !is_wired;
let type_entries: Vec<MenuListEntry> = if valid_types.is_empty() {
// Fall back to a single read-only entry when no valid types are available (e.g., a type-resolution error).
vec![MenuListEntry::new("type").label(resolved_type.clone()).disabled(true)]
} else {
valid_types
.into_iter()
.enumerate()
.map(|(i, ty)| {
let label = ty.nested_type().to_string();
let is_active = label == resolved_type;
let is_clickable = entries_clickable && !is_active;
let mut entry = MenuListEntry::new(format!("type-{i}")).label(label).disabled(!is_clickable);
// The active entry gets a bullet-point icon (the small filled circle) to mark it as the current selection.
if is_active {
entry = entry.icon("DataSourceValue");
}
if is_clickable {
entry = entry.on_commit(move |_| {
NodeGraphMessage::SetInputValue {
node_id,
input_index: index,
value: TaggedValue::from_type_or_none(&ty),
}
.into()
});
}
entry
})
.collect()
};
let type_submenu = vec![type_entries];
ParameterExposeButton::new()
.exposed(exposed)
.data_type(data_type)
@@ -62,14 +114,32 @@ pub fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphData
} else {
"Expose this parameter as a node input in the graph."
})
.on_update(move |_parameter| Message::Batched {
messages: Box::new([NodeGraphMessage::ExposeInput {
input_connector: InputConnector::node(node_id, index),
set_to_exposed: !exposed,
start_transaction: true,
}
.into()]),
})
.menu_list_children(vec![vec![
MenuListEntry::new("value")
.label("Value")
.icon("DataSourceValue")
.children(type_submenu.clone())
.on_commit(move |_| {
NodeGraphMessage::ExposeInput {
input_connector,
set_to_exposed: false,
start_transaction: true,
}
.into()
}),
MenuListEntry::new("graph")
.label("Graph")
.icon("DataSourceGraph")
.children(type_submenu)
.on_commit(move |_| {
NodeGraphMessage::ExposeInput {
input_connector,
set_to_exposed: true,
start_transaction: true,
}
.into()
}),
]])
.widget_instance()
}
@@ -114,6 +184,10 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<Widget
name,
description,
input_type,
resolved_type,
valid_types,
is_subnetwork,
is_wired,
blank_assist,
exposable,
network_interface,
@@ -132,7 +206,7 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<Widget
};
let mut widgets = Vec::with_capacity(6);
if exposable {
widgets.push(expose_widget(node_id, index, input_type, input.is_exposed()));
widgets.push(expose_widget(node_id, index, input_type, resolved_type, valid_types, is_subnetwork, is_wired, input.is_exposed()));
}
widgets.push(TextLabel::new(name).tooltip_description(description).widget_instance());
@@ -2342,7 +2416,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
let mut unit_suffix = None;
let input_type = match implementation {
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => 'early_return: {
if let Some(field) = graphene_std::registry::NODE_METADATA
let field_default_type = if let Some(field) = graphene_std::registry::NODE_METADATA
.lock()
.unwrap()
.get(proto_node_identifier)
@@ -2352,9 +2426,21 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
display_decimal_places = field.number_display_decimal_places;
unit_suffix = field.unit;
step = field.number_step;
if let Some(ref default) = field.default_type {
break 'early_return default.clone();
}
field.default_type.clone()
} else {
None
};
// Prefer the input's currently stored value type over the protonode's default. This is what lets a user-chosen
// type (e.g. swapping `f64` for `DVec2`) drive the widget, instead of always rendering the registry default.
if let Some(document_node) = context.network_interface.document_node(&node_id, context.selection_network_path)
&& let Some(NodeInput::Value { tagged_value, .. }) = document_node.inputs.get(input_index)
{
break 'early_return tagged_value.ty();
}
if let Some(default) = field_default_type {
break 'early_return default;
}
let Some(implementations) = &interpreted_executor::node_registry::NODE_REGISTRY.get(proto_node_identifier) else {
@@ -2822,6 +2908,10 @@ pub struct ParameterWidgetsInfo<'a> {
name: String,
description: String,
input_type: FrontendGraphDataType,
resolved_type: String,
valid_types: Vec<Type>,
is_subnetwork: bool,
is_wired: bool,
blank_assist: bool,
exposable: bool,
}
@@ -2829,11 +2919,28 @@ pub struct ParameterWidgetsInfo<'a> {
impl<'a> ParameterWidgetsInfo<'a> {
pub fn new(node_id: NodeId, index: usize, blank_assist: bool, context: &'a mut NodePropertiesContext) -> ParameterWidgetsInfo<'a> {
let (name, description) = context.network_interface.displayed_input_name_and_description(&node_id, index, context.selection_network_path);
let input_type = context
.network_interface
.input_type_not_invalid(&InputConnector::node(node_id, index), context.selection_network_path)
.displayed_type();
let input_connector = InputConnector::node(node_id, index);
let type_source = context.network_interface.input_type_not_invalid(&input_connector, context.selection_network_path);
let input_type = type_source.displayed_type();
let resolved_type = type_source.resolved_type_node_string();
// Start with the contextually-valid set. When that's empty for a protonode (e.g. a fresh node with no downstream
// constraints), fall back to every input type the protonode is registered with so the user can still pick.
let mut valid_types = context.network_interface.complete_valid_input_types(&input_connector, context.selection_network_path);
let implementation = context.network_interface.implementation(&node_id, context.selection_network_path);
let is_subnetwork = matches!(implementation, Some(DocumentNodeImplementation::Network(_)));
if valid_types.is_empty()
&& let Some(DocumentNodeImplementation::ProtoNode(protonode_id)) = implementation
&& let Some(implementations) = interpreted_executor::node_registry::NODE_REGISTRY.get(protonode_id)
{
valid_types = implementations.keys().filter_map(|node_io| node_io.inputs.get(index).cloned()).collect();
}
// Dedupe by the displayed type name so repeated implementations sharing the same input type collapse to one entry.
let mut seen_type_names = std::collections::HashSet::new();
valid_types.retain(|ty| seen_type_names.insert(ty.nested_type().to_string()));
let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
let is_wired = document_node.and_then(|node| node.inputs.get(index)).is_some_and(|input| matches!(input, NodeInput::Node { .. }));
ParameterWidgetsInfo {
cached_data: context.cached_data,
@@ -2845,6 +2952,10 @@ impl<'a> ParameterWidgetsInfo<'a> {
name,
description,
input_type,
resolved_type,
valid_types,
is_subnetwork,
is_wired,
blank_assist,
exposable: true,
}