Build radio widget entries from choice type metadata so variants display their icons (#4408)

* Build radio widget entries from choice type metadata so variants display their icons

* Route the node properties enum radio through the shared choice type entry builder
This commit is contained in:
Keavon Chambers
2026-08-04 21:16:14 -07:00
committed by GitHub
parent 9b3d07dc51
commit 7c8b189c71
12 changed files with 90 additions and 105 deletions

View File

@@ -623,14 +623,7 @@ impl ColorPickerMessageHandler {
// Gradient spread (only present when the picker is in gradient mode)
if self.gradient.is_some() {
let entries = [GradientSpread::Pad, GradientSpread::Reflect, GradientSpread::Repeat]
.into_iter()
.map(|gradient_spread| {
RadioEntryData::new(format!("{gradient_spread:?}"))
.label(gradient_spread.to_string())
.on_update(move |_| ColorPickerMessage::SetGradientSpread { gradient_spread }.into())
})
.collect();
let entries = RadioEntryData::list_from_choice_type(|gradient_spread| ColorPickerMessage::SetGradientSpread { gradient_spread }.into());
groups.push(LayoutGroup::row(vec![
TextLabel::new("Ends").tooltip_label("Gradient Spread").tooltip_description(ENDS_DESCRIPTION).widget_instance(),

View File

@@ -1,6 +1,7 @@
use crate::messages::frontend::IconName;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::message::Message;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use derivative::*;
use graphene_std::Color;
@@ -344,6 +345,30 @@ pub struct RadioEntryData {
pub on_commit: WidgetCallback<()>,
}
impl RadioEntryData {
/// One entry per variant of a choice type enum, shown as the variant's icon when it has one and its text label otherwise.
pub fn list_from_choice_type<E>(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> Vec<Self>
where
E: graphene_std::choice_type::ChoiceTypeStatic + 'static,
{
E::list()
.iter()
.flat_map(|section| section.iter())
.map(|(variant, metadata)| {
let to_message = to_message.clone();
let variant = *variant;
let entry = RadioEntryData::new(metadata.name)
.tooltip_label(metadata.label)
.tooltip_description(metadata.description.unwrap_or_default())
.on_update(move |_| to_message(variant));
if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry.label(metadata.label) }
})
.collect()
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)]

View File

@@ -2629,7 +2629,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
.iter()
.map(|&gradient_form| {
RadioEntryData::new(format!("{:?}", gradient_form))
.label(format!("{:?}", gradient_form))
.label(gradient_form.to_string())
.on_update(update_value(move |_| TaggedValue::GradientForm(gradient_form), node_id, GradientFormInput))
.on_commit(commit_value)
})
@@ -2931,20 +2931,18 @@ pub mod choice {
U: Fn(&E) -> Message + 'static + Send + Sync,
C: Fn(&()) -> Message + 'static + Send + Sync,
{
let items = E::list()
.iter()
.flat_map(|section| section.iter())
.map(|(item, var_meta)| {
let updater = updater_factory();
let committer = committer_factory();
let entry = RadioEntryData::new(var_meta.name)
.on_update(move |_| updater(item))
.on_commit(committer)
.tooltip_label(var_meta.label)
.tooltip_description(var_meta.description.unwrap_or_default());
if let Some(icon) = var_meta.icon { entry.icon(icon) } else { entry.label(var_meta.label) }
// The entry builder clones one callback across all variants, so each factory yields a single shared handle
let updater = std::sync::Arc::new(updater_factory());
let committer = std::sync::Arc::new(committer_factory());
let items = RadioEntryData::list_from_choice_type(move |variant: E| updater(&variant))
.into_iter()
.map(|entry| {
let committer = committer.clone();
entry.on_commit(move |value| committer(value))
})
.collect();
RadioInput::new(items).selected_index(Some(current.as_u32())).disabled(self.disabled).widget_instance()
}
}

View File

@@ -123,19 +123,9 @@ where
E: ChoiceTypeStatic + 'static,
F: Fn(E) -> Message + 'static + Send + Sync + Clone,
{
let entries = E::list()
.iter()
.flat_map(|section| section.iter())
.map(|(value, meta)| {
let to_message = to_message.clone();
let value = *value;
let entry = RadioEntryData::new(meta.name)
.tooltip_label(meta.label)
.tooltip_description(meta.description.unwrap_or_default())
.on_update(move |_| to_message(value))
.on_commit(|_| DocumentMessage::StartTransaction.into());
if let Some(icon) = meta.icon { entry.icon(icon) } else { entry.label(meta.label) }
})
let entries = RadioEntryData::list_from_choice_type(to_message)
.into_iter()
.map(|entry| entry.on_commit(|_| DocumentMessage::StartTransaction.into()))
.collect();
vec![
TextLabel::new(label_text).table_align(true).widget_instance(),

View File

@@ -231,22 +231,13 @@ impl LayoutHolder for GradientTool {
fn layout(&self) -> Layout {
let mut widgets: Vec<WidgetInstance> = Vec::new();
let gradient_form = RadioInput::new(vec![
RadioEntryData::new("Linear").label("Linear").tooltip_label("Linear Gradient").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Form(GradientForm::Linear),
}
.into()
}),
RadioEntryData::new("Radial").label("Radial").tooltip_label("Radial Gradient").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Form(GradientForm::Radial),
}
.into()
}),
])
.selected_index(Some((self.options.gradient_form == GradientForm::Radial) as u32))
.widget_instance();
let gradient_form_entries = RadioEntryData::list_from_choice_type(|gradient_form| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Form(gradient_form),
}
.into()
});
let gradient_form = RadioInput::new(gradient_form_entries).selected_index(Some(self.options.gradient_form as u32)).widget_instance();
// Display priority: the selected layer's stops, then any user-customized tool default, then the working colors
let stops_value = self.data.current_gradient_stops.clone().or_else(|| self.data.default_gradient_stops.clone()).unwrap_or_else(|| {

View File

@@ -217,26 +217,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetInstance {
}
fn create_arc_type_widget(arc_type: ArcType) -> WidgetInstance {
let entries = vec![
RadioEntryData::new("Open").label("Open").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(ArcType::Open),
}
.into()
}),
RadioEntryData::new("Closed").label("Closed").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(ArcType::Closed),
}
.into()
}),
RadioEntryData::new("Pie").label("Pie").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(ArcType::PieSlice),
}
.into()
}),
];
let entries = RadioEntryData::list_from_choice_type(|arc_type| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(arc_type),
}
.into()
});
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_instance()
}
@@ -307,20 +293,12 @@ fn create_spiral_type_widget(spiral_type: SpiralType) -> WidgetInstance {
}
fn create_grid_type_widget(grid_type: GridType) -> WidgetInstance {
let entries = vec![
RadioEntryData::new("Rectangular").label("Rectangular").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::GridType(GridType::Rectangular),
}
.into()
}),
RadioEntryData::new("Isometric").label("Isometric").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::GridType(GridType::Isometric),
}
.into()
}),
];
let entries = RadioEntryData::list_from_choice_type(|grid_type| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::GridType(grid_type),
}
.into()
});
RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_instance()
}

View File

@@ -20,7 +20,6 @@ use crate::messages::tool::utility_types::ToolRefreshOptions;
use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::choice_type::ChoiceTypeStatic;
use graphene_std::color::SRGBA8;
use graphene_std::renderer::Quad;
use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping};
@@ -214,23 +213,12 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
.into()
})
.widget_instance();
let align_entries: Vec<_> = TextAlign::list()
.iter()
.flat_map(|section| section.iter())
.map(|(item, var_meta)| {
let align = *item;
let entry = RadioEntryData::new(var_meta.name)
.tooltip_label(var_meta.label)
.tooltip_description(var_meta.description.unwrap_or_default())
.on_update(move |_| {
TextToolMessage::UpdateOptions {
options: TextOptionsUpdate::Align(align),
}
.into()
});
if let Some(icon) = var_meta.icon { entry.icon(icon) } else { entry.label(var_meta.label) }
})
.collect();
let align_entries = RadioEntryData::list_from_choice_type(|align| {
TextToolMessage::UpdateOptions {
options: TextOptionsUpdate::Align(align),
}
.into()
});
let align = RadioInput::new(align_entries).selected_index(Some(tool.options.align as u32)).widget_instance();
vec![
font,