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 } Self { widget_id: generate_uuid(), widget }
} }
pub fn unrelated_separator() -> Self { pub fn unrelated_separator() -> Self {
WidgetHolder::new(Widget::Separator(Separator { Separator::new(SeparatorDirection::Horizontal, SeparatorType::Unrelated).widget_holder()
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
}))
} }
pub fn related_separator() -> Self { pub fn related_separator() -> Self {
WidgetHolder::new(Widget::Separator(Separator { Separator::new(SeparatorDirection::Horizontal, SeparatorType::Related).widget_holder()
separator_type: SeparatorType::Related,
direction: SeparatorDirection::Horizontal,
}))
} }
pub fn text_widget(text: impl Into<String>) -> Self { pub fn text_widget(text: impl Into<String>) -> Self {
WidgetHolder::new(Widget::TextLabel(TextLabel { TextLabel::new(text).widget_holder()
value: text.into(),
..Default::default()
}))
} }
pub fn bold_text(text: impl Into<String>) -> Self { pub fn bold_text(text: impl Into<String>) -> Self {
WidgetHolder::new(Widget::TextLabel(TextLabel { TextLabel::new(text).bold(true).widget_holder()
value: text.into(),
bold: true,
..Default::default()
}))
} }
/// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. /// 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>) { 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 layout_widget;
pub mod misc; pub mod misc;
pub mod widgets; 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 crate::messages::{input_mapper::utility_types::misc::ActionKeys, portfolio::document::node_graph::FrontendGraphDataType};
use derivative::*; use derivative::*;
use graphite_proc_macros::WidgetBuilder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct IconButton { pub struct IconButton {
#[widget_builder(constructor)]
pub icon: String, pub icon: String,
#[widget_builder(constructor)]
pub size: u32, // TODO: Convert to an `IconSize` enum pub size: u32, // TODO: Convert to an `IconSize` enum
pub disabled: bool, pub disabled: bool,
@ -26,7 +29,7 @@ pub struct IconButton {
pub on_update: WidgetCallback<IconButton>, pub on_update: WidgetCallback<IconButton>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct PopoverButton { pub struct PopoverButton {
pub icon: Option<String>, pub icon: Option<String>,
@ -34,9 +37,11 @@ pub struct PopoverButton {
pub disabled: bool, pub disabled: bool,
// Placeholder popover content heading // Placeholder popover content heading
#[widget_builder(constructor)]
pub header: String, pub header: String,
// Placeholder popover content paragraph // Placeholder popover content paragraph
#[widget_builder(constructor)]
pub text: String, pub text: String,
pub tooltip: String, pub tooltip: String,
@ -45,7 +50,7 @@ pub struct PopoverButton {
pub tooltip_shortcut: Option<ActionKeys>, pub tooltip_shortcut: Option<ActionKeys>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
pub struct ParameterExposeButton { pub struct ParameterExposeButton {
@ -65,10 +70,11 @@ pub struct ParameterExposeButton {
pub on_update: WidgetCallback<ParameterExposeButton>, pub on_update: WidgetCallback<ParameterExposeButton>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
pub struct TextButton { pub struct TextButton {
#[widget_builder(constructor)]
pub label: String, pub label: String,
pub icon: Option<String>, pub icon: Option<String>,
@ -91,10 +97,11 @@ pub struct TextButton {
pub on_update: WidgetCallback<TextButton>, pub on_update: WidgetCallback<TextButton>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
pub struct BreadcrumbTrailButtons { pub struct BreadcrumbTrailButtons {
#[widget_builder(constructor)]
pub labels: Vec<String>, pub labels: Vec<String>,
pub disabled: bool, 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 crate::messages::layout::utility_types::layout_widget::WidgetCallback;
use document_legacy::{color::Color, layers::layer_info::LayerDataTypeDiscriminant, LayerId}; use document_legacy::{color::Color, layers::layer_info::LayerDataTypeDiscriminant, LayerId};
use graphite_proc_macros::WidgetBuilder;
use derivative::*; use derivative::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Derivative, Serialize, Deserialize)] #[derive(Clone, Derivative, Serialize, Deserialize, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct CheckboxInput { pub struct CheckboxInput {
#[widget_builder(constructor)]
pub checked: bool, pub checked: bool,
pub disabled: 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)] #[derivative(Debug, PartialEq, Default)]
pub struct ColorInput { pub struct ColorInput {
#[widget_builder(constructor)]
pub value: Option<Color>, pub value: Option<Color>,
// TODO: Add allow_none // TODO: Add allow_none
@ -62,9 +65,10 @@ pub struct ColorInput {
pub on_update: WidgetCallback<ColorInput>, pub on_update: WidgetCallback<ColorInput>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct DropdownInput { pub struct DropdownInput {
#[widget_builder(constructor)]
pub entries: DropdownInputEntries, 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) // 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>>; pub type DropdownInputEntries = Vec<Vec<DropdownEntryData>>;
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Serialize, Deserialize, Derivative, Default, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
#[widget_builder(not_widget_holder)]
pub struct DropdownEntryData { pub struct DropdownEntryData {
pub value: String, pub value: String,
#[widget_builder(constructor)]
pub label: String, pub label: String,
pub icon: String, pub icon: String,
@ -114,13 +120,15 @@ pub struct DropdownEntryData {
pub on_update: WidgetCallback<()>, pub on_update: WidgetCallback<()>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct FontInput { pub struct FontInput {
#[serde(rename = "fontFamily")] #[serde(rename = "fontFamily")]
#[widget_builder(constructor)]
pub font_family: String, pub font_family: String,
#[serde(rename = "fontStyle")] #[serde(rename = "fontStyle")]
#[widget_builder(constructor)]
pub font_style: String, pub font_style: String,
#[serde(rename = "isStyle")] #[serde(rename = "isStyle")]
@ -142,7 +150,7 @@ pub struct FontInput {
/// This widget allows for the flexible use of the layout system. /// 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. /// 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. /// 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)] #[derivative(Debug, PartialEq)]
pub struct InvisibleStandinInput { pub struct InvisibleStandinInput {
#[serde(skip)] #[serde(skip)]
@ -150,15 +158,18 @@ pub struct InvisibleStandinInput {
pub on_update: WidgetCallback<()>, pub on_update: WidgetCallback<()>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct LayerReferenceInput { pub struct LayerReferenceInput {
#[widget_builder(constructor)]
pub value: Option<Vec<LayerId>>, pub value: Option<Vec<LayerId>>,
#[serde(rename = "layerName")] #[serde(rename = "layerName")]
#[widget_builder(constructor)]
pub layer_name: Option<String>, pub layer_name: Option<String>,
#[serde(rename = "layerType")] #[serde(rename = "layerType")]
#[widget_builder(constructor)]
pub layer_type: Option<LayerDataTypeDiscriminant>, pub layer_type: Option<LayerDataTypeDiscriminant>,
pub disabled: bool, pub disabled: bool,
@ -178,7 +189,7 @@ pub struct LayerReferenceInput {
pub on_update: WidgetCallback<LayerReferenceInput>, pub on_update: WidgetCallback<LayerReferenceInput>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct NumberInput { pub struct NumberInput {
// Label // Label
@ -193,10 +204,13 @@ pub struct NumberInput {
pub disabled: bool, pub disabled: bool,
// Value // Value
#[widget_builder(constructor)]
pub value: Option<f64>, pub value: Option<f64>,
#[widget_builder(skip)]
pub min: Option<f64>, pub min: Option<f64>,
#[widget_builder(skip)]
pub max: Option<f64>, pub max: Option<f64>,
#[serde(rename = "isInteger")] #[serde(rename = "isInteger")]
@ -247,9 +261,6 @@ pub struct NumberInput {
} }
impl NumberInput { impl NumberInput {
pub fn new() -> Self {
Self::default()
}
pub fn int(mut self) -> Self { pub fn int(mut self) -> Self {
self.is_integer = true; self.is_integer = true;
self self
@ -265,20 +276,8 @@ impl NumberInput {
self.mode = NumberInputMode::Range; self.mode = NumberInputMode::Range;
self 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 { pub fn percentage(self) -> Self {
self.min(0.).max(100.).unit("%").dp(2) self.min(0.).max(100.).unit("%").display_decimal_places(2)
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
} }
} }
@ -297,13 +296,15 @@ pub enum NumberInputMode {
Range, Range,
} }
#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct OptionalInput { pub struct OptionalInput {
#[widget_builder(constructor)]
pub checked: bool, pub checked: bool,
pub disabled: bool, pub disabled: bool,
#[widget_builder(constructor)]
pub icon: String, pub icon: String,
pub tooltip: String, pub tooltip: String,
@ -317,9 +318,10 @@ pub struct OptionalInput {
pub on_update: WidgetCallback<OptionalInput>, pub on_update: WidgetCallback<OptionalInput>,
} }
#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct RadioInput { pub struct RadioInput {
#[widget_builder(constructor)]
pub entries: Vec<RadioEntryData>, pub entries: Vec<RadioEntryData>,
pub disabled: bool, pub disabled: bool,
@ -329,11 +331,13 @@ pub struct RadioInput {
pub selected_index: u32, pub selected_index: u32,
} }
#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derive(Clone, Default, Derivative, Serialize, Deserialize, WidgetBuilder)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
#[widget_builder(not_widget_holder)]
pub struct RadioEntryData { pub struct RadioEntryData {
pub value: String, pub value: String,
#[widget_builder(constructor)]
pub label: String, pub label: String,
pub icon: String, pub icon: String,
@ -349,17 +353,20 @@ pub struct RadioEntryData {
pub on_update: WidgetCallback<()>, pub on_update: WidgetCallback<()>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct SwatchPairInput { pub struct SwatchPairInput {
#[widget_builder(constructor)]
pub primary: Color, pub primary: Color,
#[widget_builder(constructor)]
pub secondary: Color, pub secondary: Color,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct TextAreaInput { pub struct TextAreaInput {
#[widget_builder(constructor)]
pub value: String, pub value: String,
pub label: Option<String>, pub label: Option<String>,
@ -374,9 +381,10 @@ pub struct TextAreaInput {
pub on_update: WidgetCallback<TextAreaInput>, pub on_update: WidgetCallback<TextAreaInput>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct TextInput { pub struct TextInput {
#[widget_builder(constructor)]
pub value: String, pub value: String,
pub label: Option<String>, pub label: Option<String>,

View File

@ -1,8 +1,10 @@
use derivative::*; use derivative::*;
use graphite_proc_macros::WidgetBuilder;
use serde::{Deserialize, Serialize}; 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 { pub struct IconLabel {
#[widget_builder(constructor)]
pub icon: String, pub icon: String,
pub disabled: bool, pub disabled: bool,
@ -10,29 +12,33 @@ pub struct IconLabel {
pub tooltip: String, pub tooltip: String,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, WidgetBuilder)]
pub struct Separator { pub struct Separator {
#[widget_builder(constructor)]
pub direction: SeparatorDirection, pub direction: SeparatorDirection,
#[serde(rename = "type")] #[serde(rename = "type")]
#[widget_builder(constructor)]
pub separator_type: SeparatorType, pub separator_type: SeparatorType,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum SeparatorDirection { pub enum SeparatorDirection {
#[default]
Horizontal, Horizontal,
Vertical, Vertical,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum SeparatorType { pub enum SeparatorType {
Related, Related,
#[default]
Unrelated, Unrelated,
Section, Section,
List, List,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default)] #[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default, WidgetBuilder)]
pub struct TextLabel { pub struct TextLabel {
pub disabled: bool, pub disabled: bool,
@ -51,6 +57,7 @@ pub struct TextLabel {
pub tooltip: String, pub tooltip: String,
// Body // Body
#[widget_builder(constructor)]
pub value: String, pub value: String,
} }

View File

@ -1,5 +1,4 @@
use crate::messages::layout::utility_types::layout_widget::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::layout::utility_types::widgets::{button_widgets::*, input_widgets::*};
use crate::messages::portfolio::utility_types::ImaginateServerStatus; use crate::messages::portfolio::utility_types::ImaginateServerStatus;
use crate::messages::prelude::*; use crate::messages::prelude::*;
@ -18,32 +17,31 @@ pub fn string_properties(text: impl Into<String>) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row { widgets: vec![widget] }] 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> { 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 {
WidgetCallback::new(move |input_value: &T| { move |input_value: &T| {
NodeGraphMessage::SetInputValue { NodeGraphMessage::SetInputValue {
node_id, node_id,
input_index, input_index,
value: value(input_value), value: value(input_value),
} }
.into() .into()
}) }
} }
fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder { fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder {
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { ParameterExposeButton::new()
exposed, .exposed(exposed)
data_type, .data_type(data_type)
tooltip: "Expose this parameter input in node graph".into(), .tooltip("Expose this parameter input in node graph")
on_update: WidgetCallback::new(move |_parameter| { .on_update(move |_parameter| {
NodeGraphMessage::ExposeInput { NodeGraphMessage::ExposeInput {
node_id, node_id,
input_index: index, input_index: index,
new_exposed: !exposed, new_exposed: !exposed,
} }
.into() .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> { 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(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::TextInput(TextInput { TextInput::new(x.clone())
value: x.clone(), .on_update(update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, index))
on_update: update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, index), .widget_holder(),
..TextInput::default()
})),
]) ])
} }
widgets widgets
@ -94,11 +90,9 @@ fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { TextAreaInput::new(x.clone())
value: x.clone(), .on_update(update_value(|x: &TextAreaInput| TaggedValue::String(x.value.clone()), node_id, index))
on_update: update_value(|x: &TextAreaInput| TaggedValue::String(x.value.clone()), node_id, index), .widget_holder(),
..TextAreaInput::default()
})),
]) ])
} }
widgets widgets
@ -114,11 +108,9 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { CheckboxInput::new(*x)
checked: *x, .on_update(update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, index))
on_update: update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, index), .widget_holder(),
..CheckboxInput::default()
})),
]) ])
} }
widgets widgets
@ -134,25 +126,22 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { number_props
value: Some(x), .value(Some(x))
on_update: update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index), .on_update(update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index))
..number_props .widget_holder(),
})),
]) ])
} } else if let NodeInput::Value {
if let NodeInput::Value {
tagged_value: TaggedValue::U32(x), tagged_value: TaggedValue::U32(x),
exposed: false, exposed: false,
} = document_node.inputs[index] } = document_node.inputs[index]
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { number_props
value: Some(x as f64), .value(Some(x as f64))
on_update: update_value(|x: &NumberInput| TaggedValue::U32(x.value.unwrap() as u32), node_id, index), .on_update(update_value(|x: &NumberInput| TaggedValue::U32(x.value.unwrap() as u32), node_id, index))
..NumberInput::default() .widget_holder(),
})),
]) ])
} }
widgets 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 /// 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> { 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 information = WidgetHolder::text_widget("The graph's input is the artwork under the frame layer");
let refresh_button = WidgetHolder::new(Widget::TextButton(TextButton { let refresh_button = TextButton::new("Refresh Input")
label: "Refresh Input".to_string(), .tooltip("Refresh the artwork under the frame")
tooltip: "Refresh the artwork under the frame".to_string(), .on_update(|_| DocumentMessage::NodeGraphFrameGenerate.into())
on_update: WidgetCallback::new(|_| DocumentMessage::NodeGraphFrameGenerate.into()), .widget_holder();
..Default::default()
}));
vec![LayoutGroup::Row { widgets: vec![information] }, LayoutGroup::Row { widgets: vec![refresh_button] }] 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> { 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 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::new().min(-100.).max(100.).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::new().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![ vec![
LayoutGroup::Row { widgets: hue_shift }, 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> { 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 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::new().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 }] 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> { 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 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::new().min(0.).max(10000.), 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 }] 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> { 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 }] vec![LayoutGroup::Row { widgets: gamma }]
} }
#[cfg(feature = "gpu")]
pub fn gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { 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); 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> { 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 }] vec![LayoutGroup::Row { widgets: gamma }]
} }
pub fn posterize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { 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 }] vec![LayoutGroup::Row { widgets: value }]
} }
#[cfg(feature = "quantization")]
pub fn quantize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { 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 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::new().min(0.).max(2.).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 }] vec![LayoutGroup::Row { widgets: value }, LayoutGroup::Row { widgets: index }]
} }
pub fn exposure_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { 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 }] vec![LayoutGroup::Row { widgets: value }]
} }
pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let operand = |name: &str, index| { 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 } LayoutGroup::Row { widgets }
}; };
@ -254,21 +243,17 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(vec2.x))
value: Some(vec2.x), .label("X")
label: "X".into(), .unit(" px")
unit: " px".into(), .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index))
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index), .widget_holder(),
..NumberInput::default()
})),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(vec2.y))
value: Some(vec2.y), .label("Y")
label: "Y".into(), .unit(" px")
unit: " px".into(), .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index))
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, number_input.value.unwrap())), node_id, index), .widget_holder(),
..NumberInput::default()
})),
]); ]);
} }
@ -287,15 +272,13 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(val.to_degrees()))
value: Some(val.to_degrees()), .unit("°")
unit: "°".into(), .mode(NumberInputMode::Range)
mode: NumberInputMode::Range, .range_min(Some(-180.))
range_min: Some(-180.), .range_max(Some(180.))
range_max: Some(180.), .on_update(update_value(|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()), node_id, index))
on_update: update_value(|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()), node_id, index), .widget_holder(),
..NumberInput::default()
})),
]); ]);
} }
@ -314,21 +297,15 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(vec2.x))
value: Some(vec2.x), .label("X")
label: "X".into(), .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index))
unit: "".into(), .widget_holder(),
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index),
..NumberInput::default()
})),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(vec2.y))
value: Some(vec2.y), .label("Y")
label: "Y".into(), .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index))
unit: "".into(), .widget_holder(),
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, number_input.value.unwrap())), node_id, index),
..NumberInput::default()
})),
]); ]);
} }
@ -377,23 +354,17 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
let widgets = vec![ let widgets = vec![
WidgetHolder::text_widget("Server"), WidgetHolder::text_widget("Server"),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::IconButton(IconButton { IconButton::new("Settings", 24)
size: 24, .tooltip("Preferences: Imaginate")
icon: "Settings".into(), .on_update(|_| DialogMessage::RequestPreferencesDialog.into())
tooltip: "Preferences: Imaginate".into(), .widget_holder(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestPreferencesDialog.into()),
..Default::default()
})),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::bold_text(status), WidgetHolder::bold_text(status),
WidgetHolder::related_separator(), WidgetHolder::related_separator(),
WidgetHolder::new(Widget::IconButton(IconButton { IconButton::new("Reload", 24)
size: 24, .tooltip("Refresh connection status")
icon: "Reload".into(), .on_update(|_| PortfolioMessage::ImaginateCheckServerStatus.into())
tooltip: "Refresh connection status".into(), .widget_holder(),
on_update: WidgetCallback::new(|_| PortfolioMessage::ImaginateCheckServerStatus.into()),
..Default::default()
})),
]; ];
LayoutGroup::Row { widgets }.with_tooltip("Connection status to the server that computes generated images") 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 { match imaginate_status {
ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => { ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => {
widgets.extend_from_slice(&assist_separators); widgets.extend_from_slice(&assist_separators);
widgets.push(WidgetHolder::new(Widget::TextButton(TextButton { widgets.push(TextButton::new("Beginning...").tooltip("Sending image generation request to the server").disabled(true).widget_holder());
label: "Beginning...".into(),
tooltip: "Sending image generation request to the server".into(),
disabled: true,
..Default::default()
})));
} }
ImaginateStatus::Generating => { ImaginateStatus::Generating => {
widgets.extend_from_slice(&assist_separators); widgets.extend_from_slice(&assist_separators);
widgets.push(WidgetHolder::new(Widget::TextButton(TextButton { widgets.push(
label: "Terminate".into(), TextButton::new("Terminate")
tooltip: "Cancel the in-progress image generation and keep the latest progress".into(), .tooltip("Cancel the in-progress image generation and keep the latest progress")
on_update: WidgetCallback::new(move |_| { .on_update(move |_| {
DocumentMessage::NodeGraphFrameImaginateTerminate { DocumentMessage::NodeGraphFrameImaginateTerminate {
layer_path: layer_path.clone(), layer_path: layer_path.clone(),
node_path: imaginate_node.clone(), node_path: imaginate_node.clone(),
} }
.into() .into()
}), })
..Default::default() .widget_holder(),
}))); );
} }
ImaginateStatus::Terminating => { ImaginateStatus::Terminating => {
widgets.extend_from_slice(&assist_separators); widgets.extend_from_slice(&assist_separators);
widgets.push(WidgetHolder::new(Widget::TextButton(TextButton { widgets.push(
label: "Terminating...".into(), TextButton::new("Terminating...")
tooltip: "Waiting on the final image generated after termination".into(), .tooltip("Waiting on the final image generated after termination")
disabled: true, .disabled(true)
..Default::default() .widget_holder(),
}))); );
} }
ImaginateStatus::Idle | ImaginateStatus::Terminated => widgets.extend_from_slice(&[ ImaginateStatus::Idle | ImaginateStatus::Terminated => widgets.extend_from_slice(&[
WidgetHolder::new(Widget::IconButton(IconButton { IconButton::new("Random", 24)
size: 24, .tooltip("Generate with a new random seed")
icon: "Random".into(), .on_update(move |_| {
tooltip: "Generate with a new random seed".into(),
on_update: WidgetCallback::new(move |_| {
DocumentMessage::NodeGraphFrameImaginateRandom { DocumentMessage::NodeGraphFrameImaginateRandom {
imaginate_node: imaginate_node.clone(), imaginate_node: imaginate_node.clone(),
} }
.into() .into()
}), })
..Default::default() .widget_holder(),
})),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::TextButton(TextButton { TextButton::new("Generate")
label: "Generate".into(), .tooltip("Fill layer frame by generating a new image")
tooltip: "Fill layer frame by generating a new image".into(), .on_update(move |_| {
on_update: WidgetCallback::new(move |_| {
DocumentMessage::NodeGraphFrameImaginate { DocumentMessage::NodeGraphFrameImaginate {
imaginate_node: imaginate_node_1.clone(), imaginate_node: imaginate_node_1.clone(),
} }
.into() .into()
}), })
..Default::default() .widget_holder(),
})),
WidgetHolder::related_separator(), WidgetHolder::related_separator(),
WidgetHolder::new(Widget::TextButton(TextButton { TextButton::new("Clear")
label: "Clear".into(), .tooltip("Remove generated image from the layer frame")
tooltip: "Remove generated image from the layer frame".into(), .disabled(cached_data.is_none())
disabled: cached_data.is_none(), .on_update(update_value(|_| TaggedValue::RcImage(None), node_id, cached_index))
on_update: update_value(|_| TaggedValue::RcImage(None), node_id, cached_index), .widget_holder(),
..Default::default()
})),
]), ]),
} }
LayoutGroup::Row { widgets }.with_tooltip("Buttons that control the image generation process") 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(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::IconButton(IconButton { IconButton::new("Regenerate", 24)
size: 24, .tooltip("Set a new random seed")
icon: "Regenerate".into(), .on_update(update_value(move |_| TaggedValue::F64((generate_uuid() >> 1) as f64), node_id, seed_index))
tooltip: "Set a new random seed".into(), .widget_holder(),
on_update: update_value(move |_| TaggedValue::F64((generate_uuid() >> 1) as f64), node_id, seed_index),
..Default::default()
})),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(seed))
value: Some(seed), .min(0.)
min: Some(0.), .int()
is_integer: true, .on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index))
on_update: update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index), .widget_holder(),
..Default::default()
})),
]) ])
} }
// Note: Limited by f64. You cannot even have all the possible u64 values :) // 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(); let layer_path = context.layer_path.to_vec();
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::IconButton(IconButton { IconButton::new("Rescale", 24)
size: 24, .tooltip("Set the Node Graph Frame layer dimensions to this resolution")
icon: "Rescale".into(), .on_update(move |_| {
tooltip: "Set the Node Graph Frame layer dimensions to this resolution".into(),
on_update: WidgetCallback::new(move |_| {
Operation::SetLayerScaleAroundPivot { Operation::SetLayerScaleAroundPivot {
path: layer_path.clone(), path: layer_path.clone(),
new_scale: vec2.into(), new_scale: vec2.into(),
} }
.into() .into()
}), })
..Default::default() .widget_holder(),
})),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { CheckboxInput::new(!dimensions_is_auto)
checked: !dimensions_is_auto, .icon("Edit")
icon: "Edit".into(), .tooltip("Set a custom resolution instead of using the frame's rounded dimensions")
tooltip: "Set a custom resolution instead of using the frame's rounded dimensions".into(), .on_update(update_value(
on_update: update_value(
move |checkbox_input: &CheckboxInput| { move |checkbox_input: &CheckboxInput| {
if checkbox_input.checked { if checkbox_input.checked {
TaggedValue::OptionalDVec2(Some(vec2)) TaggedValue::OptionalDVec2(Some(vec2))
@ -616,35 +566,30 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
}, },
node_id, node_id,
resolution_index, resolution_index,
), ))
..CheckboxInput::default() .widget_holder(),
})),
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(vec2.x))
value: Some(vec2.x), .label("W")
label: "W".into(), .unit(" px")
unit: " px".into(), .disabled(dimensions_is_auto)
disabled: dimensions_is_auto, .on_update(update_value(
on_update: update_value(
move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(number_input.value.unwrap(), vec2.y))), move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(number_input.value.unwrap(), vec2.y))),
node_id, node_id,
resolution_index, resolution_index,
), ))
..NumberInput::default() .widget_holder(),
})),
WidgetHolder::related_separator(), WidgetHolder::related_separator(),
WidgetHolder::new(Widget::NumberInput(NumberInput { NumberInput::new(Some(vec2.y))
value: Some(vec2.y), .label("H")
label: "H".into(), .unit(" px")
unit: " px".into(), .disabled(dimensions_is_auto)
disabled: dimensions_is_auto, .on_update(update_value(
on_update: update_value(
move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(vec2.x, number_input.value.unwrap()))), move |number_input: &NumberInput| TaggedValue::OptionalDVec2(round(DVec2::new(vec2.x, number_input.value.unwrap()))),
node_id, node_id,
resolution_index, resolution_index,
), ))
..NumberInput::default() .widget_holder(),
})),
]) ])
} }
LayoutGroup::Row { widgets }.with_tooltip( LayoutGroup::Row { widgets }.with_tooltip(
@ -656,7 +601,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
}; };
let sampling_steps = { 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") 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 sampling_methods = ImaginateSamplingMethod::list();
let mut entries = Vec::with_capacity(sampling_methods.len()); let mut entries = Vec::with_capacity(sampling_methods.len());
for method in sampling_methods { for method in sampling_methods {
entries.push(DropdownEntryData { entries.push(DropdownEntryData::new(method.to_string()).on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index)));
label: method.to_string(),
on_update: update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index),
..DropdownEntryData::default()
});
} }
let entries = vec![entries]; let entries = vec![entries];
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::DropdownInput(DropdownInput { DropdownInput::new(entries).selected_index(Some(sampling_method as u32)).widget_holder(),
entries,
selected_index: Some(sampling_method as u32),
..Default::default()
})),
]); ]);
} }
LayoutGroup::Row { widgets }.with_tooltip("Algorithm used to generate the image during each sampling step") LayoutGroup::Row { widgets }.with_tooltip("Algorithm used to generate the image during each sampling step")
}; };
let text_guidance = { 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( LayoutGroup::Row { widgets }.with_tooltip(
"Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\ "Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\
\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") LayoutGroup::Row { widgets }.with_tooltip("Generate an image based upon the bitmap data plugged into this node")
}; };
let image_creativity = { 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); let widgets = number_widget(document_node, node_id, img_creativity_index, "Image Creativity", props, true);
LayoutGroup::Row { widgets }.with_tooltip( 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\ "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(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::LayerReferenceInput(LayerReferenceInput { LayerReferenceInput::new(layer_path.clone(), layer_reference_input_layer_name.cloned(), layer_reference_input_layer_type.cloned())
value: layer_path.clone(), .disabled(!use_base_image)
layer_name: layer_reference_input_layer_name.cloned(), .on_update(update_value(|input: &LayerReferenceInput| TaggedValue::LayerPath(input.value.clone()), node_id, mask_index))
layer_type: layer_reference_input_layer_type.cloned(), .widget_holder(),
disabled: !use_base_image,
on_update: update_value(|input: &LayerReferenceInput| TaggedValue::LayerPath(input.value.clone()), node_id, mask_index),
..Default::default()
})),
]); ]);
} }
LayoutGroup::Row { widgets }.with_tooltip( LayoutGroup::Row { widgets }.with_tooltip(
@ -796,18 +729,14 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::RadioInput(RadioInput { RadioInput::new(
entries: [(true, "Inpaint"), (false, "Outpaint")] [(true, "Inpaint"), (false, "Outpaint")]
.into_iter() .into_iter()
.map(|(paint, name)| RadioEntryData { .map(|(paint, name)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index)))
label: name.to_string(),
on_update: update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index),
..Default::default()
})
.collect(), .collect(),
selected_index: 1 - in_paint as u32, )
..Default::default() .selected_index(1 - in_paint as u32)
})), .widget_holder(),
]); ]);
} }
LayoutGroup::Row { widgets }.with_tooltip( LayoutGroup::Row { widgets }.with_tooltip(
@ -820,7 +749,7 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
}; };
let blur_radius = { 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.") 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 mask_fill_content_modes = ImaginateMaskStartingFill::list();
let mut entries = Vec::with_capacity(mask_fill_content_modes.len()); let mut entries = Vec::with_capacity(mask_fill_content_modes.len());
for mode in mask_fill_content_modes { for mode in mask_fill_content_modes {
entries.push(DropdownEntryData { entries.push(DropdownEntryData::new(mode.to_string()).on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index)));
label: mode.to_string(),
on_update: update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index),
..DropdownEntryData::default()
});
} }
let entries = vec![entries]; let entries = vec![entries];
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(),
WidgetHolder::new(Widget::DropdownInput(DropdownInput { DropdownInput::new(entries).selected_index(Some(starting_fill as u32)).widget_holder(),
entries,
selected_index: Some(starting_fill as u32),
..Default::default()
})),
]); ]);
} }
LayoutGroup::Row { widgets }.with_tooltip( LayoutGroup::Row { widgets }.with_tooltip(

View File

@ -5,6 +5,7 @@ mod helper_structs;
mod helpers; mod helpers;
mod hint; mod hint;
mod transitive_child; mod transitive_child;
mod widget_builder;
use crate::as_message::derive_as_message_impl; use crate::as_message::derive_as_message_impl;
use crate::combined_message_attrs::combined_message_attrs_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::helper_structs::AttrInnerSingleString;
use crate::hint::derive_hint_impl; use crate::hint::derive_hint_impl;
use crate::transitive_child::derive_transitive_child_impl; use crate::transitive_child::derive_transitive_child_impl;
use crate::widget_builder::derive_widget_builder_impl;
use proc_macro::TokenStream; use proc_macro::TokenStream;
@ -273,6 +275,11 @@ pub fn edge(attr: TokenStream, item: TokenStream) -> TokenStream {
item 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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
}
})
}