Replace text-only tooltips with custom richly styled tooltips (#3436)

* Replace the title attribute with custom FloatingMenu tooltips

* Separate tooltip labels and descriptions into two styled blocks

* Move keyboard shortcut tooltips to a separate section at the bottom

* Update shortcut key styling in tooltips and hints bar

* Fix .to_string()
This commit is contained in:
Keavon Chambers
2025-11-30 13:32:58 -08:00
committed by GitHub
parent 94e5c8fc05
commit e8ebcc2c21
94 changed files with 1323 additions and 580 deletions
@@ -42,7 +42,7 @@ impl ToolMetadata for ArtboardTool {
fn icon_name(&self) -> String {
"GeneralArtboardTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Artboard Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -90,7 +90,7 @@ impl ToolMetadata for BrushTool {
fn icon_name(&self) -> String {
"RasterBrushTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Brush Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -215,7 +215,7 @@ impl LayoutHolder for BrushTool {
widgets.push(
DropdownInput::new(blend_mode_entries)
.selected_index(self.options.blend_mode.index_in_list().map(|index| index as u32))
.tooltip("The blend mode used with the background when performing a brush stroke. Only used in draw mode.")
.tooltip_description("The blend mode used with the background when performing a brush stroke. Only used in draw mode.")
.disabled(self.options.draw_mode != DrawMode::Draw)
.widget_holder(),
);
@@ -25,7 +25,7 @@ impl ToolMetadata for EyedropperTool {
fn icon_name(&self) -> String {
"GeneralEyedropperTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Eyedropper Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -27,7 +27,7 @@ impl ToolMetadata for FillTool {
fn icon_name(&self) -> String {
"GeneralFillTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Fill Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -72,7 +72,7 @@ impl ToolMetadata for FreehandTool {
fn icon_name(&self) -> String {
"VectorFreehandTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Freehand Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -45,7 +45,7 @@ impl ToolMetadata for GradientTool {
fn icon_name(&self) -> String {
"GeneralGradientTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Gradient Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -91,13 +91,13 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
impl LayoutHolder for GradientTool {
fn layout(&self) -> Layout {
let gradient_type = RadioInput::new(vec![
RadioEntryData::new("Linear").label("Linear").tooltip("Linear gradient").on_update(move |_| {
RadioEntryData::new("Linear").label("Linear").tooltip_label("Linear Gradient").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Type(GradientType::Linear),
}
.into()
}),
RadioEntryData::new("Radial").label("Radial").tooltip("Radial gradient").on_update(move |_| {
RadioEntryData::new("Radial").label("Radial").tooltip_label("Radial Gradient").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Type(GradientType::Radial),
}
@@ -24,7 +24,7 @@ impl ToolMetadata for NavigateTool {
fn icon_name(&self) -> String {
"GeneralNavigateTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Navigate Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -4,6 +4,7 @@ use crate::consts::{
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD,
DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
};
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments};
@@ -184,7 +185,7 @@ impl ToolMetadata for PathTool {
fn icon_name(&self) -> String {
"VectorPathTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Path Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -235,7 +236,7 @@ impl LayoutHolder for PathTool {
let related_seperator = Separator::new(SeparatorType::Related).widget_holder();
let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder();
let colinear_handles_tooltip = "Keep both handles unbent, each 180° apart, when moving either";
let colinear_handles_description = "Keep both handles unbent, each 180° apart, when moving either.";
let colinear_handles_state = manipulator_angle.and_then(|angle| match angle {
ManipulatorAngle::Colinear => Some(true),
ManipulatorAngle::Free => Some(false),
@@ -253,32 +254,39 @@ impl LayoutHolder for PathTool {
PathToolMessage::ManipulatorMakeHandlesFree.into()
}
})
.tooltip(colinear_handles_tooltip)
.tooltip_label("Colinear Handles")
.tooltip_description(colinear_handles_description)
.for_label(checkbox_id)
.widget_holder();
let colinear_handles_label = TextLabel::new("Colinear Handles")
.disabled(!self.tool_data.can_toggle_colinearity)
.tooltip(colinear_handles_tooltip)
.tooltip_label("Colinear Handles")
.tooltip_description(colinear_handles_description)
.for_checkbox(checkbox_id)
.widget_holder();
let point_editing_mode = CheckboxInput::new(self.options.path_editing_mode.point_editing_mode)
// TODO(Keavon): Replace with a real icon
.icon("Dot")
.tooltip("Point Editing Mode\n\nShift + click to select both modes.")
.tooltip_label("Point Editing Mode")
.tooltip_description("To multi-select modes, perform the shortcut shown.")
.tooltip_shortcut(KeysGroup(vec![Key::Shift, Key::MouseLeft]).to_string())
.on_update(|_| PathToolMessage::TogglePointEditing.into())
.widget_holder();
let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode)
// TODO(Keavon): Replace with a real icon
.icon("Remove")
.tooltip("Segment Editing Mode\n\nShift + click to select both modes.")
.tooltip_label("Segment Editing Mode")
.tooltip_description("To multi-select modes, perform the shortcut shown.")
.tooltip_shortcut(KeysGroup(vec![Key::Shift, Key::MouseLeft]).to_string())
.on_update(|_| PathToolMessage::ToggleSegmentEditing.into())
.widget_holder();
let path_overlay_mode_widget = RadioInput::new(vec![
RadioEntryData::new("all")
.icon("HandleVisibilityAll")
.tooltip("Show all handles regardless of selection")
.tooltip_label("Show All Handles")
.tooltip_description("Show all handles regardless of selection.")
.on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles),
@@ -287,7 +295,8 @@ impl LayoutHolder for PathTool {
}),
RadioEntryData::new("selected")
.icon("HandleVisibilitySelected")
.tooltip("Show only handles of the segments connected to selected points")
.tooltip_label("Show Connected Handles")
.tooltip_description("Show only handles of the segments connected to selected points.")
.on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles),
@@ -296,7 +305,8 @@ impl LayoutHolder for PathTool {
}),
RadioEntryData::new("frontier")
.icon("HandleVisibilityFrontier")
.tooltip("Show only handles at the frontiers of the segments connected to selected points")
.tooltip_label("Show Frontier Handles")
.tooltip_description("Show only handles at the frontiers of the segments connected to selected points.")
.on_update(move |_| {
PathToolMessage::UpdateOptions {
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles),
@@ -310,7 +320,10 @@ impl LayoutHolder for PathTool {
// Works only if a single layer is selected and its type is Vector
let path_node_button = TextButton::new("Make Path Editable")
.icon(Some("NodeShape".into()))
.tooltip("Make Path Editable")
.tooltip_label("Make Path Editable")
.tooltip_description(
"Enables the Pen and Path tools to directly edit layer geometry resulting from nondestructive operations. This inserts a 'Path' node as the last operation of the selected layer.",
)
.on_update(|_| NodeGraphMessage::AddPathNode.into())
.disabled(!self.tool_data.make_path_editable_is_allowed)
.widget_holder();
@@ -128,7 +128,7 @@ impl ToolMetadata for PenTool {
fn icon_name(&self) -> String {
"VectorPenTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Pen Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -215,7 +215,8 @@ impl LayoutHolder for PenTool {
RadioInput::new(vec![
RadioEntryData::new("all")
.icon("HandleVisibilityAll")
.tooltip("Show all handles regardless of selection")
.tooltip_label("Show All Handles")
.tooltip_description("Show all handles regardless of selection.")
.on_update(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles),
@@ -224,7 +225,8 @@ impl LayoutHolder for PenTool {
}),
RadioEntryData::new("frontier")
.icon("HandleVisibilityFrontier")
.tooltip("Show only handles at the frontiers of the segments connected to selected points")
.tooltip_label("Show Frontier Handles")
.tooltip_description("Show only handles at the frontiers of the segments connected to selected points.")
.on_update(move |_| {
PenToolMessage::UpdateOptions {
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles),
@@ -122,7 +122,7 @@ impl ToolMetadata for SelectTool {
fn icon_name(&self) -> String {
"GeneralSelectTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Select Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -146,10 +146,9 @@ impl SelectTool {
DropdownInput::new(vec![layer_selection_behavior_entries])
.selected_index(Some((self.tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest) as u32))
.tooltip(
"Selection Mode\n\
\n\
Shallow Select: clicks initially select the least-nested layers and double clicks drill deeper into the folder hierarchy.\n\
.tooltip_label("Selection Mode")
.tooltip_description(
"Shallow Select: clicks initially select the least-nested layers and double clicks drill deeper into the folder hierarchy.\n\
Deep Select: clicks directly select the most-nested layers in the folder hierarchy.",
)
.widget_holder()
@@ -160,7 +159,7 @@ impl SelectTool {
.into_iter()
.flat_map(|axis| [(axis, AlignAggregate::Min), (axis, AlignAggregate::Center), (axis, AlignAggregate::Max)])
.map(move |(axis, aggregate)| {
let (icon, tooltip) = match (axis, aggregate) {
let (icon, label) = match (axis, aggregate) {
(AlignAxis::X, AlignAggregate::Min) => ("AlignLeft", "Align Left"),
(AlignAxis::X, AlignAggregate::Center) => ("AlignHorizontalCenter", "Align Horizontal Center"),
(AlignAxis::X, AlignAggregate::Max) => ("AlignRight", "Align Right"),
@@ -169,7 +168,7 @@ impl SelectTool {
(AlignAxis::Y, AlignAggregate::Max) => ("AlignBottom", "Align Bottom"),
};
IconButton::new(icon, 24)
.tooltip(tooltip)
.tooltip_label(label)
.on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into())
.disabled(disabled)
.widget_holder()
@@ -177,21 +176,23 @@ impl SelectTool {
}
fn flip_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetHolder> + use<> {
[(FlipAxis::X, "Horizontal"), (FlipAxis::Y, "Vertical")].into_iter().map(move |(flip_axis, name)| {
IconButton::new("Flip".to_string() + name, 24)
.tooltip("Flip ".to_string() + name)
.on_update(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into())
.disabled(disabled)
.widget_holder()
})
[(FlipAxis::X, "FlipHorizontal", "Flip Horizontal"), (FlipAxis::Y, "FlipVertical", "Flip Vertical")]
.into_iter()
.map(move |(flip_axis, icon, label)| {
IconButton::new(icon, 24)
.tooltip_label(label)
.on_update(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into())
.disabled(disabled)
.widget_holder()
})
}
fn turn_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetHolder> + use<> {
[(-90., "TurnNegative90", "Turn -90°"), (90., "TurnPositive90", "Turn 90°")]
.into_iter()
.map(move |(degrees, icon, name)| {
.map(move |(degrees, icon, label)| {
IconButton::new(icon, 24)
.tooltip(name)
.tooltip_label(label)
.on_update(move |_| DocumentMessage::RotateSelectedLayers { degrees }.into())
.disabled(disabled)
.widget_holder()
@@ -201,13 +202,9 @@ impl SelectTool {
fn boolean_widgets(&self, selected_count: usize) -> impl Iterator<Item = WidgetHolder> + use<> {
let list = <BooleanOperation as graphene_std::choice_type::ChoiceTypeStatic>::list();
list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| {
let mut tooltip = info.label.to_string();
if let Some(doc) = info.docstring {
tooltip.push_str("\n\n");
tooltip.push_str(doc);
}
IconButton::new(info.icon.unwrap(), 24)
.tooltip(tooltip)
.tooltip_label(info.label)
.tooltip_description(info.description.unwrap_or_default())
.disabled(selected_count == 0)
.on_update(move |_| {
let group_folder_type = GroupFolderType::BooleanOperation(*operation);
@@ -432,7 +432,7 @@ impl ToolMetadata for ShapeTool {
fn icon_name(&self) -> String {
"VectorPolygonTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Shape Tool".into()
}
fn tool_type(&self) -> ToolType {
@@ -79,7 +79,7 @@ impl ToolMetadata for SplineTool {
fn icon_name(&self) -> String {
"VectorSplineTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Spline Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
@@ -88,7 +88,7 @@ impl ToolMetadata for TextTool {
fn icon_name(&self) -> String {
"VectorTextTool".into()
}
fn tooltip(&self) -> String {
fn tooltip_label(&self) -> String {
"Text Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {