mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Refactor the old menu bar plumbing to use standard TextButtons (#3444)
* 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:
parent
3fd0460d03
commit
600fb5c28f
34 changed files with 1222 additions and 1355 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2328,6 +2328,7 @@ dependencies = [
|
|||
name = "graphite-desktop-wrapper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"dirs",
|
||||
"futures",
|
||||
"graph-craft",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ pub(crate) enum AppEvent {
|
|||
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||
CloseWindow,
|
||||
MenuEvent { id: u64 },
|
||||
MenuEvent { id: String },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -31,3 +31,4 @@ image = { workspace = true }
|
|||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
keyboard-types = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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: _ } => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
247
desktop/wrapper/src/utils.rs
Normal file
247
desktop/wrapper/src/utils.rs
Normal 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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -279,7 +279,7 @@ pub enum FrontendMessage {
|
|||
UpdateMenuBarLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
layout: Vec<MenuBarEntry>,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateMouseCursor {
|
||||
cursor: MouseCursorIcon,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
pub mod button_widgets;
|
||||
pub mod input_widgets;
|
||||
pub mod label_widgets;
|
||||
pub mod menu_widgets;
|
||||
|
|
|
|||
|
|
@ -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"))]
|
||||
|
|
|
|||
|
|
@ -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!()
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -776,7 +776,7 @@
|
|||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.right-scrollbar .scrollbar-input {
|
||||
&:has(.top-ruler) .right-scrollbar .scrollbar-input {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@
|
|||
removeEventListener("keydown", trackCtrl);
|
||||
removeEventListener("keyup", trackCtrl);
|
||||
removeEventListener("mousemove", trackCtrl);
|
||||
clearTimeout(repeatTimeout);
|
||||
});
|
||||
|
||||
// ===============================
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@
|
|||
function handleEntryClick(radioEntryData: RadioEntryData) {
|
||||
const index = entries.indexOf(radioEntryData);
|
||||
dispatch("selectedIndex", index);
|
||||
|
||||
radioEntryData.action?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@
|
|||
window.removeEventListener("pointermove", onPointerMove);
|
||||
window.removeEventListener("mousedown", onMouseDown);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
clearTimeout(repeatTimeout);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
|
||||
.input-mouse {
|
||||
.bright {
|
||||
fill: var(--color-e-nearwhite);
|
||||
fill: var(--color-b-lightgray);
|
||||
}
|
||||
|
||||
.dim {
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue