Use builder pattern for widgets (#941)

* Use builder pattern for widgets

* Arguments to new function
This commit is contained in:
0HyperCube 2023-01-08 14:34:24 +00:00 committed by Keavon Chambers
parent 1a6ee7c542
commit de407f8b23
8 changed files with 412 additions and 304 deletions

View File

@ -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<String>) -> Self {
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: text.into(),
..Default::default()
}))
TextLabel::new(text).widget_holder()
}
pub fn bold_text(text: impl Into<String>) -> 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<WidgetDiff>) {

View File

@ -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::*;
}

View File

@ -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<IconButton>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)]
pub struct PopoverButton {
pub icon: Option<String>,
@ -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<ActionKeys>,
}
#[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<ParameterExposeButton>,
}
#[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<String>,
@ -91,10 +97,11 @@ pub struct TextButton {
pub on_update: WidgetCallback<TextButton>,
}
#[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<String>,
pub disabled: bool,

View File

@ -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<Color>,
// TODO: Add allow_none
@ -62,9 +65,10 @@ pub struct ColorInput {
pub on_update: WidgetCallback<ColorInput>,
}
#[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<Vec<DropdownEntryData>>;
#[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<Vec<LayerId>>,
#[serde(rename = "layerName")]
#[widget_builder(constructor)]
pub layer_name: Option<String>,
#[serde(rename = "layerType")]
#[widget_builder(constructor)]
pub layer_type: Option<LayerDataTypeDiscriminant>,
pub disabled: bool,
@ -178,7 +189,7 @@ pub struct LayerReferenceInput {
pub on_update: WidgetCallback<LayerReferenceInput>,
}
#[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<f64>,
#[widget_builder(skip)]
pub min: Option<f64>,
#[widget_builder(skip)]
pub max: Option<f64>,
#[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<String>) -> 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<OptionalInput>,
}
#[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<RadioEntryData>,
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<String>,
@ -374,9 +381,10 @@ pub struct TextAreaInput {
pub on_update: WidgetCallback<TextAreaInput>,
}
#[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<String>,

View File

@ -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,
}

View File

@ -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<String>) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row { widgets: vec![widget] }]
}
fn update_value<T, F: Fn(&T) -> TaggedValue + 'static + Send + Sync>(value: F, node_id: NodeId, input_index: usize) -> WidgetCallback<T> {
WidgetCallback::new(move |input_value: &T| {
fn update_value<T>(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<WidgetHolder> {
@ -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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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 |_| {
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()
}),
..Default::default()
})));
})
.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(

View File

@ -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::*;

View File

@ -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<String>`
///
/// 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<String>),
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<TokenStream2> {
// 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<TokenStream2> {
let input = syn::parse2::<DeriveInput>(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::<Result<Vec<_>, _>>()?;
// 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::<Result<Vec<_>, _>>()?;
let types_and_assignments = fields.iter().filter(|field| is_constructor(field)).map(find_type_and_assignment).collect::<Result<Vec<_>, _>>()?;
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
}
})
}