From de27f2c006531e0843560932eed32f0d952f3a00 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Tue, 1 Aug 2023 07:21:42 +0100 Subject: [PATCH] Switch to the widget builder pattern on all remaining layouts (#1346) * Prefer widget builder pattern * Nits --------- Co-authored-by: Keavon Chambers --- Cargo.lock | 8 +- Cargo.toml | 2 +- .../export_dialog_message_handler.rs | 145 +--- .../new_document_dialog_message_handler.rs | 126 +--- .../preferences_dialog_message_handler.rs | 120 +-- .../simple_dialogs/about_graphite_dialog.rs | 29 +- .../close_all_documents_dialog.rs | 47 +- .../simple_dialogs/close_document_dialog.rs | 49 +- .../simple_dialogs/coming_soon_dialog.rs | 52 +- .../dialog/simple_dialogs/error_dialog.rs | 28 +- .../layout/utility_types/layout_widget.rs | 1 + .../src/messages/layout/utility_types/misc.rs | 11 + .../utility_types/widgets/menu_widgets.rs | 7 +- .../document/document_message_handler.rs | 432 ++++------- .../node_graph/node_graph_message_handler.rs | 10 +- .../properties_panel/utility_functions.rs | 705 +++++++----------- .../common_functionality/color_selector.rs | 15 +- .../messages/tool/tool_messages/brush_tool.rs | 10 +- .../tool/tool_messages/ellipse_tool.rs | 13 +- .../tool/tool_messages/freehand_tool.rs | 17 +- .../tool/tool_messages/gradient_tool.rs | 3 +- .../messages/tool/tool_messages/line_tool.rs | 13 +- .../messages/tool/tool_messages/path_tool.rs | 2 +- .../messages/tool/tool_messages/pen_tool.rs | 18 +- .../tool/tool_messages/rectangle_tool.rs | 16 +- .../tool/tool_messages/select_tool.rs | 11 +- .../messages/tool/tool_messages/shape_tool.rs | 16 +- .../tool/tool_messages/spline_tool.rs | 17 +- .../messages/tool/tool_messages/text_tool.rs | 40 +- editor/src/messages/tool/utility_types.rs | 116 +-- 30 files changed, 714 insertions(+), 1365 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92b8ac6a..252d33d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4791,8 +4791,8 @@ dependencies = [ [[package]] name = "specta" -version = "1.0.4" -source = "git+https://github.com/0HyperCube/specta.git?rev=d28cd72ea4ea2f6eaa85e0622d3a98340dbb43d3#d28cd72ea4ea2f6eaa85e0622d3a98340dbb43d3" +version = "1.0.5" +source = "git+https://github.com/0HyperCube/specta.git?rev=c47a22b4c0863d27bc47529f300de3969480c66d#c47a22b4c0863d27bc47529f300de3969480c66d" dependencies = [ "document-features", "glam", @@ -4807,8 +4807,8 @@ dependencies = [ [[package]] name = "specta-macros" -version = "1.0.4" -source = "git+https://github.com/0HyperCube/specta.git?rev=d28cd72ea4ea2f6eaa85e0622d3a98340dbb43d3#d28cd72ea4ea2f6eaa85e0622d3a98340dbb43d3" +version = "1.0.5" +source = "git+https://github.com/0HyperCube/specta.git?rev=c47a22b4c0863d27bc47529f300de3969480c66d#c47a22b4c0863d27bc47529f300de3969480c66d" dependencies = [ "Inflector", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 6ebf9854..75d402e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ resolver = "2" exclude = ["node-graph/gpu-compiler"] [workspace.dependencies] -specta = { git = "https://github.com/0HyperCube/specta.git", rev = "d28cd72ea4ea2f6eaa85e0622d3a98340dbb43d3", features = [ +specta = { git = "https://github.com/0HyperCube/specta.git", rev = "c47a22b4c0863d27bc47529f300de3969480c66d", features = [ "glam", ] } xxhash-rust = { version = "0.8", features = ["xxh3"] } diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs index 146caea3..775737e4 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs @@ -1,9 +1,6 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, DropdownEntryData, DropdownInput, NumberInput, RadioEntryData, RadioInput, TextInput}; -use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use document_legacy::LayerId; @@ -47,46 +44,22 @@ impl MessageHandler for ExportDialogMessageHandler { impl PropertyHolder for ExportDialogMessageHandler { fn properties(&self) -> Layout { let file_name = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "File Name".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextInput(TextInput { - value: self.file_name.clone(), - on_update: WidgetCallback::new(|text_input: &TextInput| ExportDialogMessage::FileName(text_input.value.clone()).into()), - ..Default::default() - })), + TextLabel::new("File Name").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + TextInput::new(&self.file_name) + .on_update(|text_input: &TextInput| ExportDialogMessage::FileName(text_input.value.clone()).into()) + .widget_holder(), ]; let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")] .into_iter() - .map(|(val, name)| RadioEntryData { - label: name.into(), - on_update: WidgetCallback::new(move |_| ExportDialogMessage::FileType(val).into()), - ..RadioEntryData::default() - }) + .map(|(val, name)| RadioEntryData::new(name).on_update(move |_| ExportDialogMessage::FileType(val).into())) .collect(); let export_type = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "File Type".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::RadioInput(RadioInput { - selected_index: self.file_type as u32, - entries, - ..Default::default() - })), + TextLabel::new("File Type").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + RadioInput::new(entries).selected_index(self.file_type as u32).widget_holder(), ]; let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string(), false)); @@ -98,98 +71,52 @@ impl PropertyHolder for ExportDialogMessageHandler { let index = export_area_options.iter().position(|(val, _, _)| val == &self.bounds).unwrap(); let entries = vec![export_area_options .into_iter() - .map(|(val, name, disabled)| DropdownEntryData { - label: name, - on_update: WidgetCallback::new(move |_| ExportDialogMessage::ExportBounds(val).into()), - disabled, - ..Default::default() - }) + .map(|(val, name, disabled)| DropdownEntryData::new(name).on_update(move |_| ExportDialogMessage::ExportBounds(val).into()).disabled(disabled)) .collect()]; let export_area = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Bounds".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::DropdownInput(DropdownInput { - selected_index: Some(index as u32), - entries, - ..Default::default() - })), + TextLabel::new("Bounds").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(), ]; let transparent_background = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Transparency".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: self.transparent_background, - disabled: self.file_type == FileType::Jpg, - on_update: WidgetCallback::new(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground(value.checked).into()), - ..Default::default() - })), + TextLabel::new("Transparency").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + CheckboxInput::new(self.transparent_background) + .disabled(self.file_type == FileType::Jpg) + .on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground(value.checked).into()) + .widget_holder(), ]; let resolution = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Scale Factor".into(), - table_align: true, - ..TextLabel::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(self.scale_factor), - label: "".into(), - unit: " ".into(), - min: Some(0.), - disabled: self.file_type == FileType::Svg, - on_update: WidgetCallback::new(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into()), - ..NumberInput::default() - })), + TextLabel::new("Scale Factor").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + NumberInput::new(Some(self.scale_factor)) + .unit(" ") + .min(0.) + .disabled(self.file_type == FileType::Svg) + .on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into()) + .widget_holder(), ]; let button_widgets = vec![ - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Export".to_string(), - min_width: 96, - emphasized: true, - on_update: WidgetCallback::new(|_| { + TextButton::new("Export") + .min_width(96) + .emphasized(true) + .on_update(|_| { DialogMessage::CloseDialogAndThen { followups: vec![ExportDialogMessage::Submit.into()], } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Cancel".to_string(), - min_width: 96, - on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()), - ..Default::default() - })), + }) + .widget_holder(), + TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), ]; Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Export".to_string(), - bold: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new("Export").bold(true).widget_holder()], }, LayoutGroup::Row { widgets: file_name }, LayoutGroup::Row { widgets: export_type }, diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 0d04f350..a564c502 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -1,8 +1,5 @@ -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, NumberInput, TextInput}; -use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use graphene_core::uuid::generate_uuid; @@ -54,103 +51,60 @@ impl MessageHandler for NewDocumentDialogMessageHa impl PropertyHolder for NewDocumentDialogMessageHandler { fn properties(&self) -> Layout { - let title = vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "New document".into(), - bold: true, - ..Default::default() - }))]; + let title = vec![TextLabel::new("New document").bold(true).widget_holder()]; let name = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Name".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextInput(TextInput { - value: self.name.clone(), - on_update: WidgetCallback::new(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into()), - ..Default::default() - })), + TextLabel::new("Name").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + TextInput::new(&self.name) + .on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into()) + .widget_holder(), ]; let infinite = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Infinite Canvas".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: self.infinite, - on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()), - ..Default::default() - })), + TextLabel::new("Infinite Canvas").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + CheckboxInput::new(self.infinite) + .on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()) + .widget_holder(), ]; let scale = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Dimensions".into(), - table_align: true, - ..TextLabel::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - label: "W".into(), - unit: " px".into(), - value: Some(self.dimensions.x as f64), - min: Some(0.), - is_integer: true, - disabled: self.infinite, - min_width: 100, - on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX(number_input.value.unwrap()).into()), - ..NumberInput::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - label: "H".into(), - unit: " px".into(), - value: Some(self.dimensions.y as f64), - min: Some(0.), - is_integer: true, - disabled: self.infinite, - min_width: 100, - on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY(number_input.value.unwrap()).into()), - ..NumberInput::default() - })), + TextLabel::new("Dimensions").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + NumberInput::new(Some(self.dimensions.x as f64)) + .label("W") + .unit(" px") + .min(0.) + .is_integer(true) + .disabled(self.infinite) + .min_width(100) + .on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX(number_input.value.unwrap()).into()) + .widget_holder(), + WidgetHolder::related_separator(), + NumberInput::new(Some(self.dimensions.y as f64)) + .label("H") + .unit(" px") + .min(0.) + .is_integer(true) + .disabled(self.infinite) + .min_width(100) + .on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY(number_input.value.unwrap()).into()) + .widget_holder(), ]; let button_widgets = vec![ - WidgetHolder::new(Widget::TextButton(TextButton { - label: "OK".to_string(), - min_width: 96, - emphasized: true, - on_update: WidgetCallback::new(|_| { + TextButton::new("OK") + .min_width(96) + .emphasized(true) + .on_update(|_| { DialogMessage::CloseDialogAndThen { followups: vec![NewDocumentDialogMessage::Submit.into()], } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Cancel".to_string(), - min_width: 96, - on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()), - ..Default::default() - })), + }) + .widget_holder(), + TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), ]; Layout::WidgetLayout(WidgetLayout::new(vec![ diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index 6bb4ec04..69a4a54d 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -1,8 +1,5 @@ -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, NumberInput, TextInput}; -use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; /// A dialog to allow users to customize Graphite editor options @@ -31,107 +28,62 @@ impl PreferencesDialogMessageHandler { fn properties(&self, preferences: &PreferencesMessageHandler) -> Layout { let zoom_with_scroll = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Input".into(), - min_width: 60, - italic: true, - ..Default::default() - })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Zoom with Scroll".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: preferences.zoom_with_scroll, - tooltip: "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)".into(), - on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| { + TextLabel::new("Input").min_width(60).italic(true).widget_holder(), + TextLabel::new("Zoom with Scroll").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + CheckboxInput::new(preferences.zoom_with_scroll) + .tooltip("Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)") + .on_update(|checkbox_input: &CheckboxInput| { PreferencesMessage::ModifyLayout { zoom_with_scroll: checkbox_input.checked, } .into() - }), - ..Default::default() - })), + }) + .widget_holder(), ]; let imaginate_server_hostname = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Imaginate".into(), - min_width: 60, - italic: true, - ..Default::default() - })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Server Hostname".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::TextInput(TextInput { - value: preferences.imaginate_server_hostname.clone(), - min_width: 200, - on_update: WidgetCallback::new(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into()), - ..Default::default() - })), + TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(), + TextLabel::new("Server Hostname").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + TextInput::new(&preferences.imaginate_server_hostname) + .min_width(200) + .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into()) + .widget_holder(), ]; let imaginate_refresh_frequency = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { min_width: 60, ..Default::default() })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Refresh Frequency".into(), - table_align: true, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - unit: " seconds".into(), - value: Some(preferences.imaginate_refresh_frequency), - min: Some(0.), - min_width: 200, - on_update: WidgetCallback::new(|number_input: &NumberInput| PreferencesMessage::ImaginateRefreshFrequency { seconds: number_input.value.unwrap() }.into()), - ..Default::default() - })), + TextLabel::new("").min_width(60).widget_holder(), + TextLabel::new("Refresh Frequency").table_align(true).widget_holder(), + WidgetHolder::unrelated_separator(), + NumberInput::new(Some(preferences.imaginate_refresh_frequency)) + .unit(" seconds") + .min(0.) + .min_width(200) + .on_update(|number_input: &NumberInput| PreferencesMessage::ImaginateRefreshFrequency { seconds: number_input.value.unwrap() }.into()) + .widget_holder(), ]; let button_widgets = vec![ - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Ok".to_string(), - min_width: 96, - emphasized: true, - on_update: WidgetCallback::new(|_| { + TextButton::new("Ok") + .min_width(96) + .emphasized(true) + .on_update(|_| { DialogMessage::CloseDialogAndThen { followups: vec![PreferencesDialogMessage::Confirm.into()], } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Reset to Defaults".to_string(), - min_width: 96, - on_update: WidgetCallback::new(|_| PreferencesMessage::ResetToDefaults.into()), - ..Default::default() - })), + }) + .widget_holder(), + TextButton::new("Reset to Defaults") + .min_width(96) + .on_update(|_| PreferencesMessage::ResetToDefaults.into()) + .widget_holder(), ]; Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Editor Preferences".to_string(), - bold: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new("Editor Preferences").bold(true).widget_holder()], }, LayoutGroup::Row { widgets: zoom_with_scroll }, LayoutGroup::Row { widgets: imaginate_server_hostname }, diff --git a/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs b/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs index d79be9e9..4a0d2315 100644 --- a/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs @@ -1,7 +1,5 @@ use crate::application::{commit_info_localized, release_series}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; /// A dialog for displaying information on [BuildMetadata] viewable via *Help* > *About Graphite* in the menu bar. @@ -19,34 +17,17 @@ impl PropertyHolder for AboutGraphiteDialog { ]; let link_widgets = links .into_iter() - .map(|(label, url)| { - WidgetHolder::new(Widget::TextButton(TextButton { - label: label.to_string(), - on_update: WidgetCallback::new(|_| FrontendMessage::TriggerVisitLink { url: url.to_string() }.into()), - ..Default::default() - })) - }) + .map(|(label, url)| TextButton::new(label).on_update(|_| FrontendMessage::TriggerVisitLink { url: url.to_string() }.into()).widget_holder()) .collect(); Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Graphite".to_string(), - bold: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new("Graphite".to_string()).bold(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: release_series(), - ..Default::default() - }))], + widgets: vec![TextLabel::new(release_series()).widget_holder()], }, LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: commit_info_localized(self.localized_commit_date.as_str()), - multiline: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_holder()], }, LayoutGroup::Row { widgets: link_widgets }, ])) diff --git a/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs b/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs index 479c9b8c..ae0eeb75 100644 --- a/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs @@ -1,6 +1,4 @@ -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; /// A dialog for confirming the closing of all documents viewable via `file -> close all` in the menu bar. @@ -8,42 +6,25 @@ pub struct CloseAllDocumentsDialog; impl PropertyHolder for CloseAllDocumentsDialog { fn properties(&self) -> Layout { - let button_widgets = vec![ - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Discard All".to_string(), - min_width: 96, - on_update: WidgetCallback::new(|_| { - DialogMessage::CloseDialogAndThen { - followups: vec![PortfolioMessage::CloseAllDocuments.into()], - } - .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Cancel".to_string(), - min_width: 96, - on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()), - ..Default::default() - })), - ]; + let discard = TextButton::new("Discard All") + .min_width(96) + .on_update(|_| { + DialogMessage::CloseDialogAndThen { + followups: vec![PortfolioMessage::CloseAllDocuments.into()], + } + .into() + }) + .widget_holder(); + let cancel = TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(); Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Close all documents?".to_string(), - bold: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new("Close all documents?").multiline(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Unsaved work will be lost!".to_string(), - multiline: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new("Unsaved work will be lost!").multiline(true).widget_holder()], }, - LayoutGroup::Row { widgets: button_widgets }, + LayoutGroup::Row { widgets: vec![discard, cancel] }, ])) } } diff --git a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs index e5e3a43f..5b4a22ec 100644 --- a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs @@ -1,7 +1,5 @@ use crate::messages::broadcast::broadcast_event::BroadcastEvent; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; /// A dialog for confirming the closing a document with unsaved changes. @@ -15,51 +13,34 @@ impl PropertyHolder for CloseDocumentDialog { let document_id = self.document_id; let button_widgets = vec![ - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Save".to_string(), - min_width: 96, - emphasized: true, - on_update: WidgetCallback::new(|_| { + TextButton::new("Save") + .min_width(96) + .emphasized(true) + .on_update(|_| { DialogMessage::CloseDialogAndThen { followups: vec![DocumentMessage::SaveDocument.into()], } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Discard".to_string(), - min_width: 96, - on_update: WidgetCallback::new(move |_| { + }) + .widget_holder(), + TextButton::new("Discard") + .min_width(96) + .on_update(move |_| { DialogMessage::CloseDialogAndThen { followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()], } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Cancel".to_string(), - min_width: 96, - on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()), - ..Default::default() - })), + }) + .widget_holder(), + TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), ]; Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Save changes before closing?".to_string(), - bold: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new("Save changes before closing?").bold(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: self.document_name.clone(), - multiline: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new(&self.document_name).multiline(true).widget_holder()], }, LayoutGroup::Row { widgets: button_widgets }, ])) diff --git a/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs b/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs index 19938242..833210f3 100644 --- a/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs @@ -1,6 +1,4 @@ -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use std::fmt::Write; @@ -13,41 +11,33 @@ pub struct ComingSoonDialog { impl PropertyHolder for ComingSoonDialog { fn properties(&self) -> Layout { let mut details = "This feature is not implemented yet".to_string(); - let mut buttons = vec![WidgetHolder::new(Widget::TextButton(TextButton { - label: "OK".to_string(), - emphasized: true, - min_width: 96, - on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()), - ..Default::default() - }))]; + + let mut buttons = vec![TextButton::new("OK") + .emphasized(true) + .min_width(96) + .on_update(|_| FrontendMessage::DisplayDialogDismiss.into()) + .widget_holder()]; + if let Some(issue) = self.issue { let _ = write!(details, "— but you can help add it!\nSee issue #{issue} on GitHub."); - buttons.push(WidgetHolder::new(Widget::TextButton(TextButton { - label: format!("Issue #{issue}"), - min_width: 96, - on_update: WidgetCallback::new(move |_| { - FrontendMessage::TriggerVisitLink { - url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"), - } - .into() - }), - ..Default::default() - }))); + buttons.push( + TextButton::new(format!("Issue #{issue}")) + .min_width(96) + .on_update(move |_| { + FrontendMessage::TriggerVisitLink { + url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"), + } + .into() + }) + .widget_holder(), + ); } Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Coming soon".to_string(), - bold: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new("Coming soon").bold(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: details, - multiline: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new(details).multiline(true).widget_holder()], }, LayoutGroup::Row { widgets: buttons }, ])) diff --git a/editor/src/messages/dialog/simple_dialogs/error_dialog.rs b/editor/src/messages/dialog/simple_dialogs/error_dialog.rs index 1b309802..f701315c 100644 --- a/editor/src/messages/dialog/simple_dialogs/error_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/error_dialog.rs @@ -1,6 +1,4 @@ -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; -use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; /// A dialog to notify users of a non-fatal error. @@ -13,27 +11,17 @@ impl PropertyHolder for ErrorDialog { fn properties(&self) -> Layout { Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: self.title.clone(), - bold: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new(&self.title).bold(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: self.description.clone(), - multiline: true, - ..Default::default() - }))], + widgets: vec![TextLabel::new(&self.description).multiline(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextButton(TextButton { - label: "OK".to_string(), - emphasized: true, - min_width: 96, - on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()), - ..Default::default() - }))], + widgets: vec![TextButton::new("OK") + .emphasized(true) + .min_width(96) + .on_update(|_| FrontendMessage::DisplayDialogDismiss.into()) + .widget_holder()], }, ])) } diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 171e8995..5c898dfd 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -452,6 +452,7 @@ pub struct WidgetHolder { } impl WidgetHolder { + #[deprecated(since = "0.0.0", note = "Please use the builder pattern, e.g. TextLabel::new(\"hello\").widget_holder()")] pub fn new(widget: Widget) -> Self { Self { widget_id: generate_uuid(), widget } } diff --git a/editor/src/messages/layout/utility_types/misc.rs b/editor/src/messages/layout/utility_types/misc.rs index adaed654..42211712 100644 --- a/editor/src/messages/layout/utility_types/misc.rs +++ b/editor/src/messages/layout/utility_types/misc.rs @@ -4,16 +4,27 @@ use serde::{Deserialize, Serialize}; #[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize, specta::Type)] #[repr(u8)] pub enum LayoutTarget { + /// Contains the contents of the dialog, including the title and action buttons. Must be shown with the `FrontendMessage::DisplayDialog` message. DialogDetails, + /// Contains the widgets located directly above the canvas to the right, for example the zoom in and out buttons. DocumentBar, + /// Contains the dropdown for design / select / guide mode found on the top left of the canvas. DocumentMode, + /// Options for opacity seen at the top of the Layers panel. LayerTreeOptions, + /// The dropdown menu at the very top of the application: File, Edit, etc. MenuBar, + /// Bar at the top of the node graph containing the location and the 'preview' and 'hide' buttons. NodeGraphBar, + /// The bar at the top of the Properties panel containing the layer name and icon. PropertiesOptions, + /// The body of the Properties panel containing many collapsable sections. PropertiesSections, + /// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown. ToolOptions, + /// The vertical buttons for all of the tools on the left of the canvas. ToolShelf, + /// The color swatch for the working colors and a flip and reset button found at the bottom of the tool shelf. WorkingColors, // KEEP THIS ENUM LAST diff --git a/editor/src/messages/layout/utility_types/widgets/menu_widgets.rs b/editor/src/messages/layout/utility_types/widgets/menu_widgets.rs index 3f0855e9..c942884c 100644 --- a/editor/src/messages/layout/utility_types/widgets/menu_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/menu_widgets.rs @@ -1,7 +1,6 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; use crate::messages::input_mapper::utility_types::misc::ActionKeys; -use crate::messages::layout::utility_types::layout_widget::WidgetHolder; -use crate::messages::layout::utility_types::layout_widget::{Widget, WidgetCallback}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use serde::{Deserialize, Serialize}; @@ -51,9 +50,7 @@ impl MenuBarEntry { } pub fn create_action(callback: impl Fn(&()) -> Message + 'static + Send + Sync) -> WidgetHolder { - WidgetHolder::new(Widget::InvisibleStandinInput(InvisibleStandinInput { - on_update: WidgetCallback::new(callback), - })) + InvisibleStandinInput::new().on_update(callback).widget_holder() } pub fn no_action() -> WidgetHolder { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 9a7310d9..aecfafca 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -5,19 +5,12 @@ use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, use crate::messages::frontend::utility_types::ExportBounds; use crate::messages::frontend::utility_types::FileType; use crate::messages::input_mapper::utility_types::macros::action_keys; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{CheckboxInput, TextLabel}; -use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton}; -use crate::messages::layout::utility_types::widgets::input_widgets::{ - DropdownEntryData, DropdownInput, NumberInput, NumberInputIncrementBehavior, NumberInputMode, OptionalInput, RadioEntryData, RadioInput, -}; -use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::properties_panel::utility_types::PropertiesPanelMessageHandlerData; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::layer_panel::{LayerMetadata, LayerPanelEntry, RawBuffer}; -use crate::messages::portfolio::document::utility_types::misc::DocumentMode; -use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis}; +use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, DocumentSave, FlipAxis}; use crate::messages::portfolio::document::utility_types::vectorize_layer_metadata; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; @@ -1589,11 +1582,9 @@ impl DocumentMessageHandler { pub fn update_document_widgets(&self, responses: &mut VecDeque) { let snapping_state = self.snapping_state.clone(); let mut widgets = vec![ - WidgetHolder::new(Widget::OptionalInput(OptionalInput { - checked: snapping_state.snapping_enabled, - icon: "Snapping".into(), - tooltip: "Snapping".into(), - on_update: WidgetCallback::new(move |optional_input: &OptionalInput| { + OptionalInput::new(snapping_state.snapping_enabled, "Snapping") + .tooltip("Snapping") + .on_update(move |optional_input: &OptionalInput| { let snapping_enabled = optional_input.checked; DocumentMessage::SetSnapping { snapping_enabled: Some(snapping_enabled), @@ -1601,261 +1592,159 @@ impl DocumentMessageHandler { node_snapping: Some(snapping_state.node_snapping), } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Snapping".into(), - text: "Select the vectors to snap to.".into(), // TODO: check whether this is an apt description - options_widget: vec![ + }) + .widget_holder(), + PopoverButton::new("Snapping", "Snap customization settings") + .options_widget(vec![ LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - tooltip: SnappingOptions::BoundingBoxes.to_string(), - checked: snapping_state.bounding_box_snapping, - on_update: WidgetCallback::new(move |input: &CheckboxInput| { + CheckboxInput::new(snapping_state.bounding_box_snapping) + .tooltip(SnappingOptions::BoundingBoxes.to_string()) + .on_update(move |input: &CheckboxInput| { DocumentMessage::SetSnapping { snapping_enabled: None, bounding_box_snapping: Some(input.checked), node_snapping: None, } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - direction: SeparatorDirection::Horizontal, - separator_type: SeparatorType::Unrelated, - })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: SnappingOptions::BoundingBoxes.to_string(), - table_align: false, - min_width: 60, - ..Default::default() - })), - // adds appropriate space between row elements - WidgetHolder::new(Widget::Separator(Separator { - direction: SeparatorDirection::Vertical, - separator_type: SeparatorType::Related, - })), + }) + .widget_holder(), + WidgetHolder::unrelated_separator(), + TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).table_align(false).min_width(60).widget_holder(), + WidgetHolder::related_separator(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: self.snapping_state.node_snapping, - tooltip: SnappingOptions::Points.to_string(), - on_update: WidgetCallback::new(|input: &CheckboxInput| { + CheckboxInput::new(self.snapping_state.node_snapping) + .tooltip(SnappingOptions::Points.to_string()) + .on_update(|input: &CheckboxInput| { DocumentMessage::SetSnapping { snapping_enabled: None, bounding_box_snapping: None, node_snapping: Some(input.checked), } .into() - }), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - direction: SeparatorDirection::Horizontal, - separator_type: SeparatorType::Unrelated, - })), - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: SnappingOptions::Points.to_string(), - table_align: false, - min_width: 60, - ..Default::default() - })), + }) + .widget_holder(), + WidgetHolder::unrelated_separator(), + TextLabel::new(SnappingOptions::Points.to_string()).table_align(false).min_width(60).widget_holder(), ], }, - ], - - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::OptionalInput(OptionalInput { - checked: true, - icon: "Grid".into(), - tooltip: "Grid".into(), - on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Grid".into(), - text: "Coming soon".into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::OptionalInput(OptionalInput { - checked: self.overlays_visible, - icon: "Overlays".into(), - tooltip: "Overlays".into(), - on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Overlays".into(), - text: "Coming soon".into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::RadioInput(RadioInput { - selected_index: match self.view_mode { - ViewMode::Normal => 0, - _ => 1, - }, - entries: vec![ - RadioEntryData { - value: "normal".into(), - icon: "ViewModeNormal".into(), - tooltip: "View Mode: Normal".into(), - on_update: WidgetCallback::new(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Normal }.into()), - ..RadioEntryData::default() - }, - RadioEntryData { - value: "outline".into(), - icon: "ViewModeOutline".into(), - tooltip: "View Mode: Outline".into(), - on_update: WidgetCallback::new(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Outline }.into()), - ..RadioEntryData::default() - }, - RadioEntryData { - value: "pixels".into(), - icon: "ViewModePixels".into(), - tooltip: "View Mode: Pixels".into(), - on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()), - ..RadioEntryData::default() - }, - ], - ..Default::default() - })), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "View Mode".into(), - text: "Coming soon".into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Section, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "ZoomIn".into(), - tooltip: "Zoom In".into(), - tooltip_shortcut: action_keys!(NavigationMessageDiscriminant::IncreaseCanvasZoom), - on_update: WidgetCallback::new(|_| NavigationMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()), - ..IconButton::default() - })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "ZoomOut".into(), - tooltip: "Zoom Out".into(), - tooltip_shortcut: action_keys!(NavigationMessageDiscriminant::DecreaseCanvasZoom), - on_update: WidgetCallback::new(|_| NavigationMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()), - ..IconButton::default() - })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 24, - icon: "ZoomReset".into(), - tooltip: "Zoom to 100%".into(), - tooltip_shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo100Percent), - on_update: WidgetCallback::new(|_| NavigationMessage::SetCanvasZoom { zoom_factor: 1. }.into()), - ..IconButton::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - unit: "%".into(), - value: Some(self.navigation_handler.snapped_scale() * 100.), - min: Some(0.000001), - max: Some(1000000.), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + ]) + .widget_holder(), + WidgetHolder::unrelated_separator(), + OptionalInput::new(true, "Grid") + .tooltip("Grid") + .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()) + .widget_holder(), + PopoverButton::new("Grid", "Coming soon").widget_holder(), + WidgetHolder::unrelated_separator(), + OptionalInput::new(self.overlays_visible, "Overlays") + .tooltip("Overlays") + .on_update(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()) + .widget_holder(), + PopoverButton::new("Overlays", "Coming soon").widget_holder(), + WidgetHolder::unrelated_separator(), + RadioInput::new(vec![ + RadioEntryData::default() + .value("normal") + .icon("ViewModeNormal") + .tooltip("View Mode: Normal") + .on_update(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Normal }.into()), + RadioEntryData::default() + .value("outline") + .icon("ViewModeOutline") + .tooltip("View Mode: Outline") + .on_update(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Outline }.into()), + RadioEntryData::default() + .value("pixels") + .icon("ViewModePixels") + .tooltip("View Mode: Pixels") + .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()), + ]) + .selected_index(match self.view_mode { + ViewMode::Normal => 0, + _ => 1, + }) + .widget_holder(), + PopoverButton::new("View Mode", "Coming soon").widget_holder(), + WidgetHolder::section_separator(), + IconButton::new("ZoomIn", 24) + .tooltip("Zoom In") + .tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::IncreaseCanvasZoom)) + .on_update(|_| NavigationMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()) + .widget_holder(), + IconButton::new("ZoomOut", 24) + .tooltip("Zoom Out") + .tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::DecreaseCanvasZoom)) + .on_update(|_| NavigationMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()) + .widget_holder(), + IconButton::new("ZoomReset", 24) + .tooltip("Zoom to 100%") + .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo100Percent)) + .on_update(|_| NavigationMessage::SetCanvasZoom { zoom_factor: 1. }.into()) + .widget_holder(), + WidgetHolder::related_separator(), + NumberInput::new(Some(self.navigation_handler.snapped_scale() * 100.)) + .unit("%") + .min(0.000001) + .max(1000000.) + .mode_increment() + .on_update(|number_input: &NumberInput| { NavigationMessage::SetCanvasZoom { zoom_factor: number_input.value.unwrap() / 100., } .into() - }), - increment_behavior: NumberInputIncrementBehavior::Callback, - increment_callback_decrease: WidgetCallback::new(|_| NavigationMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()), - increment_callback_increase: WidgetCallback::new(|_| NavigationMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()), - ..NumberInput::default() - })), + }) + .increment_behavior(NumberInputIncrementBehavior::Callback) + .increment_callback_decrease(|_| NavigationMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()) + .increment_callback_increase(|_| NavigationMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()) + .widget_holder(), ]; let rotation_value = self.navigation_handler.snapped_angle() / (std::f64::consts::PI / 180.); if rotation_value.abs() > 0.00001 { widgets.extend([ - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - unit: "°".into(), - value: Some(rotation_value), - step: 15., - on_update: WidgetCallback::new(|number_input: &NumberInput| { + WidgetHolder::related_separator(), + NumberInput::new(Some(rotation_value)) + .unit("°") + .step(15.) + .on_update(|number_input: &NumberInput| { NavigationMessage::SetCanvasRotation { angle_radians: number_input.value.unwrap() * (std::f64::consts::PI / 180.), } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ]); } widgets.extend([ - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Canvas Navigation".into(), - text: "Interactive options in this popover menu are coming soon.\nZoom with Shift + MMB Drag or Ctrl + Scroll Wheel Roll.\nRotate with Ctrl + MMB Drag.".into(), - ..Default::default() - })), + WidgetHolder::related_separator(), + PopoverButton::new( + "Canvas Navigation", + "Interactive options in this popover menu are coming soon.\nZoom with Shift + MMB Drag or Ctrl + Scroll Wheel Roll.\nRotate with Ctrl + MMB Drag.", + ) + .widget_holder(), ]); let document_bar_layout = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]); let document_mode_layout = WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::DropdownInput(DropdownInput { - entries: vec![vec![ - DropdownEntryData { - label: DocumentMode::DesignMode.to_string(), - icon: DocumentMode::DesignMode.icon_name(), - ..DropdownEntryData::default() - }, - DropdownEntryData { - label: DocumentMode::SelectMode.to_string(), - icon: DocumentMode::SelectMode.icon_name(), - on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(330) }.into()), - ..DropdownEntryData::default() - }, - DropdownEntryData { - label: DocumentMode::GuideMode.to_string(), - icon: DocumentMode::GuideMode.icon_name(), - on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(331) }.into()), - ..DropdownEntryData::default() - }, - ]], - selected_index: Some(self.document_mode as u32), - draw_icon: true, - interactive: false, // TODO: set to true when dialogs are not spawned - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Section, - direction: SeparatorDirection::Horizontal, - })), + DropdownInput::new( + vec![vec![ + DropdownEntryData::new(DocumentMode::DesignMode.to_string()).icon(DocumentMode::DesignMode.icon_name()), + DropdownEntryData::new(DocumentMode::SelectMode.to_string()) + .icon(DocumentMode::SelectMode.icon_name()) + .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(330) }.into()), + DropdownEntryData::new(DocumentMode::GuideMode.to_string()) + .icon(DocumentMode::GuideMode.icon_name()) + .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(331) }.into()), + ]]) + .selected_index( Some(self.document_mode as u32)) + .draw_icon( true) + .interactive( false) // TODO: set to true when dialogs are not spawned + .widget_holder(), + WidgetHolder::section_separator(), ], }]); @@ -1914,11 +1803,10 @@ impl DocumentMessageHandler { .map(|modes| { modes .iter() - .map(|mode| DropdownEntryData { - label: mode.to_string(), - value: mode.to_string(), - on_update: WidgetCallback::new(|_| DocumentMessage::SetBlendModeForSelectedLayers { blend_mode: *mode }.into()), - ..Default::default() + .map(|mode| { + DropdownEntryData::new(mode.to_string()) + .value(mode.to_string()) + .on_update(|_| DocumentMessage::SetBlendModeForSelectedLayers { blend_mode: *mode }.into()) }) .collect() }) @@ -1926,57 +1814,41 @@ impl DocumentMessageHandler { let layer_tree_options = WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::DropdownInput(DropdownInput { - entries: blend_mode_menu_entries, - selected_index: blend_mode.map(|blend_mode| blend_mode as u32), - disabled: blend_mode.is_none() && !blend_mode_is_mixed, - draw_icon: false, - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Related, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - label: "Opacity".into(), - unit: "%".into(), - display_decimal_places: 2, - disabled: opacity.is_none() && !opacity_is_mixed, - value: opacity.map(|opacity| opacity * 100.), - min: Some(0.), - max: Some(100.), - range_min: Some(0.), - range_max: Some(100.), - mode: NumberInputMode::Range, - on_update: WidgetCallback::new(|number_input: &NumberInput| { + DropdownInput::new(blend_mode_menu_entries) + .selected_index(blend_mode.map(|blend_mode| blend_mode as u32)) + .disabled(blend_mode.is_none() && !blend_mode_is_mixed) + .draw_icon(false) + .widget_holder(), + WidgetHolder::related_separator(), + NumberInput::new(opacity.map(|opacity| opacity * 100.)) + .label("Opacity") + .unit("%") + .display_decimal_places(2) + .disabled(opacity.is_none() && !opacity_is_mixed) + .min(0.) + .max(100.) + .range_min(Some(0.)) + .range_max(Some(100.)) + .mode(NumberInputMode::Range) + .on_update(|number_input: &NumberInput| { if let Some(value) = number_input.value { DocumentMessage::SetOpacityForSelectedLayers { opacity: value / 100. }.into() } else { Message::NoOp } - }), - ..NumberInput::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Section, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::IconButton(IconButton { - icon: "Folder".into(), - tooltip: "New Folder".into(), - tooltip_shortcut: action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder), - size: 24, - on_update: WidgetCallback::new(|_| DocumentMessage::CreateEmptyFolder { container_path: vec![] }.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::IconButton(IconButton { - icon: "Trash".into(), - tooltip: "Delete Selected".into(), - tooltip_shortcut: action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers), - size: 24, - on_update: WidgetCallback::new(|_| DocumentMessage::DeleteSelectedLayers.into()), - ..Default::default() - })), + }) + .widget_holder(), + WidgetHolder::section_separator(), + IconButton::new("Folder", 24) + .tooltip("New Folder") + .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder)) + .on_update(|_| DocumentMessage::CreateEmptyFolder { container_path: vec![] }.into()) + .widget_holder(), + IconButton::new("Trash", 24) + .tooltip("Delete Selected") + .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers)) + .on_update(|_| DocumentMessage::DeleteSelectedLayers.into()) + .widget_holder(), ], }]); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index c6bd806d..964062a0 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -245,12 +245,10 @@ impl NodeGraphMessageHandler { // Don't show stop previewing button on the original output node if !(is_output && network.previous_outputs_contain(node_id).unwrap_or(true)) { - let output_button = WidgetHolder::new(Widget::TextButton(TextButton { - label: if is_output { "End Preview" } else { "Preview" }.to_string(), - tooltip: if is_output { "Restore preview to Output node" } else { "Preview node" }.to_string() + " (Shortcut: Alt-click node)", - on_update: WidgetCallback::new(move |_| NodeGraphMessage::TogglePreview { node_id }.into()), - ..Default::default() - })); + let output_button = TextButton::new(if is_output { "End Preview" } else { "Preview" }) + .tooltip(if is_output { "Restore preview to Output node" } else { "Preview node" }.to_string() + " (Shortcut: Alt-click node)") + .on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into()) + .widget_holder(); widgets.push(output_button); } } diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index 54e6f705..11f0b40f 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -1,11 +1,7 @@ use super::utility_types::TransformOp; use crate::application::generate_uuid; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist; -use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton, TextButton}; -use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, ColorInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextInput}; -use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, TextLabel}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::NodePropertiesContext; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; @@ -72,23 +68,13 @@ pub fn apply_transform_operation(layer: &Layer, transform_op: TransformOp, value pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque, persistent_data: &PersistentData) { let options_bar = vec![LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::IconLabel(IconLabel { - icon: "NodeArtboard".into(), - tooltip: "Artboard".into(), - ..Default::default() - })), + IconLabel::new("NodeArtboard").tooltip("Artboard").widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextInput(TextInput { - value: layer.name.clone().unwrap_or_else(|| "Untitled Artboard".to_string()), - on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), - ..Default::default() - })), + TextInput::new(layer.name.clone().unwrap_or_else(|| "Untitled Artboard".to_string())) + .on_update(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Additional Options".into(), - text: "Coming soon".into(), - ..Default::default() - })), + PopoverButton::new("Additional Options", "Coming soon").widget_holder(), ], }]; @@ -104,113 +90,92 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ layout: vec![ LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Location".into(), - ..TextLabel::default() - })), + TextLabel::new("Location").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.transform.x() + pivot.x), - label: "X".into(), - unit: " px".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NumberInput::new(Some(layer.transform.x() + pivot.x)) + .label("X") + .unit(" px") + .on_update(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap() - pivot.x, transform_op: TransformOp::X, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.transform.y() + pivot.y), - label: "Y".into(), - unit: " px".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NumberInput::new(Some(layer.transform.y() + pivot.y)) + .label("Y") + .unit(" px") + .on_update(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap() - pivot.y, transform_op: TransformOp::Y, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Dimensions".into(), - ..TextLabel::default() - })), + TextLabel::new("Dimensions").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: layer.preserve_aspect, - icon: "Link".into(), - tooltip: "Preserve Aspect Ratio".into(), - on_update: WidgetCallback::new(|input: &CheckboxInput| PropertiesPanelMessage::ModifyPreserveAspect { preserve_aspect: input.checked }.into()), - ..Default::default() - })), + CheckboxInput::new(layer.preserve_aspect) + .icon("Link") + .tooltip("Preserve Aspect Ratio") + .on_update(|input: &CheckboxInput| PropertiesPanelMessage::ModifyPreserveAspect { preserve_aspect: input.checked }.into()) + .widget_holder(), WidgetHolder::related_separator(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.bounding_transform(&render_data).scale_x()), - label: "W".into(), - unit: " px".into(), - is_integer: true, - min: Some(1.), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + NumberInput::new(Some(layer.bounding_transform(&render_data).scale_x())) + .label("W") + .unit(" px") + .is_integer(true) + .min(1.) + .on_update(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap(), transform_op: TransformOp::Width, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.bounding_transform(&render_data).scale_y()), - label: "H".into(), - unit: " px".into(), - is_integer: true, - min: Some(1.), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + NumberInput::new(Some(layer.bounding_transform(&render_data).scale_y())) + .label("H") + .unit(" px") + .is_integer(true) + .min(1.) + .on_update(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap(), transform_op: TransformOp::Height, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Background".into(), - ..TextLabel::default() - })), + TextLabel::new("Background").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::ColorInput(ColorInput { - value: Some(*color), - on_update: WidgetCallback::new(|text_input: &ColorInput| { + ColorInput::new(Some(*color)) + .on_update(|text_input: &ColorInput| { let fill = if let Some(value) = text_input.value { value } else { Color::TRANSPARENT }; PropertiesPanelMessage::ModifyFill { fill: Fill::Solid(fill) }.into() - }), - ..Default::default() - })), + }) + .widget_holder(), ], }, ], @@ -239,34 +204,16 @@ pub fn register_artwork_layer_properties( let options_bar = vec![LayoutGroup::Row { widgets: vec![ match &layer.data { - LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { - icon: "Folder".into(), - tooltip: "Folder".into(), - ..Default::default() - })), - LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { - icon: "NodeShape".into(), - tooltip: "Shape".into(), - ..Default::default() - })), - LayerDataType::Layer(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { - icon: "Layer".into(), - tooltip: "Layer".into(), - ..Default::default() - })), + LayerDataType::Folder(_) => IconLabel::new("Folder").tooltip("Folder").widget_holder(), + LayerDataType::Shape(_) => IconLabel::new("NodeShape").tooltip("Shape").widget_holder(), + LayerDataType::Layer(_) => IconLabel::new("Layer").tooltip("Layer").widget_holder(), }, WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextInput(TextInput { - value: layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string()), - on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), - ..Default::default() - })), + TextInput::new(layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string())) + .on_update(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Additional Options".into(), - text: "Coming soon".into(), - ..Default::default() - })), + PopoverButton::new("Additional Options", "Coming soon").widget_holder(), ], }]; @@ -346,158 +293,128 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La layout: vec![ LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Location".into(), - ..TextLabel::default() - })), + TextLabel::new("Location").widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::PivotAssist(PivotAssist { - position: layer.pivot.into(), - on_update: WidgetCallback::new(|pivot_assist: &PivotAssist| PropertiesPanelMessage::SetPivot { new_position: pivot_assist.position }.into()), - ..Default::default() - })), + PivotAssist::new(layer.pivot.into()) + .on_update(|pivot_assist: &PivotAssist| PropertiesPanelMessage::SetPivot { new_position: pivot_assist.position }.into()) + .widget_holder(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.transform.x() + pivot.x), - label: "X".into(), - unit: " px".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NumberInput::new(Some(layer.transform.x() + pivot.x)) + .label("X") + .unit(" px") + .on_update(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap() - pivot.x, transform_op: TransformOp::X, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.transform.y() + pivot.y), - label: "Y".into(), - unit: " px".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NumberInput::new(Some(layer.transform.y() + pivot.y)) + .label("Y") + .unit(" px") + .on_update(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap() - pivot.y, transform_op: TransformOp::Y, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Rotation".into(), - ..TextLabel::default() - })), + TextLabel::new("Rotation").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.transform.rotation() * 180. / PI), - unit: "°".into(), - mode: NumberInputMode::Range, - range_min: Some(-180.), - range_max: Some(180.), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + NumberInput::new(Some(layer.transform.rotation() * 180. / PI)) + .unit("°") + .mode(NumberInputMode::Range) + .range_min(Some(-180.)) + .range_max(Some(180.)) + .on_update(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap() / 180. * PI, transform_op: TransformOp::Rotation, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Scale".into(), - ..TextLabel::default() - })), + TextLabel::new("Scale").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { - checked: layer.preserve_aspect, - icon: "Link".into(), - tooltip: "Preserve Aspect Ratio".into(), - on_update: WidgetCallback::new(|input: &CheckboxInput| PropertiesPanelMessage::ModifyPreserveAspect { preserve_aspect: input.checked }.into()), - ..Default::default() - })), + CheckboxInput::new(layer.preserve_aspect) + .icon("Link") + .tooltip("Preserve Aspect Ratio") + .on_update(|input: &CheckboxInput| PropertiesPanelMessage::ModifyPreserveAspect { preserve_aspect: input.checked }.into()) + .widget_holder(), WidgetHolder::related_separator(), WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.transform.scale_x()), - label: "X".into(), - unit: "".into(), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + NumberInput::new(Some(layer.transform.scale_x())) + .label("X") + .unit("") + .on_update(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap(), transform_op: TransformOp::ScaleX, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.transform.scale_y()), - label: "Y".into(), - unit: "".into(), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + NumberInput::new(Some(layer.transform.scale_y())) + .label("Y") + .unit("") + .on_update(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap(), transform_op: TransformOp::ScaleY, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Dimensions".into(), - ..TextLabel::default() - })), + TextLabel::new("Dimensions").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.bounding_transform(&render_data).scale_x()), - label: "W".into(), - unit: " px".into(), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + NumberInput::new(Some(layer.bounding_transform(&render_data).scale_x())) + .label("W") + .unit(" px") + .on_update(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap(), transform_op: TransformOp::Width, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.bounding_transform(&render_data).scale_y()), - label: "H".into(), - unit: " px".into(), - on_update: WidgetCallback::new(|number_input: &NumberInput| { + NumberInput::new(Some(layer.bounding_transform(&render_data).scale_y())) + .label("H") + .unit(" px") + .on_update(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value.unwrap(), transform_op: TransformOp::Height, } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, ], @@ -515,45 +432,34 @@ fn node_gradient_type(gradient: &Gradient) -> LayoutGroup { cloned_gradient_radial.gradient_type = GradientType::Radial; LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Gradient Type".into(), - ..TextLabel::default() - })), + TextLabel::new("Gradient Type").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::RadioInput(RadioInput { - selected_index, - entries: vec![ - RadioEntryData { - value: "linear".into(), - label: "Linear".into(), - tooltip: "Linear gradient changes colors from one side to the other along a line".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyFill { - fill: Fill::Gradient(cloned_gradient_linear.clone()), - } - .into() - }), - ..RadioEntryData::default() - }, - RadioEntryData { - value: "radial".into(), - label: "Radial".into(), - tooltip: "Radial gradient changes colors from the inside to the outside of a circular area".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyFill { - fill: Fill::Gradient(cloned_gradient_radial.clone()), - } - .into() - }), - ..RadioEntryData::default() - }, - ], - ..Default::default() - })), + RadioInput::new(vec![ + RadioEntryData::new("linear") + .label("Linear") + .tooltip("Linear gradient changes colors from one side to the other along a line") + .on_update(move |_| { + PropertiesPanelMessage::ModifyFill { + fill: Fill::Gradient(cloned_gradient_linear.clone()), + } + .into() + }), + RadioEntryData::new("radial") + .label("Radial") + .tooltip("Radial gradient changes colors from the inside to the outside of a circular area") + .on_update(move |_| { + PropertiesPanelMessage::ModifyFill { + fill: Fill::Gradient(cloned_gradient_radial.clone()), + } + .into() + }), + ]) + .selected_index(selected_index) + .widget_holder(), ], } } @@ -566,49 +472,39 @@ fn node_gradient_color(gradient: &Gradient, position: usize) -> LayoutGroup { let value = format!("Gradient: {:.0}%", gradient_clone.positions[position].0 * 100.); let mut widgets = vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value, - tooltip: "Adjustable by dragging the gradient stops in the viewport with the Gradient tool active".into(), - ..TextLabel::default() - })), + TextLabel::new(value) + .tooltip("Adjustable by dragging the gradient stops in the viewport with the Gradient tool active") + .widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::ColorInput(ColorInput { - value: gradient_clone.positions[position].1, - on_update: WidgetCallback::new(move |text_input: &ColorInput| { + ColorInput::new(gradient_clone.positions[position].1) + .on_update(move |text_input: &ColorInput| { let mut new_gradient = (*gradient_clone).clone(); new_gradient.positions[position].1 = text_input.value; send_fill_message(new_gradient) - }), - ..ColorInput::default() - })), + }) + .widget_holder(), ]; let mut skip_separator = false; // Remove button if gradient.positions.len() != position + 1 && position != 0 { - let on_update = WidgetCallback::new(move |_| { + let on_update = move |_: &IconButton| { let mut new_gradient = (*gradient_3).clone(); new_gradient.positions.remove(position); send_fill_message(new_gradient) - }); + }; skip_separator = true; widgets.push(WidgetHolder::related_separator()); - widgets.push(WidgetHolder::new(Widget::IconButton(IconButton { - icon: "Remove".to_string(), - tooltip: "Remove this gradient stop".to_string(), - size: 16, - on_update, - ..Default::default() - }))); + widgets.push(IconButton::new("Remove", 16).tooltip("Remove this gradient stop").on_update(on_update).widget_holder()); } // Add button if gradient.positions.len() != position + 1 { - let on_update = WidgetCallback::new(move |_| { + let on_update = move |_: &IconButton| { let mut gradient = (*gradient_2).clone(); let get_color = |index: usize| match (gradient.positions[index].1, gradient.positions.get(index + 1).and_then(|x| x.1)) { @@ -623,18 +519,12 @@ fn node_gradient_color(gradient: &Gradient, position: usize) -> LayoutGroup { gradient.positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); send_fill_message(gradient) - }); + }; if !skip_separator { widgets.push(WidgetHolder::related_separator()); } - widgets.push(WidgetHolder::new(Widget::IconButton(IconButton { - icon: "Add".to_string(), - tooltip: "Add a gradient stop after this".to_string(), - size: 16, - on_update, - ..Default::default() - }))); + widgets.push(IconButton::new("Add", 16).tooltip("Add a gradient stop after this").on_update(on_update).widget_holder()); } LayoutGroup::Row { widgets } } @@ -648,40 +538,31 @@ fn node_section_fill(fill: &Fill) -> Option { layout: vec![ LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Color".into(), - ..TextLabel::default() - })), + TextLabel::new("Color").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::ColorInput(ColorInput { - value: if let Fill::Solid(color) = fill { Some(*color) } else { None }, - on_update: WidgetCallback::new(|text_input: &ColorInput| { + ColorInput::new(if let Fill::Solid(color) = fill { Some(*color) } else { None }) + .on_update(|text_input: &ColorInput| { let fill = if let Some(value) = text_input.value { Fill::Solid(value) } else { Fill::None }; PropertiesPanelMessage::ModifyFill { fill }.into() - }), - ..ColorInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "".into(), - ..TextLabel::default() - })), + TextLabel::new("").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Use Gradient".into(), - tooltip: "Change this fill from a solid color to a gradient".into(), - on_update: WidgetCallback::new(move |_: &TextButton| { + TextButton::new("Use Gradient") + .tooltip("Change this fill from a solid color to a gradient") + .on_update(move |_: &TextButton| { let (r, g, b, _) = initial_color.components(); let opposite_color = Color::from_rgbaf32(1. - r, 1. - g, 1. - b, 1.).unwrap(); @@ -697,9 +578,8 @@ fn node_section_fill(fill: &Fill) -> Option { )), } .into() - }), - ..TextButton::default() - })), + }) + .widget_holder(), ], }, ], @@ -714,51 +594,41 @@ fn node_section_fill(fill: &Fill) -> Option { layout.extend((0..gradient.positions.len()).map(|pos| node_gradient_color(gradient, pos))); layout.push(LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "".into(), - ..TextLabel::default() - })), + TextLabel::new("").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Invert".into(), - icon: Some("Swap".into()), - tooltip: "Reverse the order of each color stop".into(), - on_update: WidgetCallback::new(move |_: &TextButton| { + TextButton::new("Invert") + .icon(Some("Swap".into())) + .tooltip("Reverse the order of each color stop") + .on_update(move |_: &TextButton| { let mut new_gradient = cloned_gradient.clone(); new_gradient.positions = new_gradient.positions.iter().map(|(distance, color)| (1. - distance, *color)).collect(); new_gradient.positions.reverse(); PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into() - }), - ..TextButton::default() - })), + }) + .widget_holder(), ], }); layout.push(LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "".into(), - ..TextLabel::default() - })), + TextLabel::new("").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextButton(TextButton { - label: "Use Solid Color".into(), - tooltip: "Change this fill from a gradient to a solid color, keeping the 0% stop color".into(), - on_update: WidgetCallback::new(move |_: &TextButton| { + TextButton::new("Use Solid Color") + .tooltip("Change this fill from a gradient to a solid color, keeping the 0% stop color") + .on_update(move |_: &TextButton| { PropertiesPanelMessage::ModifyFill { fill: Fill::Solid(first_color.unwrap_or_default()), } .into() - }), - ..TextButton::default() - })), + }) + .widget_holder(), ], }); layout @@ -786,226 +656,167 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { layout: vec![ LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Color".into(), - ..TextLabel::default() - })), + TextLabel::new("Color").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::ColorInput(ColorInput { - value: stroke.color(), - on_update: WidgetCallback::new(move |text_input: &ColorInput| { + ColorInput::new(stroke.color()) + .on_update(move |text_input: &ColorInput| { internal_stroke1 .clone() .with_color(&text_input.value) .map_or(PropertiesPanelMessage::ResendActiveProperties.into(), |stroke| PropertiesPanelMessage::ModifyStroke { stroke }.into()) - }), - ..ColorInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Weight".into(), - ..TextLabel::default() - })), + TextLabel::new("Weight").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(stroke.weight()), - is_integer: false, - min: Some(0.), - unit: " px".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NumberInput::new(Some(stroke.weight())) + .is_integer(false) + .min(0.) + .unit(" px") + .on_update(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyStroke { stroke: internal_stroke2.clone().with_weight(number_input.value.unwrap()), } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Dash Lengths".into(), - ..TextLabel::default() - })), + TextLabel::new("Dash Lengths").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextInput(TextInput { - value: stroke.dash_lengths(), - centered: true, - on_update: WidgetCallback::new(move |text_input: &TextInput| { + TextInput::new(stroke.dash_lengths()) + .centered(true) + .on_update(move |text_input: &TextInput| { internal_stroke3 .clone() .with_dash_lengths(&text_input.value) .map_or(PropertiesPanelMessage::ResendActiveProperties.into(), |stroke| PropertiesPanelMessage::ModifyStroke { stroke }.into()) - }), - ..Default::default() - })), + }) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Dash Offset".into(), - ..TextLabel::default() - })), + TextLabel::new("Dash Offset").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(stroke.dash_offset()), - is_integer: true, - min: Some(0.), - unit: " px".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NumberInput::new(Some(stroke.dash_offset())) + .is_integer(true) + .min(0.) + .unit(" px") + .on_update(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyStroke { stroke: internal_stroke4.clone().with_dash_offset(number_input.value.unwrap()), } .into() + }) + .widget_holder(), + ], + }, + LayoutGroup::Row { + widgets: vec![ + TextLabel::new("Line Cap").widget_holder(), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, + WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. + WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::unrelated_separator(), + RadioInput::new(vec![ + RadioEntryData::new("Butt").on_update(move |_| { + PropertiesPanelMessage::ModifyStroke { + stroke: internal_stroke6.clone().with_line_cap(LineCap::Butt), + } + .into() }), - ..NumberInput::default() - })), + RadioEntryData::new("Round").on_update(move |_| { + PropertiesPanelMessage::ModifyStroke { + stroke: internal_stroke7.clone().with_line_cap(LineCap::Round), + } + .into() + }), + RadioEntryData::new("Square").on_update(move |_| { + PropertiesPanelMessage::ModifyStroke { + stroke: internal_stroke8.clone().with_line_cap(LineCap::Square), + } + .into() + }), + ]) + .selected_index(stroke.line_cap_index()) + .widget_holder(), ], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Line Cap".into(), - ..TextLabel::default() - })), + TextLabel::new("Line Join").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::RadioInput(RadioInput { - selected_index: stroke.line_cap_index(), - entries: vec![ - RadioEntryData { - label: "Butt".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke6.clone().with_line_cap(LineCap::Butt), - } - .into() - }), - ..RadioEntryData::default() - }, - RadioEntryData { - label: "Round".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke7.clone().with_line_cap(LineCap::Round), - } - .into() - }), - ..RadioEntryData::default() - }, - RadioEntryData { - label: "Square".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke8.clone().with_line_cap(LineCap::Square), - } - .into() - }), - ..RadioEntryData::default() - }, - ], - ..Default::default() - })), - ], - }, - LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Line Join".into(), - ..TextLabel::default() - })), - WidgetHolder::unrelated_separator(), - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. - WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::RadioInput(RadioInput { - selected_index: stroke.line_join_index(), - entries: vec![ - RadioEntryData { - label: "Miter".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke9.clone().with_line_join(LineJoin::Miter), - } - .into() - }), - ..RadioEntryData::default() - }, - RadioEntryData { - label: "Bevel".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke10.clone().with_line_join(LineJoin::Bevel), - } - .into() - }), - ..RadioEntryData::default() - }, - RadioEntryData { - label: "Round".into(), - on_update: WidgetCallback::new(move |_| { - PropertiesPanelMessage::ModifyStroke { - stroke: internal_stroke11.clone().with_line_join(LineJoin::Round), - } - .into() - }), - ..RadioEntryData::default() - }, - ], - ..Default::default() - })), + RadioInput::new(vec![ + RadioEntryData::new("Miter").on_update(move |_| { + PropertiesPanelMessage::ModifyStroke { + stroke: internal_stroke9.clone().with_line_join(LineJoin::Miter), + } + .into() + }), + RadioEntryData::new("Bevel").on_update(move |_| { + PropertiesPanelMessage::ModifyStroke { + stroke: internal_stroke10.clone().with_line_join(LineJoin::Bevel), + } + .into() + }), + RadioEntryData::new("Round").on_update(move |_| { + PropertiesPanelMessage::ModifyStroke { + stroke: internal_stroke11.clone().with_line_join(LineJoin::Round), + } + .into() + }), + ]) + .selected_index(stroke.line_join_index()) + .widget_holder(), ], }, // TODO: Gray out this row when Line Join isn't set to Miter LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Miter Limit".into(), - ..TextLabel::default() - })), + TextLabel::new("Miter Limit").widget_holder(), WidgetHolder::unrelated_separator(), WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(stroke.line_join_miter_limit() as f64), - is_integer: true, - min: Some(0.), - unit: "".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NumberInput::new(Some(stroke.line_join_miter_limit() as f64)) + .is_integer(true) + .min(0.) + .unit("") + .on_update(move |number_input: &NumberInput| { PropertiesPanelMessage::ModifyStroke { stroke: internal_stroke5.clone().with_line_join_miter_limit(number_input.value.unwrap()), } .into() - }), - ..NumberInput::default() - })), + }) + .widget_holder(), ], }, ], diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index 8d3ffbfb..67fb1467 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -1,5 +1,6 @@ use crate::messages::layout::utility_types::layout_widget::WidgetCallback; use crate::messages::layout::utility_types::widget_prelude::{ColorInput, IconButton, RadioEntryData, RadioInput, TextLabel, WidgetHolder}; +use crate::messages::prelude::Message; use graphene_core::Color; @@ -12,6 +13,7 @@ pub enum ToolColorType { Custom, } +/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar. pub struct ToolColorOptions { pub custom_color: Option, pub primary_working_color: Option, @@ -62,19 +64,19 @@ impl ToolColorOptions { &self, label_text: impl Into, color_allow_none: bool, - reset_callback: WidgetCallback, + reset_callback: impl Fn(&IconButton) -> Message + 'static + Send + Sync, radio_callback: fn(ToolColorType) -> WidgetCallback<()>, - color_callback: WidgetCallback, + color_callback: impl Fn(&ColorInput) -> Message + 'static + Send + Sync, ) -> Vec { let mut widgets = vec![TextLabel::new(label_text).widget_holder()]; if !color_allow_none { widgets.push(WidgetHolder::unrelated_separator()); } else { - let mut reset = IconButton::new("CloseX", 12) + let reset = IconButton::new("CloseX", 12) .disabled(self.custom_color.is_none() && self.color_type == ToolColorType::Custom) - .tooltip("Clear Color"); - reset.on_update = reset_callback; + .tooltip("Clear Color") + .on_update(reset_callback); widgets.push(WidgetHolder::related_separator()); widgets.push(reset.widget_holder()); @@ -97,8 +99,7 @@ impl ToolColorOptions { widgets.push(radio); widgets.push(WidgetHolder::related_separator()); - let mut color_input = ColorInput::new(self.active_color()).allow_none(color_allow_none); - color_input.on_update = color_callback; + let color_input = ColorInput::new(self.active_color()).allow_none(color_allow_none).on_update(color_callback); widgets.push(color_input.widget_holder()); widgets diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 465d57cb..7535afeb 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -3,7 +3,6 @@ use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; use crate::messages::portfolio::document::node_graph::transform_utils::get_current_transform; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; @@ -190,9 +189,9 @@ impl PropertyHolder for BrushTool { widgets.append(&mut self.options.color.create_widgets( "Color", false, - WidgetCallback::new(|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into()), + |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into()), + |color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into(), )); widgets.push(WidgetHolder::related_separator()); @@ -255,10 +254,7 @@ impl<'a> MessageHandler> for BrushTo } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index b4808b4b..e157e789 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -96,9 +96,9 @@ impl PropertyHolder for EllipseTool { let mut widgets = self.options.fill.create_widgets( "Fill", true, - WidgetCallback::new(|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into()), + |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value)).into()), + |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value)).into(), ); widgets.push(WidgetHolder::section_separator()); @@ -106,9 +106,9 @@ impl PropertyHolder for EllipseTool { widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, - WidgetCallback::new(|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into()), + |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value)).into()), + |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value)).into(), )); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); @@ -140,10 +140,7 @@ impl<'a> MessageHandler> for Ellipse } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index cd7bd056..7b6328d6 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -1,9 +1,7 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; -use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -102,9 +100,9 @@ impl PropertyHolder for FreehandTool { let mut widgets = self.options.fill.create_widgets( "Fill", true, - WidgetCallback::new(|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into()), + |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value)).into()), + |color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value)).into(), ); widgets.push(WidgetHolder::section_separator()); @@ -112,9 +110,9 @@ impl PropertyHolder for FreehandTool { widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, - WidgetCallback::new(|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into()), + |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value)).into()), + |color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value)).into(), )); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); @@ -146,10 +144,7 @@ impl<'a> MessageHandler> for Freehan } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index db9b64d1..a9e1bb77 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -2,8 +2,7 @@ use crate::application::generate_uuid; use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout}; -use crate::messages::layout::utility_types::widgets::input_widgets::{RadioEntryData, RadioInput}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs index 8eda4e4b..a6bb54af 100644 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ b/editor/src/messages/tool/tool_messages/line_tool.rs @@ -2,10 +2,8 @@ use crate::consts::LINE_ROTATE_SNAP_ANGLE; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; -use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -97,9 +95,9 @@ impl PropertyHolder for LineTool { let mut widgets = self.options.stroke.create_widgets( "Stroke", true, - WidgetCallback::new(|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into()), + |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value)).into()), + |color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value)).into(), ); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); @@ -124,10 +122,7 @@ impl<'a> MessageHandler> for LineToo } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 51ff7310..42ff4659 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -3,7 +3,7 @@ use std::vec; use crate::consts::{DRAG_THRESHOLD, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::PropertyHolder; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer; use crate::messages::tool::common_functionality::shape_editor::{ManipulatorPointInfo, OpposingHandleLengths, ShapeState}; diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 3addfdee..9998dd7b 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1,16 +1,13 @@ use crate::consts::LINE_ROTATE_SNAP_ANGLE; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; -use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::VectorDataModification; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer; - use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; @@ -122,9 +119,9 @@ impl PropertyHolder for PenTool { let mut widgets = self.options.fill.create_widgets( "Fill", true, - WidgetCallback::new(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into()), + |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value)).into()), + |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value)).into(), ); widgets.push(WidgetHolder::section_separator()); @@ -132,9 +129,9 @@ impl PropertyHolder for PenTool { widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, - WidgetCallback::new(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into()), + |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value)).into()), + |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value)).into(), )); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); @@ -166,10 +163,7 @@ impl<'a> MessageHandler> for PenTool } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs index 5048d5a3..0f920038 100644 --- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs +++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs @@ -1,8 +1,7 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{ColorInput, NumberInput, WidgetHolder}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -83,9 +82,9 @@ impl PropertyHolder for RectangleTool { let mut widgets = self.options.fill.create_widgets( "Fill", true, - WidgetCallback::new(|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into()), + |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value)).into()), + |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value)).into(), ); widgets.push(WidgetHolder::section_separator()); @@ -93,9 +92,9 @@ impl PropertyHolder for RectangleTool { widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, - WidgetCallback::new(|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into()), + |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value)).into()), + |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value)).into(), )); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); @@ -127,10 +126,7 @@ impl<'a> MessageHandler> for Rectang } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 616d67bb..bf28b345 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -4,11 +4,8 @@ use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::assist_widgets::{PivotAssist, PivotPosition}; -use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton}; -use crate::messages::layout::utility_types::widgets::input_widgets::{DropdownEntryData, DropdownInput}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::prelude::*; @@ -222,11 +219,7 @@ impl PropertyHolder for SelectTool { .on_update(|_| SelectToolMessage::FlipVertical.into()) .widget_holder(), WidgetHolder::related_separator(), - WidgetHolder::new(Widget::PopoverButton(PopoverButton { - header: "Flip".into(), - text: "Coming soon".into(), - ..Default::default() - })), + PopoverButton::new("Flip", "Coming soon").widget_holder(), WidgetHolder::section_separator(), IconButton::new("BooleanUnion", 24) .tooltip("Boolean Union (coming soon)") diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 625cec83..08da6e86 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -1,8 +1,7 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{ColorInput, NumberInput, RadioEntryData, RadioInput, WidgetHolder}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -135,9 +134,9 @@ impl PropertyHolder for ShapeTool { widgets.append(&mut self.options.fill.create_widgets( "Fill", true, - WidgetCallback::new(|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into()), + |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value)).into()), + |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value)).into(), )); widgets.push(WidgetHolder::section_separator()); @@ -145,9 +144,9 @@ impl PropertyHolder for ShapeTool { widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, - WidgetCallback::new(|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into()), + |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value)).into()), + |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value)).into(), )); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); @@ -180,10 +179,7 @@ impl<'a> MessageHandler> for ShapeTo } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 603bc336..04ffee98 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -1,10 +1,8 @@ use crate::consts::DRAG_THRESHOLD; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; -use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -105,9 +103,9 @@ impl PropertyHolder for SplineTool { let mut widgets = self.options.fill.create_widgets( "Fill", true, - WidgetCallback::new(|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into()), + |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value)).into()), + |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value)).into(), ); widgets.push(WidgetHolder::section_separator()); @@ -115,9 +113,9 @@ impl PropertyHolder for SplineTool { widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, - WidgetCallback::new(|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into()), + |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value)).into()), + |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value)).into(), )); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); @@ -149,10 +147,7 @@ impl<'a> MessageHandler> for SplineT } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index c85a2aba..938d481f 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -4,9 +4,8 @@ use crate::application::generate_uuid; use crate::consts::{COLOR_ACCENT, SELECTION_TOLERANCE}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::input_widgets::{ColorInput, FontInput, NumberInput}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::new_text_network; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; @@ -99,34 +98,26 @@ impl ToolMetadata for TextTool { } fn create_text_widgets(tool: &TextTool) -> Vec { - let font = FontInput { - is_style_picker: false, - font_family: tool.options.font_name.clone(), - font_style: tool.options.font_style.clone(), - on_update: WidgetCallback::new(|font_input: &FontInput| { + let font = FontInput::new(&tool.options.font_name, &tool.options.font_style) + .is_style_picker(false) + .on_update(|font_input: &FontInput| { TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { family: font_input.font_family.clone(), style: font_input.font_style.clone(), }) .into() - }), - ..Default::default() - } - .widget_holder(); - let style = FontInput { - is_style_picker: true, - font_family: tool.options.font_name.clone(), - font_style: tool.options.font_style.clone(), - on_update: WidgetCallback::new(|font_input: &FontInput| { + }) + .widget_holder(); + let style = FontInput::new(&tool.options.font_name, &tool.options.font_style) + .is_style_picker(true) + .on_update(|font_input: &FontInput| { TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { family: font_input.font_family.clone(), style: font_input.font_style.clone(), }) .into() - }), - ..Default::default() - } - .widget_holder(); + }) + .widget_holder(); let size = NumberInput::new(Some(tool.options.font_size as f64)) .unit(" px") .label("Size") @@ -146,9 +137,9 @@ impl PropertyHolder for TextTool { widgets.append(&mut self.options.fill.create_widgets( "Fill", true, - WidgetCallback::new(|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into()), + |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()), - WidgetCallback::new(|color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value)).into()), + |color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value)).into(), )); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) @@ -177,10 +168,7 @@ impl<'a> MessageHandler> for TextToo } } - responses.add(LayoutMessage::SendLayout { - layout: self.properties(), - layout_target: LayoutTarget::ToolOptions, - }); + self.register_properties(responses, LayoutTarget::ToolOptions); return; } diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index fafc8f56..38d1e657 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -7,11 +7,8 @@ use crate::messages::broadcast::BroadcastMessage; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, LayoutKeysGroup, MouseMotion}; use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::input_mapper::utility_types::misc::ActionKeys; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::button_widgets::IconButton; -use crate::messages::layout::utility_types::widgets::input_widgets::SwatchPairInput; -use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType}; +use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use crate::node_graph_executor::NodeGraphExecutor; @@ -144,29 +141,20 @@ impl DocumentToolData { pub fn update_working_colors(&self, responses: &mut VecDeque) { let layout = WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::SwatchPairInput(SwatchPairInput { - primary: self.primary_color, - secondary: self.secondary_color, - }))], + widgets: vec![SwatchPairInput::new(self.primary_color, self.secondary_color).widget_holder()], }, LayoutGroup::Row { widgets: vec![ - WidgetHolder::new(Widget::IconButton(IconButton { - size: 16, - icon: "Swap".into(), - tooltip: "Swap".into(), - tooltip_shortcut: action_keys!(ToolMessageDiscriminant::SwapColors), - on_update: WidgetCallback::new(|_| ToolMessage::SwapColors.into()), - ..Default::default() - })), - WidgetHolder::new(Widget::IconButton(IconButton { - size: 16, - icon: "WorkingColors".into(), - tooltip: "Reset".into(), - tooltip_shortcut: action_keys!(ToolMessageDiscriminant::ResetColors), - on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()), - ..Default::default() - })), + IconButton::new("Swap", 16) + .tooltip("Swap") + .tooltip_shortcut(action_keys!(ToolMessageDiscriminant::SwapColors)) + .on_update(|_| ToolMessage::SwapColors.into()) + .widget_holder(), + IconButton::new("WorkingColors", 16) + .tooltip("Reset") + .tooltip_shortcut(action_keys!(ToolMessageDiscriminant::ResetColors)) + .on_update(|_| ToolMessage::ResetColors.into()) + .widget_holder(), ], }, ]); @@ -259,36 +247,29 @@ impl PropertyHolder for ToolData { .iter() .map(|tool_group| tool_group.iter().map(|tool_availability| { match tool_availability { - ToolAvailability::Available(tool) => ToolEntry { - tooltip: tool.tooltip(), - tooltip_shortcut: action_keys!(tool_type_to_activate_tool_message(tool.tool_type())), - icon_name: tool.icon_name(), - tool_type: tool.tool_type(), - }, + ToolAvailability::Available(tool) => ToolEntry::new( tool.tool_type(), tool.icon_name()) + .tooltip( tool.tooltip()) + .tooltip_shortcut(action_keys!(tool_type_to_activate_tool_message(tool.tool_type()))) + + , ToolAvailability::ComingSoon(tool) => tool.clone(), } }).collect::>()) .flat_map(|group| { - let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator { - direction: SeparatorDirection::Vertical, - separator_type: SeparatorType::Section, - }))); + let separator = std::iter::once(Separator::new(SeparatorDirection::Vertical, SeparatorType::Section).widget_holder()); let buttons = group.into_iter().map(|ToolEntry { tooltip, tooltip_shortcut, tool_type, icon_name }| { - WidgetHolder::new(Widget::IconButton(IconButton { - icon: icon_name, - size: 32, - disabled: false, - active: self.active_tool_type == tool_type, - tooltip: tooltip.clone(), - tooltip_shortcut, - on_update: WidgetCallback::new(move |_| { + IconButton::new(icon_name, 32) + .disabled( false) + .active( self.active_tool_type == tool_type) + .tooltip( tooltip.clone()) + .tooltip_shortcut(tooltip_shortcut) + .on_update(move |_| { if !tooltip.contains("Coming Soon") { ToolMessage::ActivateTool { tool_type }.into() } else { DialogMessage::RequestComingSoonDialog { issue: None }.into() } - }), - })) + }).widget_holder() }); separator.chain(buttons) @@ -303,12 +284,15 @@ impl PropertyHolder for ToolData { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default, WidgetBuilder)] +#[widget_builder(not_widget_holder)] pub struct ToolEntry { + #[widget_builder(constructor)] + pub tool_type: ToolType, + #[widget_builder(constructor)] + pub icon_name: String, pub tooltip: String, pub tooltip_shortcut: Option, - pub icon_name: String, - pub tool_type: ToolType, } #[derive(Debug)] @@ -346,9 +330,10 @@ impl ToolFsmState { } #[repr(usize)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, specta::Type)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default, specta::Type)] pub enum ToolType { // General tool group + #[default] Select, Artboard, Navigate, @@ -412,36 +397,11 @@ fn list_tools_in_groups() -> Vec> { ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), - ToolAvailability::ComingSoon(ToolEntry { - tool_type: ToolType::Heal, - icon_name: "RasterHealTool".into(), - tooltip: "Coming Soon: Heal Tool (J)".into(), - tooltip_shortcut: None, - }), - ToolAvailability::ComingSoon(ToolEntry { - tool_type: ToolType::Clone, - icon_name: "RasterCloneTool".into(), - tooltip: "Coming Soon: Clone Tool (C)".into(), - tooltip_shortcut: None, - }), - ToolAvailability::ComingSoon(ToolEntry { - tool_type: ToolType::Patch, - icon_name: "RasterPatchTool".into(), - tooltip: "Coming Soon: Patch Tool".into(), - tooltip_shortcut: None, - }), - ToolAvailability::ComingSoon(ToolEntry { - tool_type: ToolType::Detail, - icon_name: "RasterDetailTool".into(), - tooltip: "Coming Soon: Detail Tool (D)".into(), - tooltip_shortcut: None, - }), - ToolAvailability::ComingSoon(ToolEntry { - tool_type: ToolType::Relight, - icon_name: "RasterRelightTool".into(), - tooltip: "Coming Soon: Relight Tool (O)".into(), - tooltip_shortcut: None, - }), + ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Heal, "RasterHealTool").tooltip("Coming Soon: Heal Tool (J)")), + ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Clone, "RasterCloneTool").tooltip("Coming Soon: Clone Tool (C)")), + ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Patch, "RasterPatchTool").tooltip("Coming Soon: Patch Tool")), + ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Detail, "RasterDetailTool").tooltip("Coming Soon: Detail Tool (D)")), + ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Relight, "RasterRelightTool").tooltip("Coming Soon: Relight Tool (O)")), ], ] }