Refactor the old menu bar plumbing to use standard TextButtons (#3444)
Some checks are pending
Editor: Dev & CI / build (push) Waiting to run
Editor: Dev & CI / cargo-deny (push) Waiting to run

* Refactor the old menu bar plumbing to use standard TextButtons

* WIP: Fix Mac native menu bar

* WIP: fix desktop menu bar mac

* Refactor menu bar definitions to use the builder pattern

* WIP: fixup desktop

* cleanup

* fix linux

* Remove dead code that was failing to lint

---------

Co-authored-by: Timon Schelling <me@timon.zip>
This commit is contained in:
Keavon Chambers 2025-12-03 04:41:54 -08:00 committed by GitHub
parent 3fd0460d03
commit 600fb5c28f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1222 additions and 1355 deletions

1
Cargo.lock generated
View file

@ -2328,6 +2328,7 @@ dependencies = [
name = "graphite-desktop-wrapper"
version = "0.1.0"
dependencies = [
"base64",
"dirs",
"futures",
"graph-craft",

View file

@ -9,7 +9,7 @@ pub(crate) enum AppEvent {
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
CloseWindow,
MenuEvent { id: u64 },
MenuEvent { id: String },
}
#[derive(Clone)]

View file

@ -1,6 +1,6 @@
use muda::Menu as MudaMenu;
use muda::accelerator::Accelerator;
use muda::{CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu};
use muda::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu};
use crate::event::{AppEvent, AppEventScheduler};
use crate::wrapper::messages::MenuItem as WrapperMenuItem;
@ -30,9 +30,8 @@ impl Menu {
return;
}
if let Some(id) = menu_id_to_u64(event.id()) {
event_scheduler.schedule(AppEvent::MenuEvent { id });
}
let id = event.id().0.clone();
event_scheduler.schedule(AppEvent::MenuEvent { id });
}));
Menu { inner: menu }
@ -67,13 +66,11 @@ fn menu_items_from_wrapper(entries: Vec<WrapperMenuItem>) -> Vec<MenuItemKind> {
for entry in entries {
match entry {
WrapperMenuItem::Action { id, text, enabled, shortcut } => {
let id = u64_to_menu_id(id);
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
let item = MenuItem::with_id(id, text, enabled, accelerator);
menu_items.push(MenuItemKind::MenuItem(item));
}
WrapperMenuItem::Checkbox { id, text, enabled, shortcut, checked } => {
let id = u64_to_menu_id(id);
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
let check = CheckMenuItem::with_id(id, text, enabled, checked, accelerator);
menu_items.push(MenuItemKind::Check(check));
@ -103,14 +100,6 @@ fn menu_item_kind_to_dyn(item: &MenuItemKind) -> &dyn IsMenuItem {
}
}
fn u64_to_menu_id(id: u64) -> String {
format!("{id:08x}")
}
fn menu_id_to_u64(id: &MenuId) -> Option<u64> {
u64::from_str_radix(&id.0, 16).ok()
}
fn replace_children<'a, T: Into<MenuContainer<'a>>>(menu: T, new_items: Vec<MenuItemKind>) {
let menu: MenuContainer = menu.into();
let items = menu.items();

View file

@ -31,3 +31,4 @@ image = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
keyboard-types = { workspace = true }
base64 = { workspace = true }

View file

@ -1,14 +1,10 @@
use graphene_std::Color;
use graphene_std::raster::Image;
use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform;
use graphite_editor::messages::layout::LayoutMessage;
use graphite_editor::messages::prelude::*;
use graphite_editor::messages::tool::tool_messages::tool_prelude::{LayoutTarget, WidgetId};
use crate::messages::Platform;
use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext};
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, Platform, SaveFileDialogContext};
pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) {
match message {
@ -150,13 +146,15 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
let message = PreferencesMessage::Load { preferences };
dispatcher.queue_editor_message(message);
}
#[cfg(target_os = "macos")]
DesktopWrapperMessage::MenuEvent { id } => {
let message = LayoutMessage::WidgetValueUpdate {
layout_target: LayoutTarget::MenuBar,
widget_id: WidgetId(id),
value: serde_json::Value::Bool(true),
};
dispatcher.queue_editor_message(message);
if let Some(message) = crate::utils::menu::parse_item_path(id) {
dispatcher.queue_editor_message(message);
} else {
tracing::error!("Received a malformed MenuEvent id");
}
}
#[cfg(not(target_os = "macos"))]
DesktopWrapperMessage::MenuEvent { id: _ } => {}
}
}

View file

