diff --git a/.branding b/.branding index 9aaf3e42cb..ab3a12bc40 100644 --- a/.branding +++ b/.branding @@ -1,2 +1,2 @@ -https://github.com/Keavon/graphite-branded-assets/archive/0d004aa61e6b48d316e8e5db6d59ccc4788f192d.tar.gz -772d64518be43c99977ba56f69e574531c56e83d2df2f42ab066f77f74b0dd1f +https://github.com/Keavon/graphite-branded-assets/archive/6687dc6d3d8552948458d00d35e2bee3a40ab66b.tar.gz +7663b4a35f377a5d3363430a725a1d5f634fbdc56adca5e43167786b8fe1b253 diff --git a/editor/src/messages/color_picker/color_picker_message_handler.rs b/editor/src/messages/color_picker/color_picker_message_handler.rs index a74ec14cd9..740593df04 100644 --- a/editor/src/messages/color_picker/color_picker_message_handler.rs +++ b/editor/src/messages/color_picker/color_picker_message_handler.rs @@ -623,14 +623,7 @@ impl ColorPickerMessageHandler { // Gradient spread (only present when the picker is in gradient mode) if self.gradient.is_some() { - let entries = [GradientSpread::Pad, GradientSpread::Reflect, GradientSpread::Repeat] - .into_iter() - .map(|gradient_spread| { - RadioEntryData::new(format!("{gradient_spread:?}")) - .label(gradient_spread.to_string()) - .on_update(move |_| ColorPickerMessage::SetGradientSpread { gradient_spread }.into()) - }) - .collect(); + let entries = RadioEntryData::list_from_choice_type(|gradient_spread| ColorPickerMessage::SetGradientSpread { gradient_spread }.into()); groups.push(LayoutGroup::row(vec![ TextLabel::new("Ends").tooltip_label("Gradient Spread").tooltip_description(ENDS_DESCRIPTION).widget_instance(), diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index e4bf83e02f..49adf893d3 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -1,6 +1,7 @@ use crate::messages::frontend::IconName; use crate::messages::input_mapper::utility_types::misc::ActionShortcut; use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::message::Message; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use derivative::*; use graphene_std::Color; @@ -344,6 +345,30 @@ pub struct RadioEntryData { pub on_commit: WidgetCallback<()>, } +impl RadioEntryData { + /// One entry per variant of a choice type enum, shown as the variant's icon when it has one and its text label otherwise. + pub fn list_from_choice_type(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> Vec + where + E: graphene_std::choice_type::ChoiceTypeStatic + 'static, + { + E::list() + .iter() + .flat_map(|section| section.iter()) + .map(|(variant, metadata)| { + let to_message = to_message.clone(); + let variant = *variant; + + let entry = RadioEntryData::new(metadata.name) + .tooltip_label(metadata.label) + .tooltip_description(metadata.description.unwrap_or_default()) + .on_update(move |_| to_message(variant)); + + if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry.label(metadata.label) } + }) + .collect() + } +} + #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index c4234815d2..a9e2ac8fba 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2629,7 +2629,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte .iter() .map(|&gradient_form| { RadioEntryData::new(format!("{:?}", gradient_form)) - .label(format!("{:?}", gradient_form)) + .label(gradient_form.to_string()) .on_update(update_value(move |_| TaggedValue::GradientForm(gradient_form), node_id, GradientFormInput)) .on_commit(commit_value) }) @@ -2931,20 +2931,18 @@ pub mod choice { U: Fn(&E) -> Message + 'static + Send + Sync, C: Fn(&()) -> Message + 'static + Send + Sync, { - let items = E::list() - .iter() - .flat_map(|section| section.iter()) - .map(|(item, var_meta)| { - let updater = updater_factory(); - let committer = committer_factory(); - let entry = RadioEntryData::new(var_meta.name) - .on_update(move |_| updater(item)) - .on_commit(committer) - .tooltip_label(var_meta.label) - .tooltip_description(var_meta.description.unwrap_or_default()); - if let Some(icon) = var_meta.icon { entry.icon(icon) } else { entry.label(var_meta.label) } + // The entry builder clones one callback across all variants, so each factory yields a single shared handle + let updater = std::sync::Arc::new(updater_factory()); + let committer = std::sync::Arc::new(committer_factory()); + + let items = RadioEntryData::list_from_choice_type(move |variant: E| updater(&variant)) + .into_iter() + .map(|entry| { + let committer = committer.clone(); + entry.on_commit(move |value| committer(value)) }) .collect(); + RadioInput::new(items).selected_index(Some(current.as_u32())).disabled(self.disabled).widget_instance() } } diff --git a/editor/src/messages/tool/common_functionality/stroke_options.rs b/editor/src/messages/tool/common_functionality/stroke_options.rs index 95d5b9d59a..a03ecfa05e 100644 --- a/editor/src/messages/tool/common_functionality/stroke_options.rs +++ b/editor/src/messages/tool/common_functionality/stroke_options.rs @@ -123,19 +123,9 @@ where E: ChoiceTypeStatic + 'static, F: Fn(E) -> Message + 'static + Send + Sync + Clone, { - let entries = E::list() - .iter() - .flat_map(|section| section.iter()) - .map(|(value, meta)| { - let to_message = to_message.clone(); - let value = *value; - let entry = RadioEntryData::new(meta.name) - .tooltip_label(meta.label) - .tooltip_description(meta.description.unwrap_or_default()) - .on_update(move |_| to_message(value)) - .on_commit(|_| DocumentMessage::StartTransaction.into()); - if let Some(icon) = meta.icon { entry.icon(icon) } else { entry.label(meta.label) } - }) + let entries = RadioEntryData::list_from_choice_type(to_message) + .into_iter() + .map(|entry| entry.on_commit(|_| DocumentMessage::StartTransaction.into())) .collect(); vec![ TextLabel::new(label_text).table_align(true).widget_instance(), diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index ca0b39864b..12cdec5a60 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -231,22 +231,13 @@ impl LayoutHolder for GradientTool { fn layout(&self) -> Layout { let mut widgets: Vec = Vec::new(); - let gradient_form = RadioInput::new(vec![ - RadioEntryData::new("Linear").label("Linear").tooltip_label("Linear Gradient").on_update(move |_| { - GradientToolMessage::UpdateOptions { - options: GradientOptionsUpdate::Form(GradientForm::Linear), - } - .into() - }), - RadioEntryData::new("Radial").label("Radial").tooltip_label("Radial Gradient").on_update(move |_| { - GradientToolMessage::UpdateOptions { - options: GradientOptionsUpdate::Form(GradientForm::Radial), - } - .into() - }), - ]) - .selected_index(Some((self.options.gradient_form == GradientForm::Radial) as u32)) - .widget_instance(); + let gradient_form_entries = RadioEntryData::list_from_choice_type(|gradient_form| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::Form(gradient_form), + } + .into() + }); + let gradient_form = RadioInput::new(gradient_form_entries).selected_index(Some(self.options.gradient_form as u32)).widget_instance(); // Display priority: the selected layer's stops, then any user-customized tool default, then the working colors let stops_value = self.data.current_gradient_stops.clone().or_else(|| self.data.default_gradient_stops.clone()).unwrap_or_else(|| { diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 7348dcf387..beae60848f 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -217,26 +217,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetInstance { } fn create_arc_type_widget(arc_type: ArcType) -> WidgetInstance { - let entries = vec![ - RadioEntryData::new("Open").label("Open").on_update(move |_| { - ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::ArcType(ArcType::Open), - } - .into() - }), - RadioEntryData::new("Closed").label("Closed").on_update(move |_| { - ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::ArcType(ArcType::Closed), - } - .into() - }), - RadioEntryData::new("Pie").label("Pie").on_update(move |_| { - ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::ArcType(ArcType::PieSlice), - } - .into() - }), - ]; + let entries = RadioEntryData::list_from_choice_type(|arc_type| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ArcType(arc_type), + } + .into() + }); RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_instance() } @@ -307,20 +293,12 @@ fn create_spiral_type_widget(spiral_type: SpiralType) -> WidgetInstance { } fn create_grid_type_widget(grid_type: GridType) -> WidgetInstance { - let entries = vec![ - RadioEntryData::new("Rectangular").label("Rectangular").on_update(move |_| { - ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::GridType(GridType::Rectangular), - } - .into() - }), - RadioEntryData::new("Isometric").label("Isometric").on_update(move |_| { - ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::GridType(GridType::Isometric), - } - .into() - }), - ]; + let entries = RadioEntryData::list_from_choice_type(|grid_type| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::GridType(grid_type), + } + .into() + }); RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_instance() } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 2ff7319432..cc88f6c627 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -20,7 +20,6 @@ use crate::messages::tool::utility_types::ToolRefreshOptions; use graph_craft::application_io::resource::ResourceId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::choice_type::ChoiceTypeStatic; use graphene_std::color::SRGBA8; use graphene_std::renderer::Quad; use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping}; @@ -214,23 +213,12 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D .into() }) .widget_instance(); - let align_entries: Vec<_> = TextAlign::list() - .iter() - .flat_map(|section| section.iter()) - .map(|(item, var_meta)| { - let align = *item; - let entry = RadioEntryData::new(var_meta.name) - .tooltip_label(var_meta.label) - .tooltip_description(var_meta.description.unwrap_or_default()) - .on_update(move |_| { - TextToolMessage::UpdateOptions { - options: TextOptionsUpdate::Align(align), - } - .into() - }); - if let Some(icon) = var_meta.icon { entry.icon(icon) } else { entry.label(var_meta.label) } - }) - .collect(); + let align_entries = RadioEntryData::list_from_choice_type(|align| { + TextToolMessage::UpdateOptions { + options: TextOptionsUpdate::Align(align), + } + .into() + }); let align = RadioInput::new(align_entries).selected_index(Some(tool.options.align as u32)).widget_instance(); vec![ font, diff --git a/frontend/src/icons.ts b/frontend/src/icons.ts index c827390157..f1562530cf 100644 --- a/frontend/src/icons.ts +++ b/frontend/src/icons.ts @@ -19,6 +19,10 @@ import Empty12px from "/../branding/assets/icon-12px-solid/empty-12px.svg"; import Failure from "/../branding/assets/icon-12px-solid/failure.svg"; import FullscreenEnter from "/../branding/assets/icon-12px-solid/fullscreen-enter.svg"; import FullscreenExit from "/../branding/assets/icon-12px-solid/fullscreen-exit.svg"; +import GradientSpreadClear from "/../branding/assets/icon-12px-solid/gradient-spread-clear.svg"; +import GradientSpreadPad from "/../branding/assets/icon-12px-solid/gradient-spread-pad.svg"; +import GradientSpreadReflect from "/../branding/assets/icon-12px-solid/gradient-spread-reflect.svg"; +import GradientSpreadRepeat from "/../branding/assets/icon-12px-solid/gradient-spread-repeat.svg"; import GridDotted from "/../branding/assets/icon-12px-solid/grid-dotted.svg"; import Grid from "/../branding/assets/icon-12px-solid/grid.svg"; import Info from "/../branding/assets/icon-12px-solid/info.svg"; @@ -67,6 +71,10 @@ const SOLID_12PX = { Failure: { svg: Failure, size: 12 }, FullscreenEnter: { svg: FullscreenEnter, size: 12 }, FullscreenExit: { svg: FullscreenExit, size: 12 }, + GradientSpreadClear: { svg: GradientSpreadClear, size: 12 }, + GradientSpreadPad: { svg: GradientSpreadPad, size: 12 }, + GradientSpreadReflect: { svg: GradientSpreadReflect, size: 12 }, + GradientSpreadRepeat: { svg: GradientSpreadRepeat, size: 12 }, Grid: { svg: Grid, size: 12 }, GridDotted: { svg: GridDotted, size: 12 }, Info: { svg: Info, size: 12 }, diff --git a/frontend/src/stores/tooltip.ts b/frontend/src/stores/tooltip.ts index 21444a79c4..e2f431359a 100644 --- a/frontend/src/stores/tooltip.ts +++ b/frontend/src/stores/tooltip.ts @@ -89,7 +89,13 @@ export function destroyTooltipStore() { // Listen for mouse movements onto tooltip-bearing HTML elements to track the future target of a tooltip function onMouseOver(e: MouseEvent) { - const element = (e.target instanceof Element && e.target.closest("[data-tooltip-label], [data-tooltip-description], [data-tooltip-shortcut]")) || undefined; + const target = (e.target instanceof Element && e.target) || undefined; + let element = target?.closest("[data-tooltip-label], [data-tooltip-description], [data-tooltip-shortcut]") || undefined; + + // A floating menu renders within the DOM of the widget that spawned it, so a match beyond the menu's own content + // is the spawner's tooltip rather than one belonging to whatever the cursor is actually over + const floatingMenuContent = target?.closest("[data-floating-menu-content]"); + if (element && floatingMenuContent && !floatingMenuContent.contains(element)) element = undefined; update((state) => { state.visible = false; diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 27d481aa63..9b40ef2d96 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -10,8 +10,10 @@ use glam::{DAffine2, DVec2}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum GradientForm { + /// Transitions the colors along a straight line. #[default] Linear, + /// Transitions the colors outward from a center point. Radial, } @@ -787,10 +789,13 @@ impl Gradient { pub enum GradientSpread { /// Extends the end colors outward. #[default] + #[icon("GradientSpreadPad")] Pad, /// Loops the gradient by mirroring back-and-forth. + #[icon("GradientSpreadReflect")] Reflect, /// Loops the gradient as copies of itself. + #[icon("GradientSpreadRepeat")] Repeat, // TODO: Add a "Clear" variant that returns transparent black outside the gradient's range } diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index f7a201f783..fbe2df1569 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -155,9 +155,12 @@ pub enum GridType { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum ArcType { + /// Leaves the two ends of the arc unconnected. #[default] Open = 0, + /// Connects the two ends of the arc with a straight line. Closed, + /// Connects the two ends of the arc to its center, forming a wedge. PieSlice, }