diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 700477d5c..b1f59604e 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -447,29 +447,16 @@ impl WidgetHolder { Self { widget_id: generate_uuid(), widget } } pub fn unrelated_separator() -> Self { - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })) + Separator::new(SeparatorDirection::Horizontal, SeparatorType::Unrelated).widget_holder() } pub fn related_separator() -> Self { - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })) + Separator::new(SeparatorDirection::Horizontal, SeparatorType::Related).widget_holder() } pub fn text_widget(text: impl Into) -> Self { - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: text.into(), - ..Default::default() - })) + TextLabel::new(text).widget_holder() } pub fn bold_text(text: impl Into) -> Self { - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: text.into(), - bold: true, - ..Default::default() - })) + TextLabel::new(text).bold(true).widget_holder() } /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. pub fn diff(&mut self, new: Self, widget_path: &mut [usize], widget_diffs: &mut Vec) { diff --git a/editor/src/messages/layout/utility_types/mod.rs b/editor/src/messages/layout/utility_types/mod.rs index 627175b9f..95fe0ab5d 100644 --- a/editor/src/messages/layout/utility_types/mod.rs +++ b/editor/src/messages/layout/utility_types/mod.rs @@ -1,3 +1,12 @@ pub mod layout_widget; pub mod misc; pub mod widgets; + +pub mod widget_prelude { + pub use super::layout_widget::{LayoutGroup, Widget, WidgetHolder, WidgetLayout}; + pub use super::widgets::assist_widgets::*; + pub use super::widgets::button_widgets::*; + pub use super::widgets::input_widgets::*; + pub use super::widgets::label_widgets::*; + pub use super::widgets::menu_widgets::*; +} diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index c84c46f1f..aec5e985e 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -2,13 +2,16 @@ use crate::messages::layout::utility_types::layout_widget::WidgetCallback; use crate::messages::{input_mapper::utility_types::misc::ActionKeys, portfolio::document::node_graph::FrontendGraphDataType}; use derivative::*; +use graphite_proc_macros::WidgetBuilder; use serde::{Deserialize, Serialize}; -#[derive(Clone, Default, Derivative, Serialize, Deserialize)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct IconButton { + #[widget_builder(constructor)] pub icon: String, + #[widget_builder(constructor)] pub size: u32, // TODO: Convert to an `IconSize` enum pub disabled: bool, @@ -26,7 +29,7 @@ pub struct IconButton { pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct PopoverButton { pub icon: Option, @@ -34,9 +37,11 @@ pub struct PopoverButton { pub disabled: bool, // Placeholder popover content heading + #[widget_builder(constructor)] pub header: String, // Placeholder popover content paragraph + #[widget_builder(constructor)] pub text: String, pub tooltip: String, @@ -45,7 +50,7 @@ pub struct PopoverButton { pub tooltip_shortcut: Option, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] pub struct ParameterExposeButton { @@ -65,10 +70,11 @@ pub struct ParameterExposeButton { pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] pub struct TextButton { + #[widget_builder(constructor)] pub label: String, pub icon: Option, @@ -91,10 +97,11 @@ pub struct TextButton { pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] pub struct BreadcrumbTrailButtons { + #[widget_builder(constructor)] pub labels: Vec, pub disabled: bool, diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index d3501276b..14f61073c 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -2,13 +2,15 @@ use crate::messages::input_mapper::utility_types::misc::ActionKeys; use crate::messages::layout::utility_types::layout_widget::WidgetCallback; use document_legacy::{color::Color, layers::layer_info::LayerDataTypeDiscriminant, LayerId}; +use graphite_proc_macros::WidgetBuilder; use derivative::*; use serde::{Deserialize, Serialize}; -#[derive(Clone, Derivative, Serialize, Deserialize)] +#[derive(Clone, Derivative, Serialize, Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct CheckboxInput { + #[widget_builder(constructor)] pub checked: bool, pub disabled: bool, @@ -39,9 +41,10 @@ impl Default for CheckboxInput { } } -#[derive(Clone, Derivative, Serialize, Deserialize)] +#[derive(Clone, Derivative, Serialize, Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct ColorInput { + #[widget_builder(constructor)] pub value: Option, // TODO: Add allow_none @@ -62,9 +65,10 @@ pub struct ColorInput { pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct DropdownInput { + #[widget_builder(constructor)] pub entries: DropdownInputEntries, // This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI) @@ -90,11 +94,13 @@ pub struct DropdownInput { pub type DropdownInputEntries = Vec>; -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] +#[widget_builder(not_widget_holder)] pub struct DropdownEntryData { pub value: String, + #[widget_builder(constructor)] pub label: String, pub icon: String, @@ -114,13 +120,15 @@ pub struct DropdownEntryData { pub on_update: WidgetCallback<()>, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct FontInput { #[serde(rename = "fontFamily")] + #[widget_builder(constructor)] pub font_family: String, #[serde(rename = "fontStyle")] + #[widget_builder(constructor)] pub font_style: String, #[serde(rename = "isStyle")] @@ -142,7 +150,7 @@ pub struct FontInput { /// This widget allows for the flexible use of the layout system. /// In a custom layout, one can define a widget that is just used to trigger code on the backend. /// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend. -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct InvisibleStandinInput { #[serde(skip)] @@ -150,15 +158,18 @@ pub struct InvisibleStandinInput { pub on_update: WidgetCallback<()>, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct LayerReferenceInput { + #[widget_builder(constructor)] pub value: Option>, #[serde(rename = "layerName")] + #[widget_builder(constructor)] pub layer_name: Option, #[serde(rename = "layerType")] + #[widget_builder(constructor)] pub layer_type: Option, pub disabled: bool, @@ -178,7 +189,7 @@ pub struct LayerReferenceInput { pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct NumberInput { // Label @@ -193,10 +204,13 @@ pub struct NumberInput { pub disabled: bool, // Value + #[widget_builder(constructor)] pub value: Option, + #[widget_builder(skip)] pub min: Option, + #[widget_builder(skip)] pub max: Option, #[serde(rename = "isInteger")] @@ -247,9 +261,6 @@ pub struct NumberInput { } impl NumberInput { - pub fn new() -> Self { - Self::default() - } pub fn int(mut self) -> Self { self.is_integer = true; self @@ -265,20 +276,8 @@ impl NumberInput { self.mode = NumberInputMode::Range; self } - pub fn unit(mut self, val: impl Into) -> Self { - self.unit = val.into(); - self - } - pub fn dp(mut self, decimal_places: u32) -> Self { - self.display_decimal_places = decimal_places; - self - } pub fn percentage(self) -> Self { - self.min(0.).max(100.).unit("%").dp(2) - } - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self + self.min(0.).max(100.).unit("%").display_decimal_places(2) } } @@ -297,13 +296,15 @@ pub enum NumberInputMode { Range, } -#[derive(Clone, Default, Derivative, Serialize, Deserialize)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct OptionalInput { + #[widget_builder(constructor)] pub checked: bool, pub disabled: bool, + #[widget_builder(constructor)] pub icon: String, pub tooltip: String, @@ -317,9 +318,10 @@ pub struct OptionalInput { pub on_update: WidgetCallback, } -#[derive(Clone, Default, Derivative, Serialize, Deserialize)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct RadioInput { + #[widget_builder(constructor)] pub entries: Vec, pub disabled: bool, @@ -329,11 +331,13 @@ pub struct RadioInput { pub selected_index: u32, } -#[derive(Clone, Default, Derivative, Serialize, Deserialize)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] +#[widget_builder(not_widget_holder)] pub struct RadioEntryData { pub value: String, + #[widget_builder(constructor)] pub label: String, pub icon: String, @@ -349,17 +353,20 @@ pub struct RadioEntryData { pub on_update: WidgetCallback<()>, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct SwatchPairInput { + #[widget_builder(constructor)] pub primary: Color, + #[widget_builder(constructor)] pub secondary: Color, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct TextAreaInput { + #[widget_builder(constructor)] pub value: String, pub label: Option, @@ -374,9 +381,10 @@ pub struct TextAreaInput { pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct TextInput { + #[widget_builder(constructor)] pub value: String, pub label: Option, diff --git a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs index 1424196e9..8045e7d25 100644 --- a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs @@ -1,8 +1,10 @@ use derivative::*; +use graphite_proc_macros::WidgetBuilder; use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq, WidgetBuilder)] pub struct IconLabel { + #[widget_builder(constructor)] pub icon: String, pub disabled: bool, @@ -10,29 +12,33 @@ pub struct IconLabel { pub tooltip: String, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, WidgetBuilder)] pub struct Separator { + #[widget_builder(constructor)] pub direction: SeparatorDirection, #[serde(rename = "type")] + #[widget_builder(constructor)] pub separator_type: SeparatorType, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SeparatorDirection { + #[default] Horizontal, Vertical, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SeparatorType { Related, + #[default] Unrelated, Section, List, } -#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default, WidgetBuilder)] pub struct TextLabel { pub disabled: bool, @@ -51,6 +57,7 @@ pub struct TextLabel { pub tooltip: String, // Body + #[widget_builder(constructor)] pub value: String, } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 157f3576e..20ae0f5fb 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1,5 +1,4 @@ -use crate::messages::layout::utility_types::layout_widget::*; -use crate::messages::layout::utility_types::widgets::{button_widgets::*, input_widgets::*}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::utility_types::ImaginateServerStatus; use crate::messages::prelude::*; @@ -18,32 +17,31 @@ pub fn string_properties(text: impl Into) -> Vec { vec![LayoutGroup::Row { widgets: vec![widget] }] } -fn update_value TaggedValue + 'static + Send + Sync>(value: F, node_id: NodeId, input_index: usize) -> WidgetCallback { - WidgetCallback::new(move |input_value: &T| { +fn update_value(value: impl Fn(&T) -> TaggedValue + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync { + move |input_value: &T| { NodeGraphMessage::SetInputValue { node_id, input_index, value: value(input_value), } .into() - }) + } } fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder { - WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { - exposed, - data_type, - tooltip: "Expose this parameter input in node graph".into(), - on_update: WidgetCallback::new(move |_parameter| { + ParameterExposeButton::new() + .exposed(exposed) + .data_type(data_type) + .tooltip("Expose this parameter input in node graph") + .on_update(move |_parameter| { NodeGraphMessage::ExposeInput { node_id, input_index: index, new_exposed: !exposed, } .into() - }), - ..Default::default() - })) + }) + .widget_holder() } fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec { @@ -74,11 +72,9 @@ fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextInput(TextInput { - value: x.clone(), - on_update: update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, index), - ..TextInput::default() - })), + TextInput::new(x.clone()) + .on_update(update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, index)) + .widget_holder(), ]) } widgets @@ -94,11 +90,9 @@ fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { - value: x.clone(), - on_update: update_value(|x: &TextAreaInput| TaggedValue::String(x.value.clone()), node_id, index), - ..TextAreaInput::default() - })), + TextAreaInput::new(x.clone()) + .on_update(update_value(|x: &TextAreaInput| TaggedValue::String(x.value.clone()), node_id, index)) + .widget_holder(), ]) } widgets @@ -114,11 +108,9 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: *x, - on_update: update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, index), - ..CheckboxInput::default() - })), + CheckboxInput::new(*x) + .on_update(update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, index)) + .widget_holder(), ]) } widgets @@ -134,25 +126,22 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(x), - on_update: update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index), - ..number_props - })), + number_props + .value(Some(x)) + .on_update(update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index)) + .widget_holder(), ]) - } - if let NodeInput::Value { + } else if let NodeInput::Value { tagged_value: TaggedValue::U32(x), exposed: false, } = document_node.inputs[index] { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(x as f64), - on_update: update_value(|x: &NumberInput| TaggedValue::U32(x.value.unwrap() as u32), node_id, index), - ..NumberInput::default() - })), + number_props + .value(Some(x as f64)) + .on_update(update_value(|x: &NumberInput| TaggedValue::U32(x.value.unwrap() as u32), node_id, index)) + .widget_holder(), ]) } widgets @@ -161,19 +150,17 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na /// Properties for the input node, with information describing how frames work and a refresh button pub fn input_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let information = WidgetHolder::text_widget("The graph's input is the artwork under the frame layer"); - let refresh_button = WidgetHolder::new(Widget::TextButton(TextButton { - label: "Refresh Input".to_string(), - tooltip: "Refresh the artwork under the frame".to_string(), - on_update: WidgetCallback::new(|_| DocumentMessage::NodeGraphFrameGenerate.into()), - ..Default::default() - })); + let refresh_button = TextButton::new("Refresh Input") + .tooltip("Refresh the artwork under the frame") + .on_update(|_| DocumentMessage::NodeGraphFrameGenerate.into()) + .widget_holder(); vec![LayoutGroup::Row { widgets: vec![information] }, LayoutGroup::Row { widgets: vec![refresh_button] }] } pub fn adjust_hsl_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let hue_shift = number_widget(document_node, node_id, 1, "Hue Shift", NumberInput::new().min(-180.).max(180.).unit("°"), true); - let saturation_shift = number_widget(document_node, node_id, 2, "Saturation Shift", NumberInput::new().min(-100.).max(100.).unit("%"), true); - let lightness_shift = number_widget(document_node, node_id, 3, "Lightness Shift", NumberInput::new().min(-100.).max(100.).unit("%"), true); + let hue_shift = number_widget(document_node, node_id, 1, "Hue Shift", NumberInput::default().min(-180.).max(180.).unit("°"), true); + let saturation_shift = number_widget(document_node, node_id, 2, "Saturation Shift", NumberInput::default().min(-100.).max(100.).unit("%"), true); + let lightness_shift = number_widget(document_node, node_id, 3, "Lightness Shift", NumberInput::default().min(-100.).max(100.).unit("%"), true); vec![ LayoutGroup::Row { widgets: hue_shift }, @@ -183,25 +170,26 @@ pub fn adjust_hsl_properties(document_node: &DocumentNode, node_id: NodeId, _con } pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let brightness = number_widget(document_node, node_id, 1, "Brightness", NumberInput::new().min(-255.).max(255.), true); - let contrast = number_widget(document_node, node_id, 2, "Contrast", NumberInput::new().min(-255.).max(255.), true); + let brightness = number_widget(document_node, node_id, 1, "Brightness", NumberInput::default().min(-255.).max(255.), true); + let contrast = number_widget(document_node, node_id, 2, "Contrast", NumberInput::default().min(-255.).max(255.), true); vec![LayoutGroup::Row { widgets: brightness }, LayoutGroup::Row { widgets: contrast }] } pub fn blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let radius = number_widget(document_node, node_id, 1, "Radius", NumberInput::new().min(0.).max(20.).int(), true); - let sigma = number_widget(document_node, node_id, 2, "Sigma", NumberInput::new().min(0.).max(10000.), true); + let radius = number_widget(document_node, node_id, 1, "Radius", NumberInput::default().min(0.).max(20.).int(), true); + let sigma = number_widget(document_node, node_id, 2, "Sigma", NumberInput::default().min(0.).max(10000.), true); vec![LayoutGroup::Row { widgets: radius }, LayoutGroup::Row { widgets: sigma }] } pub fn adjust_gamma_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let gamma = number_widget(document_node, node_id, 1, "Gamma", NumberInput::new().min(0.01), true); + let gamma = number_widget(document_node, node_id, 1, "Gamma", NumberInput::default().min(0.01), true); vec![LayoutGroup::Row { widgets: gamma }] } +#[cfg(feature = "gpu")] pub fn gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let map = text_widget(document_node, node_id, 1, "Map", true); @@ -209,32 +197,33 @@ pub fn gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, _contex } pub fn multiply_opacity(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let gamma = number_widget(document_node, node_id, 1, "Factor", NumberInput::new().min(0.).max(1.), true); + let gamma = number_widget(document_node, node_id, 1, "Factor", NumberInput::default().min(0.).max(1.), true); vec![LayoutGroup::Row { widgets: gamma }] } pub fn posterize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let value = number_widget(document_node, node_id, 1, "Levels", NumberInput::new().min(2.).max(255.).int(), true); + let value = number_widget(document_node, node_id, 1, "Levels", NumberInput::default().min(2.).max(255.).int(), true); vec![LayoutGroup::Row { widgets: value }] } +#[cfg(feature = "quantization")] pub fn quantize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let value = number_widget(document_node, node_id, 1, "Levels", NumberInput::new().min(1.).max(1000.).int(), true); - let index = number_widget(document_node, node_id, 1, "Fit Fn Index", NumberInput::new().min(0.).max(2.).int(), true); + let value = number_widget(document_node, node_id, 1, "Levels", NumberInput::default().min(1.).max(1000.).int(), true); + let index = number_widget(document_node, node_id, 1, "Fit Fn Index", NumberInput::default().min(0.).max(2.).int(), true); vec![LayoutGroup::Row { widgets: value }, LayoutGroup::Row { widgets: index }] } pub fn exposure_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let value = number_widget(document_node, node_id, 1, "Value", NumberInput::new().min(-3.).max(3.), true); + let value = number_widget(document_node, node_id, 1, "Value", NumberInput::default().min(-3.).max(3.), true); vec![LayoutGroup::Row { widgets: value }] } pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let operand = |name: &str, index| { - let widgets = number_widget(document_node, node_id, index, name, NumberInput::new(), true); + let widgets = number_widget(document_node, node_id, index, name, NumberInput::default(), true); LayoutGroup::Row { widgets } }; @@ -254,21 +243,17 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(vec2.x), - label: "X".into(), - unit: " px".into(), - on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index), - ..NumberInput::default() - })), + NumberInput::new(Some(vec2.x)) + .label("X") + .unit(" px") + .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index)) + .widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(vec2.y), - label: "Y".into(), - unit: " px".into(), - on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, number_input.value.unwrap())), node_id, index), - ..NumberInput::default() - })), + NumberInput::new(Some(vec2.y)) + .label("Y") + .unit(" px") + .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index)) + .widget_holder(), ]); } @@ -287,15 +272,13 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(val.to_degrees()), - unit: "°".into(), - mode: NumberInputMode::Range, - range_min: Some(-180.), - range_max: Some(180.), - on_update: update_value(|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()), node_id, index), - ..NumberInput::default() - })), + NumberInput::new(Some(val.to_degrees())) + .unit("°") + .mode(NumberInputMode::Range) + .range_min(Some(-180.)) + .range_max(Some(180.)) + .on_update(update_value(|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()), node_id, index)) + .widget_holder(), ]); } @@ -314,21 +297,15 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(vec2.x), - label: "X".into(), - unit: "".into(), - on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index), - ..NumberInput::default() - })), + NumberInput::new(Some(vec2.x)) + .label("X") + .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index)) + .widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(vec2.y), - label: "Y".into(), - unit: "".into(), - on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, number_input.value.unwrap())), node_id, index), - ..NumberInput::default() - })), + NumberInput::new(Some(vec2.y)) + .label("Y") + .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index)) + .widget_holder(), ]); } @@ -377,23 +354,17 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte let widgets = vec![ WidgetHolder::text_widget("Server"), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Settings".into(), - tooltip: "Preferences: Imaginate".into(), - on_update: WidgetCallback::new(|_| DialogMessage::RequestPreferencesDialog.into()), - ..Default::default() - })), + IconButton::new("Settings", 24) + .tooltip("Preferences: Imaginate") + .on_update(|_| DialogMessage::RequestPreferencesDialog.into()) + .widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::bold_text(status), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Reload".into(), - tooltip: "Refresh connection status".into(), - on_update: WidgetCallback::new(|_| PortfolioMessage::ImaginateCheckServerStatus.into()), - ..Default::default() - })), + IconButton::new("Reload", 24) + .tooltip("Refresh connection status") + .on_update(|_| PortfolioMessage::ImaginateCheckServerStatus.into()) + .widget_holder(), ]; LayoutGroup::Row { widgets }.with_tooltip("Connection status to the server that computes generated images") }; @@ -458,70 +429,58 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte match imaginate_status { ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => { widgets.extend_from_slice(&assist_separators); - widgets.push(WidgetHolder::new(Widget::TextButton(TextButton { - label: "Beginning...".into(), - tooltip: "Sending image generation request to the server".into(), - disabled: true, - ..Default::default() - }))); + widgets.push(TextButton::new("Beginning...").tooltip("Sending image generation request to the server").disabled(true).widget_holder()); } ImaginateStatus::Generating => { widgets.extend_from_slice(&assist_separators); - widgets.push(WidgetHolder::new(Widget::TextButton(TextButton { - label: "Terminate".into(), - tooltip: "Cancel the in-progress image generation and keep the latest progress".into(), - on_update: WidgetCallback::new(move |_| { - DocumentMessage::NodeGraphFrameImaginateTerminate { - layer_path: layer_path.clone(), - node_path: imaginate_node.clone(), - } - .into() - }), - ..Default::default() - }))); + widgets.push( + TextButton::new("Terminate") + .tooltip("Cancel the in-progress image generation and keep the latest progress") + .on_update(move |_| { + DocumentMessage::NodeGraphFrameImaginateTerminate { + layer_path: layer_path.clone(), + node_path: imaginate_node.clone(), + } + .into() + }) + .widget_holder(), + ); } ImaginateStatus::Terminating => { widgets.extend_from_slice(&assist_separators); - widgets.push(WidgetHolder::new(Widget::TextButton(TextButton { - label: "Terminating...".into(), - tooltip: "Waiting on the final image generated after termination".into(), - disabled: true, - ..Default::default() - }))); + widgets.push( + TextButton::new("Terminating...") + .tooltip("Waiting on the final image generated after termination") + .disabled(true) + .widget_holder(), + ); } ImaginateStatus::Idle | ImaginateStatus::Terminated => widgets.extend_from_slice(&[ - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Random".into(), - tooltip: "Generate with a new random seed".into(), - on_update: WidgetCallback::new(move |_| { + IconButton::new("Random", 24) + .tooltip("Generate with a new random seed") + .on_update(move |_| { DocumentMessage::NodeGraphFrameImaginateRandom { imaginate_node: imaginate_node.clone(), } .into() - }), - ..Default::default() - })), + }) + .widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Generate".into(), - tooltip: "Fill layer frame by generating a new image".into(), - on_update: WidgetCallback::new(move |_| { + TextButton::new("Generate") + .tooltip("Fill layer frame by generating a new image") + .on_update(move |_| { DocumentMessage::NodeGraphFrameImaginate { imaginate_node: imaginate_node_1.clone(), } .into() - }), - ..Default::default() - })), + }) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Clear".into(), - tooltip: "Remove generated image from the layer frame".into(), - disabled: cached_data.is_none(), - on_update: update_value(|_| TaggedValue::RcImage(None), node_id, cached_index), - ..Default::default() - })), + TextButton::new("Clear") + .tooltip("Remove generated image from the layer frame") + .disabled(cached_data.is_none()) + .on_update(update_value(|_| TaggedValue::RcImage(None), node_id, cached_index)) + .widget_holder(), ]), } LayoutGroup::Row { widgets }.with_tooltip("Buttons that control the image generation process") @@ -538,21 +497,16 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Regenerate".into(), - tooltip: "Set a new random seed".into(), - on_update: update_value(move |_| TaggedValue::F64((generate_uuid() >> 1) as f64), node_id, seed_index), - ..Default::default() - })), + IconButton::new("Regenerate", 24) + .tooltip("Set a new random seed") + .on_update(update_value(move |_| TaggedValue::F64((generate_uuid() >> 1) as f64), node_id, seed_index)) + .widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(seed), - min: Some(0.), - is_integer: true, - on_update: update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index), - ..Default::default() - })), + NumberInput::new(Some(seed)) + .min(0.) + .int() + .on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index)) + .widget_holder(), ]) } // Note: Limited by f64. You cannot even have all the possible u64 values :) @@ -588,25 +542,21 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte let layer_path = context.layer_path.to_vec(); widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "Rescale".into(), - tooltip: "Set the Node Graph Frame layer dimensions to this resolution".into(), - on_update: WidgetCallback::new(move |_| { + IconButton::new("Rescale", 24) + .tooltip("Set the Node Graph Frame layer dimensions to this resolution") + .on_update(move |_| { Operation::SetLayerScaleAroundPivot { path: layer_path.clone(), new_scale: vec2.into(), } .into() - }), - ..Default::default() - })), + }) + .widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: !dimensions_is_auto, - icon: "Edit".into(), - tooltip: "Set a custom resolution instead of using the frame's rounded dimensions".into(), - on_update: update_value( + CheckboxInput::new(!dimensions_is_auto) + .icon("Edit") + .tooltip("Set a custom resolution instead of using the frame's rounded dimensions") + .on_update(update_value( move |checkbox_input: &CheckboxInput| { if checkbox_input.checked { TaggedValue::OptionalDVec2(Some(vec2)) @@ -616,35 +566,30 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte }, node_id, resolution_index, - ), - ..CheckboxInput::default() - })), + )) + .widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(vec2.x), - label: "W".into(), - unit: " px".into(), - disabled: dimensions_is_auto, - on_update: update_value( + NumberInput::new(Some(vec2.x)) + .label("W") + .unit(" px") + .disabled(dimensions_is_auto) + .on_update(update_value( move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(number_input.value.unwrap(), vec2.y))), node_id, resolution_index, - ), - ..NumberInput::default() - })), + )) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(vec2.y), - label: "H".into(), - unit: " px".into(), - disabled: dimensions_is_auto, - on_update: update_value( + NumberInput::new(Some(vec2.y)) + .label("H") + .unit(" px") + .disabled(dimensions_is_auto) + .on_update(update_value( move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(vec2.x, number_input.value.unwrap()))), node_id, resolution_index, - ), - ..NumberInput::default() - })), + )) + .widget_holder(), ]) } LayoutGroup::Row { widgets }.with_tooltip( @@ -656,7 +601,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte }; let sampling_steps = { - let widgets = number_widget(document_node, node_id, samples_index, "Sampling Steps", NumberInput::new().min(0.).max(150.).int(), true); + let widgets = number_widget(document_node, node_id, samples_index, "Sampling Steps", NumberInput::default().min(0.).max(150.).int(), true); LayoutGroup::Row { widgets }.with_tooltip("Number of iterations to improve the image generation quality, with diminishing returns around 40 when using the Euler A sampling method") }; @@ -671,28 +616,20 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte let sampling_methods = ImaginateSamplingMethod::list(); let mut entries = Vec::with_capacity(sampling_methods.len()); for method in sampling_methods { - entries.push(DropdownEntryData { - label: method.to_string(), - on_update: update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index), - ..DropdownEntryData::default() - }); + entries.push(DropdownEntryData::new(method.to_string()).on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index))); } let entries = vec![entries]; widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::DropdownInput(DropdownInput { - entries, - selected_index: Some(sampling_method as u32), - ..Default::default() - })), + DropdownInput::new(entries).selected_index(Some(sampling_method as u32)).widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip("Algorithm used to generate the image during each sampling step") }; let text_guidance = { - let widgets = number_widget(document_node, node_id, text_guidance_index, "Prompt Guidance", NumberInput::new().min(0.).max(30.), true); + let widgets = number_widget(document_node, node_id, text_guidance_index, "Prompt Guidance", NumberInput::default().min(0.).max(30.), true); LayoutGroup::Row { widgets }.with_tooltip( "Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\ \n\ @@ -722,7 +659,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte LayoutGroup::Row { widgets }.with_tooltip("Generate an image based upon the bitmap data plugged into this node") }; let image_creativity = { - let props = NumberInput::new().percentage().disabled(!use_base_image); + let props = NumberInput::default().percentage().disabled(!use_base_image); let widgets = number_widget(document_node, node_id, img_creativity_index, "Image Creativity", props, true); LayoutGroup::Row { widgets }.with_tooltip( "Strength of the artistic liberties allowing changes from the input image. The image is unchanged at 0% and completely different at 100%.\n\ @@ -752,14 +689,10 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::LayerReferenceInput(LayerReferenceInput { - value: layer_path.clone(), - layer_name: layer_reference_input_layer_name.cloned(), - layer_type: layer_reference_input_layer_type.cloned(), - disabled: !use_base_image, - on_update: update_value(|input: &LayerReferenceInput| TaggedValue::LayerPath(input.value.clone()), node_id, mask_index), - ..Default::default() - })), + LayerReferenceInput::new(layer_path.clone(), layer_reference_input_layer_name.cloned(), layer_reference_input_layer_type.cloned()) + .disabled(!use_base_image) + .on_update(update_value(|input: &LayerReferenceInput| TaggedValue::LayerPath(input.value.clone()), node_id, mask_index)) + .widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip( @@ -796,18 +729,14 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte { widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::RadioInput(RadioInput { - entries: [(true, "Inpaint"), (false, "Outpaint")] + RadioInput::new( + [(true, "Inpaint"), (false, "Outpaint")] .into_iter() - .map(|(paint, name)| RadioEntryData { - label: name.to_string(), - on_update: update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index), - ..Default::default() - }) + .map(|(paint, name)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index))) .collect(), - selected_index: 1 - in_paint as u32, - ..Default::default() - })), + ) + .selected_index(1 - in_paint as u32) + .widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip( @@ -820,7 +749,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte }; let blur_radius = { - let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", NumberInput::new().unit(" px").min(0.).max(25.).int(), true); + let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", NumberInput::default().unit(" px").min(0.).max(25.).int(), true); LayoutGroup::Row { widgets }.with_tooltip("Blur radius for the mask. Useful for softening sharp edges to blend the masked area with the rest of the image.") }; @@ -835,21 +764,13 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte let mask_fill_content_modes = ImaginateMaskStartingFill::list(); let mut entries = Vec::with_capacity(mask_fill_content_modes.len()); for mode in mask_fill_content_modes { - entries.push(DropdownEntryData { - label: mode.to_string(), - on_update: update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index), - ..DropdownEntryData::default() - }); + entries.push(DropdownEntryData::new(mode.to_string()).on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index))); } let entries = vec![entries]; widgets.extend_from_slice(&[ WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::DropdownInput(DropdownInput { - entries, - selected_index: Some(starting_fill as u32), - ..Default::default() - })), + DropdownInput::new(entries).selected_index(Some(starting_fill as u32)).widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip( diff --git a/proc-macros/src/lib.rs b/proc-macros/src/lib.rs index 273cd4b2c..d953cebf2 100644 --- a/proc-macros/src/lib.rs +++ b/proc-macros/src/lib.rs @@ -5,6 +5,7 @@ mod helper_structs; mod helpers; mod hint; mod transitive_child; +mod widget_builder; use crate::as_message::derive_as_message_impl; use crate::combined_message_attrs::combined_message_attrs_impl; @@ -12,6 +13,7 @@ use crate::discriminant::derive_discriminant_impl; use crate::helper_structs::AttrInnerSingleString; use crate::hint::derive_hint_impl; use crate::transitive_child::derive_transitive_child_impl; +use crate::widget_builder::derive_widget_builder_impl; use proc_macro::TokenStream; @@ -273,6 +275,11 @@ pub fn edge(attr: TokenStream, item: TokenStream) -> TokenStream { item } +#[proc_macro_derive(WidgetBuilder, attributes(widget_builder))] +pub fn derive_widget_builder(input_item: TokenStream) -> TokenStream { + TokenStream::from(derive_widget_builder_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error())) +} + #[cfg(test)] mod tests { use super::*; diff --git a/proc-macros/src/widget_builder.rs b/proc-macros/src/widget_builder.rs new file mode 100644 index 000000000..3b31eb0d6 --- /dev/null +++ b/proc-macros/src/widget_builder.rs @@ -0,0 +1,162 @@ +use proc_macro2::{Ident, Literal, TokenStream as TokenStream2}; +use quote::ToTokens; +use syn::spanned::Spanned; +use syn::{Attribute, Data, DeriveInput, Field, PathArguments, Type}; + +/// Check if a specified `#[widget_builder target]` attribute can be found in the list +fn has_attribute(attrs: &[Attribute], target: &str) -> bool { + attrs + .iter() + .filter(|attr| attr.path.to_token_stream().to_string() == "widget_builder") + .any(|attr| attr.tokens.to_token_stream().to_string() == target) +} + +/// Make setting strings easier by allowing all types that `impl Into` +/// +/// Returns the new input type and a conversion to the origional. +fn easier_string_assignment(field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) { + if let Type::Path(type_path) = field_ty { + if let Some(last_segement) = type_path.path.segments.last() { + // Check if this type is a `String` + // Based on https://stackoverflow.com/questions/66906261/rust-proc-macro-derive-how-do-i-check-if-a-field-is-of-a-primitive-type-like-b + if last_segement.ident == Ident::new("String", last_segement.ident.span()) { + return ( + quote::quote_spanned!(type_path.span() => impl Into), + quote::quote_spanned!(field_ident.span() => #field_ident.into()), + ); + } + } + } + (quote::quote_spanned!(field_ty.span() => #field_ty), quote::quote_spanned!(field_ident.span() => #field_ident)) +} + +/// Extract the identifier of the field (which should always be present) +fn extract_ident(field: &Field) -> syn::Result<&Ident> { + field + .ident + .as_ref() + .ok_or_else(|| syn::Error::new_spanned(field, "Constructing a builder not supported for unnamed fields")) +} + +/// Find the type passed into the builder and the right hand side of the assignment. +/// +/// Applies special behaviour for easier String and WidgetCallback assignment. +fn find_type_and_assignment(field: &Field) -> syn::Result<(TokenStream2, TokenStream2)> { + let field_ty = &field.ty; + let field_ident = extract_ident(field)?; + + let (mut function_input_ty, mut assignment) = easier_string_assignment(field_ty, field_ident); + + // Check if type is `WidgetCallback` + if let Type::Path(type_path) = field_ty { + if let Some(last_segement) = type_path.path.segments.last() { + if let PathArguments::AngleBracketed(generic_args) = &last_segement.arguments { + if let Some(first_generic) = generic_args.args.first() { + if last_segement.ident == Ident::new("WidgetCallback", last_segement.ident.span()) { + // Assign builder pattern to assign the closure directly + function_input_ty = quote::quote_spanned!(field_ty.span() => impl Fn(&#first_generic) -> crate::messages::message::Message + 'static + Send + Sync); + assignment = quote::quote_spanned!(field_ident.span() => crate::messages::layout::utility_types::layout_widget::WidgetCallback::new(#field_ident)); + } + } + } + } + } + Ok((function_input_ty, assignment)) +} + +// Construct a builder function for a specific field in the struct +fn construct_builder(field: &Field) -> syn::Result { + // Check if this field should be skipped with `#[widget_builder(skip)]` + if has_attribute(&field.attrs, "(skip)") { + return Ok(Default::default()); + } + let field_ident = extract_ident(field)?; + + // Create a doc comment literal describing the behaviour of the function + let doc_comment = Literal::string(&format!("Set the `{field_ident}` field using a builder pattern.")); + + let (function_input_ty, assignment) = find_type_and_assignment(field)?; + + // Create builder function + Ok(quote::quote_spanned!(field.span() => + #[doc = #doc_comment] + pub fn #field_ident(mut self, #field_ident: #function_input_ty) -> Self{ + self.#field_ident = #assignment; + self + } + )) +} + +pub fn derive_widget_builder_impl(input_item: TokenStream2) -> syn::Result { + let input = syn::parse2::(input_item)?; + + let struct_name_ident = input.ident; + + // Extract the struct fields + let fields = match &input.data { + Data::Enum(enum_data) => return Err(syn::Error::new_spanned(enum_data.enum_token, "Derive widget builder is not supported for enums")), + Data::Union(union_data) => return Err(syn::Error::new_spanned(union_data.union_token, "Derive widget builder is not supported for unions")), + Data::Struct(struct_data) => &struct_data.fields, + }; + + // Create functions based on each field + let builder_functions = fields.iter().map(construct_builder).collect::, _>>()?; + + // Check if this should not have the `widget_holder()` function due to a `#[widget_builder(not_widget_holder)]` attribute + let widget_holder_fn = if !has_attribute(&input.attrs, "(not_widget_holder)") { + // A doc comment for the widget_holder function + let widget_holder_doc_comment = Literal::string(&format!("Wrap {struct_name_ident} as a WidgetHolder.")); + + // Construct the `widget_holder` function + quote::quote! { + #[doc = #widget_holder_doc_comment] + pub fn widget_holder(self) -> crate::messages::layout::utility_types::layout_widget::WidgetHolder{ + crate::messages::layout::utility_types::layout_widget::WidgetHolder::new( crate::messages::layout::utility_types::layout_widget::Widget::#struct_name_ident(self)) + } + } + } else { + quote::quote!() + }; + + // The new function takes any fields tagged with `#[widget_builder(constructor)]` as arguments. + let new_fn = { + // A doc comment for the new function + let new_doc_comment = Literal::string(&format!("Create a new {struct_name_ident}, based on default values.")); + + let is_constructor = |field: &Field| has_attribute(&field.attrs, "(constructor)"); + + let idents = fields.iter().filter(|field| is_constructor(field)).map(extract_ident).collect::, _>>()?; + let types_and_assignments = fields.iter().filter(|field| is_constructor(field)).map(find_type_and_assignment).collect::, _>>()?; + let (types, assignments): (Vec<_>, Vec<_>) = types_and_assignments.into_iter().unzip(); + + let construction = if idents.is_empty() { + quote::quote!(Default::default()) + } else { + let default = (idents.len() != fields.len()).then_some(quote::quote!(..Default::default())).unwrap_or_default(); + quote::quote! { + Self { + #(#idents: #assignments,)* + #default + } + } + }; + + quote::quote! { + #[doc = #new_doc_comment] + pub fn new(#(#idents: #types),*) -> Self { + #construction + } + } + }; + + // Construct the code block + Ok(quote::quote! { + impl #struct_name_ident { + #new_fn + + #(#builder_functions)* + + #widget_holder_fn + } + }) +}