@ -1,12 +1,9 @@
use std::path::PathBuf;
use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LayoutKey, LayoutKeysGroup};
use graphite_editor::messages::input_mapper::utility_types::misc::ActionKeys;
use graphite_editor::messages::layout::utility_types::widgets::menu_widgets::MenuBarEntry;
use graphite_editor::messages::prelude::FrontendMessage;
use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, Document, FileFilter, KeyCode, MenuItem, Modifiers, OpenFileDialogContext, SaveFileDialogContext, Shortcut};
use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
match message {
@ -113,11 +110,24 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
FrontendMessage::TriggerLoadPreferences => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
}
FrontendMessage::UpdateMenuBarLayout { layout_target, layout } => {
let entries = convert_menu_bar_entries_to_menu_items(&layout);
dispatcher.respond(DesktopFrontendMessage::UpdateMenu { entries });
return Some(FrontendMessage::UpdateMenuBarLayout { layout, layout_target });
#[cfg(target_os = "macos")]
FrontendMessage::UpdateMenuBarLayout {
layout_target: graphite_editor::messages::tool::tool_messages::tool_prelude::LayoutTarget::MenuBar,
diff,
} => {
use graphite_editor::messages::tool::tool_messages::tool_prelude::{DiffUpdate, WidgetDiff};
match diff.as_slice() {
[
WidgetDiff {
widget_path,
new_value: DiffUpdate::SubLayout(layout),
},
] if widget_path.is_empty() => {
let entries = crate::utils::menu::convert_menu_bar_layout_to_menu_items(layout);
dispatcher.respond(DesktopFrontendMessage::UpdateMenu { entries });
}
_ => {}
}
}
FrontendMessage::WindowClose => {
dispatcher.respond(DesktopFrontendMessage::WindowClose);
@ -144,191 +154,3 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
}
None
}
fn convert_menu_bar_entries_to_menu_items(layout: &[MenuBarEntry]) -> Vec<MenuItem> {
layout.iter().filter_map(convert_menu_bar_entry_to_menu_item).collect()
}
fn convert_menu_bar_entry_to_menu_item(
MenuBarEntry {
label,
icon,
shortcut,
action,
children,
disabled,
}: &MenuBarEntry,
) -> Option<MenuItem> {
let id = action.widget_id.0;
let text = label.clone();
let enabled = !*disabled;
if !children.0.is_empty() {
let items = convert_menu_bar_entry_children_to_menu_items(&children.0);
return Some(MenuItem::SubMenu { id, text, enabled, items });
}
let shortcut = match shortcut {
Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => convert_layout_keys_to_shortcut(keys),
_ => None,
};
// TODO: Find a better way to determine if this is a checkbox
match icon.as_deref() {
Some("CheckboxChecked") => {
return Some(MenuItem::Checkbox {
id,
text,
enabled,
shortcut,
checked: true,
});
}
Some("CheckboxUnchecked") => {
return Some(MenuItem::Checkbox {
id,
text,
enabled,
shortcut,
checked: false,
});
}
_ => {}
}
Some(MenuItem::Action { id, text, shortcut, enabled })
}
fn convert_menu_bar_entry_children_to_menu_items(children: &[Vec<MenuBarEntry>]) -> Vec<MenuItem> {
let mut items = Vec::new();
for (i, section) in children.iter().enumerate() {
for entry in section.iter() {
if let Some(item) = convert_menu_bar_entry_to_menu_item(entry) {
items.push(item);
}
}
if i != children.len() - 1 {
items.push(MenuItem::Separator);
}
}
items
}
fn convert_layout_keys_to_shortcut(layout_keys: &Vec<LayoutKey>) -> Option<Shortcut> {
let mut key: Option<KeyCode> = None;
let mut modifiers = Modifiers::default();
for layout_key in layout_keys {
match layout_key.key() {
Key::Shift => modifiers |= Modifiers::SHIFT,
Key::Control => modifiers |= Modifiers::CONTROL,
Key::Alt => modifiers |= Modifiers::ALT,
Key::Meta => modifiers |= Modifiers::META,
Key::Command => modifiers |= Modifiers::ALT,
Key::Accel => modifiers |= Modifiers::META,
Key::Digit0 => key = Some(KeyCode::Digit0),
Key::Digit1 => key = Some(KeyCode::Digit1),
Key::Digit2 => key = Some(KeyCode::Digit2),
Key::Digit3 => key = Some(KeyCode::Digit3),
Key::Digit4 => key = Some(KeyCode::Digit4),
Key::Digit5 => key = Some(KeyCode::Digit5),
Key::Digit6 => key = Some(KeyCode::Digit6),
Key::Digit7 => key = Some(KeyCode::Digit7),
Key::Digit8 => key = Some(KeyCode::Digit8),
Key::Digit9 => key = Some(KeyCode::Digit9),
Key::KeyA => key = Some(KeyCode::KeyA),
Key::KeyB => key = Some(KeyCode::KeyB),
Key::KeyC => key = Some(KeyCode::KeyC),
Key::KeyD => key = Some(KeyCode::KeyD),
Key::KeyE => key = Some(KeyCode::KeyE),
Key::KeyF => key = Some(KeyCode::KeyF),
Key::KeyG => key = Some(KeyCode::KeyG),
Key::KeyH => key = Some(KeyCode::KeyH),
Key::KeyI => key = Some(KeyCode::KeyI),
Key::KeyJ => key = Some(KeyCode::KeyJ),
Key::KeyK => key = Some(KeyCode::KeyK),
Key::KeyL => key = Some(KeyCode::KeyL),
Key::KeyM => key = Some(KeyCode::KeyM),
Key::KeyN => key = Some(KeyCode::KeyN),
Key::KeyO => key = Some(KeyCode::KeyO),
Key::KeyP => key = Some(KeyCode::KeyP),
Key::KeyQ => key = Some(KeyCode::KeyQ),
Key::KeyR => key = Some(KeyCode::KeyR),
Key::KeyS => key = Some(KeyCode::KeyS),
Key::KeyT => key = Some(KeyCode::KeyT),
Key::KeyU => key = Some(KeyCode::KeyU),
Key::KeyV => key = Some(KeyCode::KeyV),
Key::KeyW => key = Some(KeyCode::KeyW),
Key::KeyX => key = Some(KeyCode::KeyX),
Key::KeyY => key = Some(KeyCode::KeyY),
Key::KeyZ => key = Some(KeyCode::KeyZ),
Key::Backquote => key = Some(KeyCode::Backquote),
Key::Backslash => key = Some(KeyCode::Backslash),
Key::BracketLeft => key = Some(KeyCode::BracketLeft),
Key::BracketRight => key = Some(KeyCode::BracketRight),
Key::Comma => key = Some(KeyCode::Comma),
Key::Equal => key = Some(KeyCode::Equal),
Key::Minus => key = Some(KeyCode::Minus),
Key::Period => key = Some(KeyCode::Period),
Key::Quote => key = Some(KeyCode::Quote),
Key::Semicolon => key = Some(KeyCode::Semicolon),
Key::Slash => key = Some(KeyCode::Slash),
Key::Backspace => key = Some(KeyCode::Backspace),
Key::CapsLock => key = Some(KeyCode::CapsLock),
Key::ContextMenu => key = Some(KeyCode::ContextMenu),
Key::Enter => key = Some(KeyCode::Enter),
Key::Space => key = Some(KeyCode::Space),
Key::Tab => key = Some(KeyCode::Tab),
Key::Delete => key = Some(KeyCode::Delete),
Key::End => key = Some(KeyCode::End),
Key::Help => key = Some(KeyCode::Help),
Key::Home => key = Some(KeyCode::Home),
Key::Insert => key = Some(KeyCode::Insert),
Key::PageDown => key = Some(KeyCode::PageDown),
Key::PageUp => key = Some(KeyCode::PageUp),
Key::ArrowDown => key = Some(KeyCode::ArrowDown),
Key::ArrowLeft => key = Some(KeyCode::ArrowLeft),
Key::ArrowRight => key = Some(KeyCode::ArrowRight),
Key::ArrowUp => key = Some(KeyCode::ArrowUp),
Key::NumLock => key = Some(KeyCode::NumLock),
Key::NumpadAdd => key = Some(KeyCode::NumpadAdd),
Key::NumpadHash => key = Some(KeyCode::NumpadHash),
Key::NumpadMultiply => key = Some(KeyCode::NumpadMultiply),
Key::NumpadParenLeft => key = Some(KeyCode::NumpadParenLeft),
Key::NumpadParenRight => key = Some(KeyCode::NumpadParenRight),
Key::Escape => key = Some(KeyCode::Escape),
Key::F1 => key = Some(KeyCode::F1),
Key::F2 => key = Some(KeyCode::F2),
Key::F3 => key = Some(KeyCode::F3),
Key::F4 => key = Some(KeyCode::F4),
Key::F5 => key = Some(KeyCode::F5),
Key::F6 => key = Some(KeyCode::F6),
Key::F7 => key = Some(KeyCode::F7),
Key::F8 => key = Some(KeyCode::F8),
Key::F9 => key = Some(KeyCode::F9),
Key::F10 => key = Some(KeyCode::F10),
Key::F11 => key = Some(KeyCode::F11),
Key::F12 => key = Some(KeyCode::F12),
Key::F13 => key = Some(KeyCode::F13),
Key::F14 => key = Some(KeyCode::F14),
Key::F15 => key = Some(KeyCode::F15),
Key::F16 => key = Some(KeyCode::F16),
Key::F17 => key = Some(KeyCode::F17),
Key::F18 => key = Some(KeyCode::F18),
Key::F19 => key = Some(KeyCode::F19),
Key::F20 => key = Some(KeyCode::F20),
Key::F21 => key = Some(KeyCode::F21),
Key::F22 => key = Some(KeyCode::F22),
Key::F23 => key = Some(KeyCode::F23),
Key::F24 => key = Some(KeyCode::F24),
Key::Fn => key = Some(KeyCode::Fn),
Key::FnLock => key = Some(KeyCode::FnLock),
Key::PrintScreen => key = Some(KeyCode::PrintScreen),
Key::ScrollLock => key = Some(KeyCode::ScrollLock),
Key::Pause => key = Some(KeyCode::Pause),
Key::Unidentified => key = Some(KeyCode::Unidentified),
Key::FakeKeyPlus => key = Some(KeyCode::Equal),
_ => key = None,
}
}
key.map(|key| Shortcut { key, modifiers })
}

View file

@ -20,6 +20,8 @@ mod handle_desktop_wrapper_message;
mod intercept_editor_message;
mod intercept_frontend_message;
pub(crate) mod utils;
pub struct DesktopWrapper {
editor: Editor,
}

View file

