Make font selection show a live preview on hover; move its code to the backend (#3487)

* Remove FontInput.svelte

* Move font picking to the backend

* Fix Text tool font choice style turning to "-" on font that doesn't support previous style
This commit is contained in:
Keavon Chambers
2025-12-19 22:17:28 -08:00
committed by GitHub
parent 6f087eb981
commit 2d6d054359
31 changed files with 766 additions and 702 deletions
@@ -2,7 +2,6 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use graphene_std::raster::color::Color;
use graphene_std::text::Font;
use graphene_std::vector::style::{FillChoice, GradientStops};
use serde_json::Value;
use std::collections::HashMap;
@@ -48,7 +47,6 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
}
LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value } => {
self.handle_widget_callback(layout_target, widget_id, value, WidgetValueAction::Update, responses);
responses.add(LayoutMessage::ResendActiveWidget { layout_target, widget_id });
}
}
}
@@ -264,44 +262,6 @@ impl LayoutMessageHandler {
responses.add(callback_message);
}
Widget::FontInput(font_input) => {
let callback_message = match action {
WidgetValueAction::Commit => (font_input.on_commit.callback)(&()),
WidgetValueAction::Update => {
let Some(update_value) = value.as_object() else {
error!("FontInput update was not of type: object");
return;
};
let Some(font_family_value) = update_value.get("fontFamily") else {
error!("FontInput update does not have a fontFamily");
return;
};
let Some(font_style_value) = update_value.get("fontStyle") else {
error!("FontInput update does not have a fontStyle");
return;
};
let Some(font_family) = font_family_value.as_str() else {
error!("FontInput update fontFamily was not of type: string");
return;
};
let Some(font_style) = font_style_value.as_str() else {
error!("FontInput update fontStyle was not of type: string");
return;
};
font_input.font_family = font_family.into();
font_input.font_style = font_style.into();
responses.add(PortfolioMessage::LoadFont {
font: Font::new(font_family.into(), font_style.into()),
});
(font_input.on_update.callback)(font_input)
}
};
responses.add(callback_message);
}
Widget::IconButton(icon_button) => {
let callback_message = match action {
WidgetValueAction::Commit => (icon_button.on_commit.callback)(&()),
@@ -353,43 +353,7 @@ impl From<Vec<WidgetInstance>> for LayoutGroup {
}
impl LayoutGroup {
/// Applies a tooltip label to all widgets in this row or column without a tooltip.
pub fn with_tooltip_label(self, label: impl Into<String>) -> Self {
let (is_col, mut widgets) = match self {
LayoutGroup::Column { widgets } => (true, widgets),
LayoutGroup::Row { widgets } => (false, widgets),
_ => unimplemented!(),
};
let label = label.into();
for widget in &mut widgets {
let val = match &mut widget.widget {
Widget::CheckboxInput(x) => &mut x.tooltip_label,
Widget::ColorInput(x) => &mut x.tooltip_label,
Widget::CurveInput(x) => &mut x.tooltip_label,
Widget::DropdownInput(x) => &mut x.tooltip_label,
Widget::FontInput(x) => &mut x.tooltip_label,
Widget::IconButton(x) => &mut x.tooltip_label,
Widget::IconLabel(x) => &mut x.tooltip_label,
Widget::ImageButton(x) => &mut x.tooltip_label,
Widget::ImageLabel(x) => &mut x.tooltip_label,
Widget::NumberInput(x) => &mut x.tooltip_label,
Widget::ParameterExposeButton(x) => &mut x.tooltip_label,
Widget::PopoverButton(x) => &mut x.tooltip_label,
Widget::TextAreaInput(x) => &mut x.tooltip_label,
Widget::TextButton(x) => &mut x.tooltip_label,
Widget::TextInput(x) => &mut x.tooltip_label,
Widget::TextLabel(x) => &mut x.tooltip_label,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_label,
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::ShortcutLabel(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
};
if val.is_empty() {
val.clone_from(&label);
}
}
if is_col { Self::Column { widgets } } else { Self::Row { widgets } }
}
/// Applies a tooltip description to all widgets in this row or column without a tooltip.
/// Applies a tooltip description to all widgets without a tooltip in this row or column.
pub fn with_tooltip_description(self, description: impl Into<String>) -> Self {
let (is_col, mut widgets) = match self {
LayoutGroup::Column { widgets } => (true, widgets),
@@ -403,20 +367,24 @@ impl LayoutGroup {
Widget::ColorInput(x) => &mut x.tooltip_description,
Widget::CurveInput(x) => &mut x.tooltip_description,
Widget::DropdownInput(x) => &mut x.tooltip_description,
Widget::FontInput(x) => &mut x.tooltip_description,
Widget::IconButton(x) => &mut x.tooltip_description,
Widget::IconLabel(x) => &mut x.tooltip_description,
Widget::ImageButton(x) => &mut x.tooltip_description,
Widget::ImageLabel(x) => &mut x.tooltip_description,
Widget::NumberInput(x) => &mut x.tooltip_description,
Widget::ParameterExposeButton(x) => &mut x.tooltip_description,
Widget::PopoverButton(x) => &mut x.tooltip_description,
Widget::TextAreaInput(x) => &mut x.tooltip_description,
Widget::TextButton(x) => &mut x.tooltip_description,
Widget::TextInput(x) => &mut x.tooltip_description,
Widget::TextLabel(x) => &mut x.tooltip_description,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_description,
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::ShortcutLabel(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
Widget::ReferencePointInput(_)
| Widget::RadioInput(_)
| Widget::Separator(_)
| Widget::ShortcutLabel(_)
| Widget::WorkingColorsInput(_)
| Widget::NodeCatalog(_)
| Widget::ParameterExposeButton(_) => continue,
};
if val.is_empty() {
val.clone_from(&description);
@@ -727,7 +695,6 @@ pub enum Widget {
ColorInput(ColorInput),
CurveInput(CurveInput),
DropdownInput(DropdownInput),
FontInput(FontInput),
IconButton(IconButton),
IconLabel(IconLabel),
ImageButton(ImageButton),
@@ -782,7 +749,6 @@ impl DiffUpdate {
Widget::CheckboxInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ColorInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::DropdownInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::FontInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::IconButton(widget) => widget.tooltip_shortcut.as_mut(),
Widget::NumberInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ParameterExposeButton(widget) => widget.tooltip_shortcut.as_mut(),
@@ -838,10 +804,38 @@ impl DiffUpdate {
(recursive_wrapper.0)(entry_sections, &recursive_wrapper)
};
// Hash the menu list entry sections for caching purposes
let hash_menu_list_entry_sections = |entry_sections: &MenuListEntrySections| {
struct RecursiveHasher<'a> {
hasher: DefaultHasher,
hash_fn: &'a dyn Fn(&mut RecursiveHasher, &MenuListEntrySections),
}
let mut recursive_hasher = RecursiveHasher {
hasher: DefaultHasher::new(),
hash_fn: &|recursive_hasher, entry_sections| {
for (index, entries) in entry_sections.iter().enumerate() {
index.hash(&mut recursive_hasher.hasher);
for entry in entries {
entry.hash(&mut recursive_hasher.hasher);
(recursive_hasher.hash_fn)(recursive_hasher, &entry.children);
}
}
},
};
(recursive_hasher.hash_fn)(&mut recursive_hasher, entry_sections);
recursive_hasher.hasher.finish()
};
// Apply shortcut conversions to all widgets that have menu lists
let convert_menu_lists = |widget_instance: &mut WidgetInstance| match &mut widget_instance.widget {
Widget::DropdownInput(dropdown_input) => apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries),
Widget::TextButton(text_button) => apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children),
Widget::DropdownInput(dropdown_input) => {
apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries);
dropdown_input.entries_hash = hash_menu_list_entry_sections(&dropdown_input.entries);
}
Widget::TextButton(text_button) => {
apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children);
text_button.menu_list_children_hash = hash_menu_list_entry_sections(&text_button.menu_list_children);
}
_ => {}
};
@@ -144,6 +144,10 @@ pub struct TextButton {
#[serde(rename = "menuListChildren")]
pub menu_list_children: MenuListEntrySections,
#[serde(rename = "menuListChildrenHash")]
#[widget_builder(skip)]
pub menu_list_children_hash: u64,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -80,6 +80,10 @@ pub struct DropdownInput {
#[widget_builder(constructor)]
pub entries: MenuListEntrySections,
#[serde(rename = "entriesHash")]
#[widget_builder(skip)]
pub entries_hash: u64,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
#[serde(rename = "selectedIndex")]
pub selected_index: Option<u32>,
@@ -94,6 +98,9 @@ pub struct DropdownInput {
pub narrow: bool,
#[serde(rename = "virtualScrolling")]
pub virtual_scrolling: bool,
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
@@ -142,6 +149,10 @@ pub struct MenuListEntry {
pub children: MenuListEntrySections,
#[serde(rename = "childrenHash")]
#[widget_builder(skip)]
pub children_hash: u64,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -152,39 +163,13 @@ pub struct MenuListEntry {
pub on_commit: WidgetCallback<()>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct FontInput {
#[serde(rename = "fontFamily")]
#[widget_builder(constructor)]
pub font_family: String,
#[serde(rename = "fontStyle")]
#[widget_builder(constructor)]
pub font_style: String,
#[serde(rename = "isStyle")]
pub is_style_picker: bool,
pub disabled: bool,
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<FontInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
impl std::hash::Hash for MenuListEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.value.hash(state);
self.label.hash(state);
self.icon.hash(state);
self.disabled.hash(state);
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]