@ -112,7 +112,7 @@ pub enum DesktopWrapperMessage {
preferences: Option<Preferences>,
},
MenuEvent {
id: u64,
id: String,
},
}
@ -147,20 +147,20 @@ pub enum Platform {
pub enum MenuItem {
Action {
id: u64,
id: String,
text: String,
enabled: bool,
shortcut: Option<Shortcut>,
},
Checkbox {
id: u64,
id: String,
text: String,
enabled: bool,
shortcut: Option<Shortcut>,
checked: bool,
},
SubMenu {
id: u64,
id: String,
text: String,
enabled: bool,
items: Vec<MenuItem>,

View file

@ -0,0 +1,247 @@
#[cfg(target_os = "macos")]
pub(crate) mod menu {
use base64::engine::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LayoutKey, LayoutKeysGroup};
use graphite_editor::messages::input_mapper::utility_types::misc::ActionKeys;
use graphite_editor::messages::layout::LayoutMessage;
use graphite_editor::messages::tool::tool_messages::tool_prelude::{LayoutGroup, LayoutTarget, MenuListEntry, SubLayout, Widget, WidgetId};
use crate::messages::{EditorMessage, KeyCode, MenuItem, Modifiers, Shortcut};
pub(crate) fn convert_menu_bar_layout_to_menu_items(layout: &SubLayout) -> Vec<MenuItem> {
let layout_group = match layout.as_slice() {
[layout_group] => layout_group,
_ => panic!("Menu bar layout is supposed to have exactly one layout group"),
};
let LayoutGroup::Row { widgets } = layout_group else {
panic!("Menu bar layout group is supposed to be a row");
};
widgets
.into_iter()
.map(|widget| {
let text_button = match &widget.widget {
Widget::TextButton(text_button) => text_button,
_ => panic!("Menu bar layout top-level widgets are supposed to be text buttons"),
};
MenuItem::SubMenu {
id: widget.widget_id.to_string(),
text: text_button.label.clone(),
enabled: !text_button.disabled,
items: convert_menu_bar_entry_children_to_menu_items(&text_button.menu_list_children, widget.widget_id.0, Vec::new()),
}
})
.collect::<Vec<MenuItem>>()
}
pub(crate) fn parse_item_path(id: String) -> Option<EditorMessage> {
let mut id_parts = id.split(':');
let widget_id = id_parts.next()?.parse::<u64>().ok()?;
let value = id_parts
.map(|part| {
let bytes = BASE64.decode(part).ok()?;
String::from_utf8(bytes).ok()
})
.collect::<Option<Vec<String>>>()?;
let value = serde_json::to_value(value).ok()?;
Some(
LayoutMessage::WidgetValueUpdate {
layout_target: LayoutTarget::MenuBar,
widget_id: WidgetId(widget_id),
value,
}
.into(),
)
}
fn item_path_to_string(widget_id: u64, path: Vec<String>) -> String {
let path = path.into_iter().map(|element| BASE64.encode(element)).collect::<Vec<_>>().join(":");
format!("{widget_id}:{path}")
}
fn convert_menu_bar_layout_to_menu_item(entry: &MenuListEntry, root_widget_id: u64, mut path: Vec<String>) -> MenuItem {
let MenuListEntry {
value,
label,
icon,
shortcut_keys,
children,
disabled,
..
}: &MenuListEntry = entry;
path.push(value.clone());
let id = item_path_to_string(root_widget_id, path.clone());
let text = label.clone();
let enabled = !*disabled;
if !children.is_empty() {
let items = convert_menu_bar_entry_children_to_menu_items(&children, root_widget_id, path.clone());
return MenuItem::SubMenu { id, text, enabled, items };
}
let shortcut = match shortcut_keys {
Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => convert_layout_keys_to_shortcut(keys),
_ => None,
};
match icon.as_str() {
"CheckboxChecked" => {
return MenuItem::Checkbox {
id,
text,
enabled,
shortcut,
checked: true,
};
}
"CheckboxUnchecked" => {
return MenuItem::Checkbox {
id,
text,
enabled,
shortcut,
checked: false,
};
}
_ => {}
}
MenuItem::Action { id, text, shortcut, enabled }
}
fn convert_menu_bar_entry_children_to_menu_items(children: &[Vec<MenuListEntry>], root_widget_id: u64, path: Vec<String>) -> Vec<MenuItem> {
let mut items = Vec::new();
for (i, section) in children.iter().enumerate() {
for entry in section.iter() {
items.push(convert_menu_bar_layout_to_menu_item(entry, root_widget_id, path.clone()));
}
if i != children.len() - 1 {
items.push(MenuItem::Separator);
}
}
items
}
fn convert_layout_keys_to_shortcut(layout_keys: &Vec<LayoutKey>) -> Option<Shortcut> {
let mut key: Option<KeyCode> = None;
let mut modifiers = Modifiers::default();
for layout_key in layout_keys {
match layout_key.key() {
Key::Shift => modifiers |= Modifiers::SHIFT,
Key::Control => modifiers |= Modifiers::CONTROL,
Key::Alt => modifiers |= Modifiers::ALT,
Key::Meta => modifiers |= Modifiers::META,
Key::Command => modifiers |= Modifiers::ALT,
Key::Accel => modifiers |= Modifiers::META,
Key::Digit0 => key = Some(KeyCode::Digit0),
Key::Digit1 => key = Some(KeyCode::Digit1),
Key::Digit2 => key = Some(KeyCode::Digit2),
Key::Digit3 => key = Some(KeyCode::Digit3),
Key::Digit4 => key = Some(KeyCode::Digit4),
Key::Digit5 => key = Some(KeyCode::Digit5),
Key::Digit6 => key = Some(KeyCode::Digit6),
Key::Digit7 => key = Some(KeyCode::Digit7),
Key::Digit8 => key = Some(KeyCode::Digit8),
Key::Digit9 => key = Some(KeyCode::Digit9),
Key::KeyA => key = Some(KeyCode::KeyA),
Key::KeyB => key = Some(KeyCode::KeyB),
Key::KeyC => key = Some(KeyCode::KeyC),
Key::KeyD => key = Some(KeyCode::KeyD),
Key::KeyE => key = Some(KeyCode::KeyE),
Key::KeyF => key = Some(KeyCode::KeyF),
Key::KeyG => key = Some(KeyCode::KeyG),
Key::KeyH => key = Some(KeyCode::KeyH),
Key::KeyI => key = Some(KeyCode::KeyI),
Key::KeyJ => key = Some(KeyCode::KeyJ),
Key::KeyK => key = Some(KeyCode::KeyK),
Key::KeyL => key = Some(KeyCode::KeyL),
Key::KeyM => key = Some(KeyCode::KeyM),
Key::KeyN => key = Some(KeyCode::KeyN),
Key::KeyO => key = Some(KeyCode::KeyO),
Key::KeyP => key = Some(KeyCode::KeyP),
Key::KeyQ => key = Some(KeyCode::KeyQ),
Key::KeyR => key = Some(KeyCode::KeyR),
Key::KeyS => key = Some(KeyCode::KeyS),
Key::KeyT => key = Some(KeyCode::KeyT),
Key::KeyU => key = Some(KeyCode::KeyU),
Key::KeyV => key = Some(KeyCode::KeyV),
Key::KeyW => key = Some(KeyCode::KeyW),
Key::KeyX => key = Some(KeyCode::KeyX),
Key::KeyY => key = Some(KeyCode::KeyY),
Key::KeyZ => key = Some(KeyCode::KeyZ),
Key::Backquote => key = Some(KeyCode::Backquote),
Key::Backslash => key = Some(KeyCode::Backslash),
Key::BracketLeft => key = Some(KeyCode::BracketLeft),
Key::BracketRight => key = Some(KeyCode::BracketRight),
Key::Comma => key = Some(KeyCode::Comma),
Key::Equal => key = Some(KeyCode::Equal),
Key::Minus => key = Some(KeyCode::Minus),
Key::Period => key = Some(KeyCode::Period),
Key::Quote => key = Some(KeyCode::Quote),
Key::Semicolon => key = Some(KeyCode::Semicolon),
Key::Slash => key = Some(KeyCode::Slash),
Key::Backspace => key = Some(KeyCode::Backspace),
Key::CapsLock => key = Some(KeyCode::CapsLock),
Key::ContextMenu => key = Some(KeyCode::ContextMenu),
Key::Enter => key = Some(KeyCode::Enter),
Key::Space => key = Some(KeyCode::Space),
Key::Tab => key = Some(KeyCode::Tab),
Key::Delete => key = Some(KeyCode::Delete),
Key::End => key = Some(KeyCode::End),
Key::Help => key = Some(KeyCode::Help),
Key::Home => key = Some(KeyCode::Home),
Key::Insert => key = Some(KeyCode::Insert),
Key::PageDown => key = Some(KeyCode::PageDown),
Key::PageUp => key = Some(KeyCode::PageUp),
Key::ArrowDown => key = Some(KeyCode::ArrowDown),
Key::ArrowLeft => key = Some(KeyCode::ArrowLeft),
Key::ArrowRight => key = Some(KeyCode::ArrowRight),
Key::ArrowUp => key = Some(KeyCode::ArrowUp),
Key::NumLock => key = Some(KeyCode::NumLock),
Key::NumpadAdd => key = Some(KeyCode::NumpadAdd),
Key::NumpadHash => key = Some(KeyCode::NumpadHash),
Key::NumpadMultiply => key = Some(KeyCode::NumpadMultiply),
Key::NumpadParenLeft => key = Some(KeyCode::NumpadParenLeft),
Key::NumpadParenRight => key = Some(KeyCode::NumpadParenRight),
Key::Escape => key = Some(KeyCode::Escape),
Key::F1 => key = Some(KeyCode::F1),
Key::F2 => key = Some(KeyCode::F2),
Key::F3 => key = Some(KeyCode::F3),
Key::F4 => key = Some(KeyCode::F4),
Key::F5 => key = Some(KeyCode::F5),
Key::F6 => key = Some(KeyCode::F6),
Key::F7 => key = Some(KeyCode::F7),
Key::F8 => key = Some(KeyCode::F8),
Key::F9 => key = Some(KeyCode::F9),
Key::F10 => key = Some(KeyCode::F10),
Key::F11 => key = Some(KeyCode::F11),
Key::F12 => key = Some(KeyCode::F12),
Key::F13 => key = Some(KeyCode::F13),
Key::F14 => key = Some(KeyCode::F14),
Key::F15 => key = Some(KeyCode::F15),
Key::F16 => key = Some(KeyCode::F16),
Key::F17 => key = Some(KeyCode::F17),
Key::F18 => key = Some(KeyCode::F18),
Key::F19 => key = Some(KeyCode::F19),
Key::F20 => key = Some(KeyCode::F20),
Key::F21 => key = Some(KeyCode::F21),
Key::F22 => key = Some(KeyCode::F22),
Key::F23 => key = Some(KeyCode::F23),
Key::F24 => key = Some(KeyCode::F24),
Key::Fn => key = Some(KeyCode::Fn),
Key::FnLock => key = Some(KeyCode::FnLock),
Key::PrintScreen => key = Some(KeyCode::PrintScreen),
Key::ScrollLock => key = Some(KeyCode::ScrollLock),
Key::Pause => key = Some(KeyCode::Pause),
Key::Unidentified => key = Some(KeyCode::Unidentified),
Key::FakeKeyPlus => key = Some(KeyCode::Equal),
_ => key = None,
}
}
key.map(|key| Shortcut { key, modifiers })
}
}

View file

@ -279,7 +279,7 @@ pub enum FrontendMessage {
UpdateMenuBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: Vec<MenuBarEntry>,
diff: Vec<WidgetDiff>,
},
UpdateMouseCursor {
cursor: MouseCursorIcon,

View file

@ -25,7 +25,6 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
LayoutMessage::ResendActiveWidget { layout_target, widget_id } => {
// Find the updated diff based on the specified layout target
let Some(diff) = (match &self.layouts[layout_target as usize] {
Layout::MenuLayout(_) => return,
Layout::WidgetLayout(layout) => Self::get_widget_path(layout, widget_id).map(|(widget, widget_path)| {
// Create a widget update diff for the relevant id
let new_value = DiffUpdate::Widget(widget.clone());
@ -112,7 +111,10 @@ impl LayoutMessageHandler {
return;
};
let Some(widget_holder) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) else {
let mut layout_iter = match layout {
Layout::WidgetLayout(widget_layout) => widget_layout.iter_mut(),
};
let Some(widget_holder) = layout_iter.find(|widget| widget.widget_id == widget_id) else {
warn!("handle_widget_callback was called referencing an invalid widget ID, although the layout target was valid. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",);
return;
};
@ -309,14 +311,6 @@ impl LayoutMessageHandler {
}
Widget::ImageLabel(_) => {}
Widget::IconLabel(_) => {}
Widget::InvisibleStandinInput(invisible) => {
let callback_message = match action {
WidgetValueAction::Commit => (invisible.on_commit.callback)(&()),
WidgetValueAction::Update => (invisible.on_update.callback)(&()),
};
responses.add(callback_message);
}
Widget::NodeCatalog(node_type_input) => match action {
WidgetValueAction::Commit => {
let callback_message = (node_type_input.on_commit.callback)(&());
@ -411,7 +405,34 @@ impl LayoutMessageHandler {
Widget::TextButton(text_button) => {
let callback_message = match action {
WidgetValueAction::Commit => (text_button.on_commit.callback)(&()),
WidgetValueAction::Update => (text_button.on_update.callback)(text_button),
WidgetValueAction::Update => {
let Some(value_path) = value.as_array() else {
error!("TextButton update was not of type: array");
return;
};
// Process the text button click, since no menu is involved if we're given an empty array.
if value_path.is_empty() {
(text_button.on_update.callback)(text_button)
}
// Process the text button's menu list entry click, since we have a path to the value of the contained menu entry.
else {
let mut current_submenu = &text_button.menu_list_children;
let mut final_entry: Option<&MenuListEntry> = None;
// Loop through all menu entry value strings in the path until we reach the final entry (which we store).
// Otherwise we exit early if we can't traverse the full path.
for value in value_path.iter().filter_map(|v| v.as_str().map(|s| s.to_string())) {
let Some(next_entry) = current_submenu.iter().flatten().find(|e| e.value == value) else { return };
current_submenu = &next_entry.children;
final_entry = Some(next_entry);
}
// If we've reached here without returning early, we have a final entry in the path and we should now execute its callback.
(final_entry.unwrap().on_commit.callback)(&())
}
}
};
responses.add(callback_message);
@ -447,31 +468,26 @@ impl LayoutMessageHandler {
match new_layout {
Layout::WidgetLayout(_) => {
let mut widget_diffs = Vec::new();
self.layouts[layout_target as usize].diff(new_layout, &mut Vec::new(), &mut widget_diffs);
// Skip sending if no diff.
let Layout::WidgetLayout(current) = &mut self.layouts[layout_target as usize];
let Layout::WidgetLayout(new) = new_layout;
current.diff(new, &mut Vec::new(), &mut widget_diffs);
// Skip sending if no diff
if widget_diffs.is_empty() {
return;
}
self.send_diff(widget_diffs, layout_target, responses, action_input_mapping);
}
// We don't diff the menu bar layout yet.
Layout::MenuLayout(_) => {
// Skip update if the same
if self.layouts[layout_target as usize] == new_layout {
return;
// On Mac we need the full MenuBar layout to construct the native menu
#[cfg(target_os = "macos")]
if layout_target == LayoutTarget::MenuBar {
widget_diffs = vec![WidgetDiff {
widget_path: Vec::new(),
new_value: DiffUpdate::SubLayout(current.layout.clone()),
}];
}
// Update the backend storage
self.layouts[layout_target as usize] = new_layout;
// Update the UI
let Some(layout) = self.layouts[layout_target as usize].clone().as_menu_layout(action_input_mapping).map(|x| x.layout) else {
error!("Called unwrap_menu_layout on a widget layout");
return;
};
responses.add(FrontendMessage::UpdateMenuBarLayout { layout_target, layout });
self.send_diff(widget_diffs, layout_target, responses, action_input_mapping);
}
}
}
@ -481,24 +497,25 @@ impl LayoutMessageHandler {
diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping));
let message = match layout_target {
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff },
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff },
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff },
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff },
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff },
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff },
LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff },
LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout { layout_target, diff },
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff },
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
};
responses.add(message);
}
}

View file

@ -1,7 +1,6 @@
use super::widgets::button_widgets::*;
use super::widgets::input_widgets::*;
use super::widgets::label_widgets::*;
use super::widgets::menu_widgets::MenuLayout;
use crate::application::generate_uuid;
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
@ -101,56 +100,11 @@ pub trait DialogLayoutHolder: LayoutHolder {
}
}
// TODO: Unwrap this enum
/// Wraps a choice of layout type. The chosen layout contains an arrangement of widgets mounted somewhere specific in the frontend.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum Layout {
WidgetLayout(WidgetLayout),
MenuLayout(MenuLayout),
}
impl Layout {
pub fn as_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) -> Option<MenuLayout> {
if let Self::MenuLayout(mut menu) = self {
menu.layout
.iter_mut()
.for_each(|menu_column| menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping));
Some(menu)
} else {
None
}
}
pub fn iter(&self) -> Box<dyn Iterator<Item = &WidgetHolder> + '_> {
match self {
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter()),
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter()),
}
}
pub fn iter_mut(&mut self) -> Box<dyn Iterator<Item = &mut WidgetHolder> + '_> {
match self {
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter_mut()),
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter_mut()),
}
}
/// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so.
pub fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
match (self, new) {
// Simply diff the internal layout
(Self::WidgetLayout(current), Self::WidgetLayout(new)) => current.diff(new, widget_path, widget_diffs),
(current, Self::WidgetLayout(widget_layout)) => {
// Update current to the new value
*current = Self::WidgetLayout(widget_layout.clone());
// Push an update sublayout value
let new_value = DiffUpdate::SubLayout(widget_layout.layout);
let widget_path = widget_path.to_vec();
widget_diffs.push(WidgetDiff { widget_path, new_value });
}
(_, Self::MenuLayout(_)) => panic!("Cannot diff menu layout"),
}
}
}
impl Default for Layout {
@ -159,6 +113,7 @@ impl Default for Layout {
}
}
// TODO: Unwrap this struct
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)]
pub struct WidgetLayout {
pub layout: SubLayout,
@ -327,7 +282,6 @@ pub enum LayoutGroup {
#[serde(rename = "tableWidgets")]
rows: Vec<Vec<WidgetHolder>>,
},
// TODO: Move this from being a child of `enum LayoutGroup` to being a child of `enum Layout`
#[serde(rename = "section")]
Section {
name: String,
@ -378,7 +332,7 @@ impl LayoutGroup {
Widget::TextInput(x) => &mut x.tooltip_label,
Widget::TextLabel(x) => &mut x.tooltip_label,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_label,
Widget::InvisibleStandinInput(_) | Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
};
if val.is_empty() {
val.clone_from(&label);
@ -414,7 +368,7 @@ impl LayoutGroup {
Widget::TextInput(x) => &mut x.tooltip_description,
Widget::TextLabel(x) => &mut x.tooltip_description,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_description,
Widget::InvisibleStandinInput(_) | Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
};
if val.is_empty() {
val.clone_from(&description);
@ -520,6 +474,7 @@ impl LayoutGroup {
}
}
// TODO: Rename to WidgetInstance
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WidgetHolder {
#[serde(rename = "widgetId")]
@ -609,7 +564,6 @@ pub enum Widget {
IconLabel(IconLabel),
ImageButton(ImageButton),
ImageLabel(ImageLabel),
InvisibleStandinInput(InvisibleStandinInput),
NodeCatalog(NodeCatalog),
NumberInput(NumberInput),
ParameterExposeButton(ParameterExposeButton),
@ -680,7 +634,6 @@ impl DiffUpdate {
Widget::IconLabel(_)
| Widget::ImageLabel(_)
| Widget::CurveInput(_)
| Widget::InvisibleStandinInput(_)
| Widget::NodeCatalog(_)
| Widget::ReferencePointInput(_)
| Widget::RadioInput(_)
@ -709,10 +662,45 @@ impl DiffUpdate {
}
};
// Recursively fill menu list entries with their realized shortcut keys specific to the current bindings and platform
let apply_action_keys_to_menu_lists = |entry_sections: &mut MenuListEntrySections| {
struct RecursiveWrapper<'a>(&'a dyn Fn(&mut MenuListEntrySections, &RecursiveWrapper));
let recursive_wrapper = RecursiveWrapper(&|entry_sections: &mut MenuListEntrySections, recursive_wrapper| {
for entries in entry_sections {
for entry in entries {
// Convert the shortcut actions to keys for this menu entry
if let Some(shortcut_keys) = &mut entry.shortcut_keys {
shortcut_keys.to_keys(action_input_mapping);
}
// Recursively call this inner closure on the menu's children
(recursive_wrapper.0)(&mut entry.children, recursive_wrapper);
}
}
});
(recursive_wrapper.0)(entry_sections, &recursive_wrapper)
};
// Apply shortcut conversions to all widgets that have menu lists
let convert_menu_lists = |widget_holder: &mut WidgetHolder| match &mut widget_holder.widget {
Widget::DropdownInput(dropdown_input) => apply_action_keys_to_menu_lists(&mut dropdown_input.entries),
Widget::TextButton(text_button) => apply_action_keys_to_menu_lists(&mut text_button.menu_list_children),
_ => {}
};
match self {
Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(convert_tooltip),
Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(convert_tooltip),
Self::Widget(widget_holder) => convert_tooltip(widget_holder),
Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(|widget_holder| {
convert_tooltip(widget_holder);
convert_menu_lists(widget_holder);
}),
Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(|widget_holder| {
convert_tooltip(widget_holder);
convert_menu_lists(widget_holder);
}),
Self::Widget(widget_holder) => {
convert_tooltip(widget_holder);
convert_menu_lists(widget_holder);
}
}
}
}

View file

@ -6,5 +6,4 @@ pub mod widget_prelude {
pub use super::widgets::button_widgets::*;
pub use super::widgets::input_widgets::*;
pub use super::widgets::label_widgets::*;
pub use super::widgets::menu_widgets::*;
}

View file

@ -133,15 +133,28 @@ pub struct MenuListEntry {
pub label: String,
pub font: String,
pub icon: String,
pub shortcut: Vec<String>,
pub disabled: bool,
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: String,
// TODO: Make this serde(skip)
#[serde(rename = "shortcutKeys")]
pub shortcut_keys: Option<ActionKeys>,
#[serde(rename = "shortcutRequiresLock")]
pub shortcut_requires_lock: bool,
pub disabled: bool,
pub children: MenuListEntrySections,
// Callbacks
@ -192,21 +205,6 @@ pub struct FontInput {
pub on_commit: WidgetCallback<()>,
}
/// This widget allows for the flexible use of the layout system.
/// In a custom layout, one can define a widget that is just used to trigger code on the backend.
/// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend.
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct InvisibleStandinInput {
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<()>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct NumberInput {

View file

@ -1,131 +0,0 @@
use super::input_widgets::InvisibleStandinInput;
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::widget_prelude::*;
use crate::messages::prelude::*;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Default, specta::Type)]
pub struct MenuBarEntryChildren(pub Vec<Vec<MenuBarEntry>>);
impl MenuBarEntryChildren {
pub fn empty() -> Self {
Self(Vec::new())
}
pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
let entries = self.0.iter_mut().flatten();
for entry in entries {
if let Some(action_keys) = &mut entry.shortcut {
action_keys.to_keys(action_input_mapping);
}
// Recursively do this for the children also
entry.children.fill_in_shortcut_actions_with_keys(action_input_mapping);
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)]
pub struct MenuBarEntry {
pub label: String,
pub icon: Option<String>,
pub shortcut: Option<ActionKeys>,
pub action: WidgetHolder,
pub children: MenuBarEntryChildren,
pub disabled: bool,
}
impl MenuBarEntry {
pub fn new_root(label: String, disabled: bool, children: MenuBarEntryChildren) -> Self {
Self {
label,
disabled,
children,
..Default::default()
}
}
pub fn create_action(callback: impl Fn(&()) -> Message + 'static + Send + Sync) -> WidgetHolder {
InvisibleStandinInput::new().on_update(callback).widget_holder()
}
pub fn no_action() -> WidgetHolder {
MenuBarEntry::create_action(|_| Message::NoOp)
}
}
impl Default for MenuBarEntry {
fn default() -> Self {
Self {
label: "".into(),
icon: None,
shortcut: None,
action: MenuBarEntry::no_action(),
children: MenuBarEntryChildren::empty(),
disabled: false,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct MenuLayout {
pub layout: Vec<MenuBarEntry>,
}
impl MenuLayout {
pub fn new(layout: Vec<MenuBarEntry>) -> Self {
Self { layout }
}
pub fn iter(&self) -> impl Iterator<Item = &WidgetHolder> + '_ {
MenuLayoutIter { stack: self.layout.iter().collect() }
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut WidgetHolder> + '_ {
MenuLayoutIterMut {
stack: self.layout.iter_mut().collect(),
}
}
}
#[derive(Debug, Default)]
pub struct MenuLayoutIter<'a> {
pub stack: Vec<&'a MenuBarEntry>,
}
impl<'a> Iterator for MenuLayoutIter<'a> {
type Item = &'a WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
Some(menu_entry) => {
let more_entries = menu_entry.children.0.iter().flat_map(|entry| entry.iter());
self.stack.extend(more_entries);
Some(&menu_entry.action)
}
None => None,
}
}
}
pub struct MenuLayoutIterMut<'a> {
pub stack: Vec<&'a mut MenuBarEntry>,
}
impl<'a> Iterator for MenuLayoutIterMut<'a> {
type Item = &'a mut WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
Some(menu_entry) => {
let more_entries = menu_entry.children.0.iter_mut().flat_map(|entry| entry.iter_mut());
self.stack.extend(more_entries);
Some(&mut menu_entry.action)
}
None => None,
}
}
}

View file

@ -1,4 +1,3 @@
pub mod button_widgets;
pub mod input_widgets;
pub mod label_widgets;
pub mod menu_widgets;

View file

@ -157,7 +157,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
description: Cow::Borrowed(
"Improves rendering performance if used in rare circumstances where automatic caching is not yet advanced enough to handle the situation.
"Improves rendering performance if used in rare circumstances where automatic caching is not yet advanced enough to handle the situation.\n\
\n\
Stores the last evaluated data that flowed through this node, and immediately returns that data on subsequent renders if the context has not changed.",
),
properties: None,
@ -1014,7 +1015,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Loads an image from a given URL"),
description: Cow::Borrowed("Loads an image from a given URL."),
properties: None,
},
#[cfg(all(feature = "gpu", target_family = "wasm"))]

View file

@ -2112,7 +2112,7 @@ pub mod choice {
}
/// Not yet implemented!
pub fn into_menu_entries(self, _action: impl Fn(E) -> Message + 'static + Send + Sync) -> Vec<Vec<MenuBarEntry>> {
pub fn into_menu_entries(self, _action: impl Fn(E) -> Message + 'static + Send + Sync) -> MenuListEntrySections {
todo!()
}

View file

@ -19,8 +19,16 @@
let scroller: LayoutCol | undefined;
let searchTextInput: TextInput | undefined;
const dispatch = createEventDispatcher<{ open: boolean; activeEntry: MenuListEntry; hoverInEntry: MenuListEntry; hoverOutEntry: undefined; naturalWidth: number }>();
const dispatch = createEventDispatcher<{
open: boolean;
activeEntry: MenuListEntry;
selectedEntryValuePath: string[];
hoverInEntry: MenuListEntry;
hoverOutEntry: undefined;
naturalWidth: number;
}>();
export let parentsValuePath: string[] = [];
export let entries: MenuListEntry[][];
export let activeEntry: MenuListEntry | undefined = undefined;
export let open: boolean;
@ -30,9 +38,6 @@
export let interactive = false;
export let scrollableY = false;
export let virtualScrollingEntryHeight = 0;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Keep the child references outside of the entries array so as to avoid infinite recursion.
let childReferences: MenuList[][] = [];
@ -149,11 +154,9 @@
}
function onEntryClick(menuListEntry: MenuListEntry) {
// Call the action if available
if (menuListEntry.action) menuListEntry.action();
// Notify the parent about the clicked entry as the new active entry
dispatch("activeEntry", menuListEntry);
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
// Close the containing menu
let childReference = getChildReference(menuListEntry);
@ -425,9 +428,9 @@
class="row"
classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
styles={{ height: virtualScrollingEntryHeight || "20px" }}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
tooltipLabel={entry.tooltipLabel}
tooltipDescription={entry.tooltipDescription}
tooltipShortcut={entry.tooltipShortcut}
on:click={() => !entry.disabled && onEntryClick(entry)}
on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)}
on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)}
@ -444,8 +447,8 @@
<TextLabel class="entry-label" styles={{ "font-family": `${!entry.font ? "inherit" : entry.value}` }}>{entry.label}</TextLabel>
{#if entry.shortcut?.keys.length}
<UserInputLabel keysWithLabelsGroups={[entry.shortcut.keys]} requiresLock={entry.shortcutRequiresLock} textOnly={true} />
{#if entry.shortcutKeys?.keys.length}
<UserInputLabel keysWithLabelsGroups={[entry.shortcutKeys.keys]} requiresLock={entry.shortcutRequiresLock} textOnly={true} />
{/if}
{#if entry.children?.length}
@ -462,6 +465,8 @@
// See explanation at <https://github.com/sveltejs/language-tools/issues/452#issuecomment-723148184>.
dispatch("naturalWidth", detail);
}}
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
parentsValuePath={[...parentsValuePath, entry.value]}
open={getChildReference(entry)?.open || false}
direction="TopRight"
entries={entry.children}

View file

@ -4,6 +4,7 @@
import type { FrontendNodeType } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
@ -109,8 +110,8 @@
});
</script>
<div class="node-catalog">
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
<LayoutCol class="node-catalog">
<TextInput placeholder="Search Nodes" value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
<div class="list-results" on:wheel|passive|stopPropagation>
{#each nodeCategories as nodeCategory}
<details open={nodeCategory[1].open}>
@ -131,15 +132,12 @@
<TextLabel>No search results</TextLabel>
{/each}
</div>
</div>
</LayoutCol>
<style lang="scss" global>
.node-catalog {
max-height: 30vh;
min-width: 250px;
display: flex;
flex-direction: column;
align-items: stretch;
.text-input {
flex: 0 0 auto;
@ -149,13 +147,10 @@
.list-results {
overflow-y: auto;
flex: 1 1 auto;
// Together with the `margin-right: 4px;` on `details` below, this keeps a gap between the listings and the scrollbar
margin-right: -4px;
details {
cursor: pointer;
position: relative;
// Together with the `margin-right: -4px;` on `.list-results` above, this keeps a gap between the listings and the scrollbar
margin-right: 4px;
&[open] summary .text-label::before {
@ -164,8 +159,6 @@
summary {
display: flex;
align-items: center;
gap: 2px;
.text-label {
padding-left: 16px;
@ -189,6 +182,11 @@
.text-button {
width: 100%;
margin: 4px 0;
text-align: left;
}
&:last-child .text-button {
margin-bottom: 0;
}
}
}

View file

@ -495,13 +495,22 @@
--floating-menu-content-offset: 0;
.tail {
width: 0;
height: 0;
border-style: solid;
// Put the tail above the floating menu's shadow
z-index: 10;
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
position: fixed;
&,
&::before {
width: 0;
height: 0;
border-style: solid;
}
&::before {
content: "";
position: absolute;
}
}
.floating-menu-container {
@ -510,6 +519,7 @@
.floating-menu-content {
background: var(--color-2-mildblack);
box-shadow: rgba(var(--color-0-black-rgb), 0.5) 0 2px 4px;
border: 1px solid var(--color-4-dimgray);
border-radius: 4px;
color: var(--color-e-nearwhite);
font-size: inherit;
@ -517,6 +527,8 @@
z-index: 0;
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
position: fixed;
// Counteract the rightward shift caused by the border
margin-left: -1px;
}
}
@ -603,33 +615,69 @@
&.top .tail,
&.topleft .tail,
&.topright .tail {
border-width: 8px 6px 0 6px;
border-color: var(--color-2-mildblack) transparent transparent transparent;
margin-left: -6px;
margin-bottom: 2px;
border-color: var(--color-4-dimgray) transparent transparent transparent;
&::before {
border-color: var(--color-2-mildblack) transparent transparent transparent;
bottom: 0;
}
&,
&::before {
border-width: 8px 6px 0 6px;
margin-left: -6px;
margin-bottom: 2px;
}
}
&.bottom .tail,
&.bottomleft .tail,
&.bottomright .tail {
border-width: 0 6px 8px 6px;
border-color: transparent transparent var(--color-2-mildblack) transparent;
margin-left: -6px;
margin-top: 2px;
border-color: transparent transparent var(--color-4-dimgray) transparent;
&::before {
border-color: transparent transparent var(--color-2-mildblack) transparent;
top: 0;
}
&,
&::before {
border-width: 0 6px 8px 6px;
margin-left: -6px;
margin-top: 2px;
}
}
&.left .tail {
border-width: 6px 0 6px 8px;
border-color: transparent transparent transparent var(--color-2-mildblack);
margin-top: -6px;
margin-right: 2px;
border-color: transparent transparent transparent var(--color-4-dimgray);
&::before {
border-color: transparent transparent transparent var(--color-2-mildblack);
right: 0;
}
&,
&::before {
border-width: 6px 0 6px 8px;
margin-top: -6px;
margin-right: 2px;
}
}
&.right .tail {
border-width: 6px 8px 6px 0;
border-color: transparent var(--color-2-mildblack) transparent transparent;
margin-top: -6px;
margin-left: 2px;
border-color: transparent var(--color-4-dimgray) transparent transparent;
&::before {
border-color: transparent var(--color-2-mildblack) transparent transparent;
left: 0;
}
&,
&::before {
border-width: 6px 8px 6px 0;
margin-top: -6px;
margin-left: 2px;
}
}
&.top .floating-menu-container {

View file

@ -776,7 +776,7 @@
margin-right: 16px;
}
.right-scrollbar .scrollbar-input {
&:has(.top-ruler) .right-scrollbar .scrollbar-input {
margin-top: -16px;
}

View file

@ -9,13 +9,11 @@
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const GRID_COLLAPSE_SPACING = 10;
@ -202,46 +200,44 @@
>
<!-- Right click menu for adding nodes -->
{#if $nodeGraph.contextMenuInformation}
<LayoutCol
<FloatingMenu
class="context-menu"
data-context-menu
styles={{
left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.x * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
}}
open={true}
type="Popover"
direction="BottomLeft"
>
{#if $nodeGraph.contextMenuInformation.contextMenuData.type === "CreateNode"}
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.data.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
{:else if $nodeGraph.contextMenuInformation.contextMenuData.type === "ModifyNode"}
<LayoutRow class="toggle-layer-or-node">
<TextLabel>Display as</TextLabel>
<RadioInput
selectedIndex={$nodeGraph.contextMenuInformation.contextMenuData.data.currentlyIsNode ? 0 : 1}
entries={[
{
value: "node",
label: "Node",
action: () =>
$nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" &&
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, false),
},
{
value: "layer",
label: "Layer",
action: () =>
$nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" &&
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, true),
},
]}
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
<LayoutCol class="modify-node-menu">
<TextButton
label="Merge Selected Nodes"
action={() => {
editor.handle.mergeSelectedNodes();
nodeGraph.closeContextMenu();
}}
flush={true}
/>
</LayoutRow>
<Separator type="Section" direction="Vertical" />
<LayoutRow class="merge-selected-nodes">
<TextButton label="Merge Selected Nodes" action={() => editor.handle.mergeSelectedNodes()} />
</LayoutRow>
{@const currentlyIsNode = $nodeGraph.contextMenuInformation.contextMenuData.data.currentlyIsNode}
<TextButton
label={currentlyIsNode ? "Display as Layer" : "Display as Node"}
action={() => {
if ($nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode") {
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, currentlyIsNode);
}
nodeGraph.closeContextMenu();
}}
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
flush={true}
/>
</LayoutCol>
{/if}
</LayoutCol>
</FloatingMenu>
{/if}
{#if $nodeGraph.error}
@ -822,20 +818,17 @@
.context-menu {
width: max-content;
position: absolute;
box-sizing: border-box;
padding: 5px;
z-index: 3;
background-color: var(--color-3-darkgray);
border-radius: 4px;
.toggle-layer-or-node .text-label {
line-height: 24px;
margin-right: 8px;
.modify-node-menu {
margin: -4px;
.text-button {
justify-content: left;
}
}
.merge-selected-nodes {
justify-content: center;
.tail {
display: none;
}
}
@ -968,11 +961,16 @@
.imports-and-exports {
width: 100%;
height: 100%;
pointer-events: none;
position: absolute;
pointer-events: none;
// Keeps the connectors above the wires
z-index: 1;
// Zero specificity with `:where()` to allow other rules to override `pointer-events`
:where(.graph-view.open & > *) {
pointer-events: auto;
}
.connector {
position: absolute;
width: 8px;
@ -1071,7 +1069,7 @@
height: 100%;
// Zero specificity with `:where()` to allow other rules to override `pointer-events`
:where(& > *) {
:where(.graph-view.open & > *) {
pointer-events: auto;
}
}

View file

@ -177,7 +177,7 @@
{/if}
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
{#if textButton}
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, [])} on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
{/if}
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
{#if breadcrumbTrailButtons}

View file

@ -1,6 +1,7 @@
<script lang="ts">
import type { IconName } from "@graphite/icons";
import { createEventDispatcher } from "svelte";
import type { IconName } from "@graphite/icons";
import type { MenuListEntry } from "@graphite/messages";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
@ -8,6 +9,8 @@
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const dispatch = createEventDispatcher<{ selectedEntryValuePath: string[] }>();
let self: MenuList;
// Note: IconButton should be used if only an icon, but no label, is desired.
@ -36,7 +39,7 @@
// If there's no menu to open, trigger the action
if ((menuListChildren?.length ?? 0) === 0) {
// Call the action
if (action && !disabled) action();
if (!disabled) action?.();
// Exit early so we don't continue on and try to open the menu
return;
@ -84,6 +87,7 @@
{#if menuListChildrenExists}
<MenuList
on:open={({ detail }) => self && (self.open = detail)}
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
open={self?.open || false}
entries={menuListChildren || []}
direction="Bottom"
@ -165,6 +169,11 @@
&.open {
--button-background-color: var(--color-5-dullgray);
}
&.disabled {
--button-text-color: var(--color-8-uppergray);
--button-background-color: none;
}
}
.icon-label {

View file

@ -84,8 +84,8 @@
async function getEntries(): Promise<MenuListEntry[]> {
const x = isStyle ? fonts.getFontStyles(fontFamily) : fonts.fontNames();
return (await x).map((entry: { name: string; url: URL | undefined }) => ({
label: entry.name,
value: entry.name,
label: entry.name,
font: entry.url,
action: () => selectFont(entry.name),
}));

View file

@ -110,6 +110,7 @@
removeEventListener("keydown", trackCtrl);
removeEventListener("keyup", trackCtrl);
removeEventListener("mousemove", trackCtrl);
clearTimeout(repeatTimeout);
});
// ===============================

View file

@ -20,8 +20,6 @@
function handleEntryClick(radioEntryData: RadioEntryData) {
const index = entries.indexOf(radioEntryData);
dispatch("selectedIndex", index);
radioEntryData.action?.();
}
</script>

View file

@ -203,6 +203,7 @@
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("mousedown", onMouseDown);
window.removeEventListener("keydown", onKeyDown);
clearTimeout(repeatTimeout);
}
</script>

View file

@ -231,7 +231,7 @@
.input-mouse {
.bright {
fill: var(--color-e-nearwhite);
fill: var(--color-b-lightgray);
}
.dim {

View file

@ -2,12 +2,11 @@
import { getContext, onMount } from "svelte";
import type { Editor } from "@graphite/editor";
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/messages";
import { defaultWidgetLayout, patchWidgetLayout, UpdateMenuBarLayout } from "@graphite/messages";
import type { AppWindowState } from "@graphite/state-providers/app-window";
import { operatingSystem } from "@graphite/utility-functions/platform";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
import WindowButtonsLinux from "@graphite/components/window/title-bar/WindowButtonsLinux.svelte";
import WindowButtonsWeb from "@graphite/components/window/title-bar/WindowButtonsWeb.svelte";
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
@ -15,44 +14,12 @@
const appWindow = getContext<AppWindowState>("appWindow");
const editor = getContext<Editor>("editor");
// TODO: Apparently, Safari does not support the Keyboard.lock() API but does relax its authority over certain keyboard shortcuts in fullscreen mode, which we should take advantage of
const ACCEL_KEY = operatingSystem() === "Mac" ? "Command" : "Control";
const LOCK_REQUIRING_SHORTCUTS: KeyRaw[][] = [
[ACCEL_KEY, "KeyW"],
[ACCEL_KEY, "KeyN"],
[ACCEL_KEY, "Shift", "KeyN"],
[ACCEL_KEY, "KeyT"],
[ACCEL_KEY, "Shift", "KeyT"],
];
let entries: MenuListEntry[] = [];
let menuBarLayout = defaultWidgetLayout();
onMount(() => {
const arraysEqual = (a: KeyRaw[], b: KeyRaw[]): boolean => a.length === b.length && a.every((aValue, i) => aValue === b[i]);
const shortcutRequiresLock = (shortcut: LayoutKeysGroup): boolean => {
const shortcutKeys = shortcut.map((keyWithLabel) => keyWithLabel.key);
// If this shortcut matches any of the browser-reserved shortcuts
return LOCK_REQUIRING_SHORTCUTS.some((lockKeyCombo) => arraysEqual(shortcutKeys, lockKeyCombo));
};
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
const menuBarEntryToMenuListEntry = (entry: MenuBarEntry): MenuListEntry => ({
// From `MenuEntryCommon`
...entry,
// Shared names with fields that need to be converted from the type used in `MenuBarEntry` to that of `MenuListEntry`
action: () => editor.handle.widgetValueCommitAndUpdate(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
children: entry.children ? entry.children.map((entries) => entries.map((entry) => menuBarEntryToMenuListEntry(entry))) : undefined,
// New fields in `MenuListEntry`
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut.keys) : undefined,
value: "",
disabled: entry.disabled ?? undefined,
font: undefined,
});
entries = updateMenuBarLayout.layout.map(menuBarEntryToMenuListEntry);
patchWidgetLayout(menuBarLayout, updateMenuBarLayout);
menuBarLayout = menuBarLayout;
});
});
</script>
@ -61,9 +28,7 @@
<!-- Menu bar -->
<LayoutRow>
{#if $appWindow.platform !== "Mac"}
{#each entries as entry}
<TextButton label={entry.label} icon={entry.icon} menuListChildren={entry.children} action={entry.action} flush={true} />
{/each}
<WidgetLayout layout={menuBarLayout} />
{/if}
</LayoutRow>
<!-- Spacer -->
@ -88,6 +53,14 @@
> .layout-row {
flex: 0 0 auto;
> .widget-span {
--row-height: 28px;
> * {
--widget-height: 28px;
}
}
&.spacer {
flex: 1 1 100%;
}

View file

@ -991,30 +991,19 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri
return contrast(value);
}
type MenuEntryCommon = {
label: string;
icon?: IconName;
shortcut?: ActionKeys;
};
// The entry in the expanded menu or a sub-menu as received from the Rust backend
export type MenuBarEntry = MenuEntryCommon & {
action: Widget;
children?: MenuBarEntry[][];
disabled?: boolean;
};
// An entry in the all-encompassing MenuList component which defines all types of menus (which are spawned by widgets like `TextButton` and `DropdownInput`)
export type MenuListEntry = MenuEntryCommon & {
action?: () => void;
children?: MenuListEntry[][];
export type MenuListEntry = {
value: string;
shortcutRequiresLock?: boolean;
label: string;
font?: URL;
icon?: IconName;
disabled?: boolean;
tooltipLabel?: string;
tooltipDescription?: string;
font?: URL;
tooltipShortcut?: string;
shortcutKeys?: ActionKeys;
shortcutRequiresLock?: boolean;
children?: MenuListEntry[][];
};
export class CurveManipulatorGroup {
@ -1263,9 +1252,6 @@ export type RadioEntryData = {
tooltipLabel?: string;
tooltipDescription?: string;
tooltipShortcut?: string;
// Callbacks
action?: () => void;
};
export type RadioEntries = RadioEntryData[];
@ -1359,27 +1345,6 @@ export class TextButton extends WidgetProps {
menuListChildren!: MenuListEntry[][];
}
export type TextButtonWidget = {
tooltipLabel?: string;
tooltipDescription?: string;
message?: string | object;
callback?: () => void;
props: {
kind: "TextButton";
label: string;
icon?: IconName;
emphasized?: boolean;
flush?: boolean;
minWidth?: number;
disabled?: boolean;
tooltipLabel?: string;
tooltipDescription?: string;
// Callbacks
// `action` is used via `IconButtonWidget.callback`
};
};
export class BreadcrumbTrailButtons extends WidgetProps {
labels!: string[];
@ -1552,7 +1517,7 @@ export class WidgetDiffUpdate extends JsMessage {
diff!: WidgetDiff[];
}
type UIItem = LayoutGroup[] | LayoutGroup | Widget | Widget[] | MenuBarEntry[] | MenuBarEntry;
type UIItem = LayoutGroup[] | LayoutGroup | Widget | Widget[];
type WidgetDiff = { widgetPath: number[]; newValue: UIItem };
export function defaultWidgetLayout(): WidgetLayout {
@ -1581,8 +1546,6 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
console.error("Tried to index widget");
return targetLayout;
}
// This is a path traversal so we can assume from the backend that it exists
if (targetLayout && "action" in targetLayout) return targetLayout.children![index];
return targetLayout?.[index];
}, layout.layout as UIItem);
@ -1704,14 +1667,7 @@ export class UpdateLayersPanelControlBarRightLayout extends WidgetDiffUpdate {}
export class UpdateLayersPanelBottomBarLayout extends WidgetDiffUpdate {}
// Extends JsMessage instead of WidgetDiffUpdate because the menu bar isn't diffed
export class UpdateMenuBarLayout extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
@Transform(({ value }: { value: any }) => createMenuLayout(value))
layout!: MenuBarEntry[];
}
export class UpdateMenuBarLayout extends WidgetDiffUpdate {}
export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {}
@ -1725,23 +1681,6 @@ export class UpdateToolShelfLayout extends WidgetDiffUpdate {}
export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {}
function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] {
return menuBarEntry.map((entry) => ({
...entry,
children: createMenuLayoutRecursive(entry.children),
}));
}
function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] {
return children.map((groups) =>
groups.map((entry) => ({
...entry,
action: hoistWidgetHolders([entry.action])[0],
children: entry.children ? createMenuLayoutRecursive(entry.children) : undefined,
disabled: entry.disabled ?? false,
})),
);
}
// `any` is used since the type of the object should be known from the Rust side
type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHandle) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory;

View file

@ -54,6 +54,13 @@ export function createNodeGraphState(editor: Editor) {
reorderExportIndex: undefined as number | undefined,
});
function closeContextMenu() {
update((state) => {
state.contextMenuInformation = undefined;
return state;
});
}
// Set up message subscriptions on creation
editor.subscriptions.subscribeJsMessage(SendUIMetadata, (uiMetadata) => {
update((state) => {
@ -184,6 +191,7 @@ export function createNodeGraphState(editor: Editor) {
return {
subscribe,
closeContextMenu,
};
}
export type NodeGraphState = ReturnType<typeof createNodeGraphState>;