Replace globals with editor environment (#3656)

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Timon 2026-01-19 18:06:02 +01:00 committed by GitHub
parent 07fbcd489c
commit 95d3556204
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 193 additions and 259 deletions

View File

@ -1,3 +1,4 @@
use rand::Rng;
use rfd::AsyncFileDialog;
use std::fs;
use std::path::PathBuf;
@ -16,7 +17,7 @@ use crate::event::{AppEvent, AppEventScheduler};
use crate::persist::PersistentData;
use crate::render::{RenderError, RenderState};
use crate::window::Window;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Platform};
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState};
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
pub(crate) struct App {
@ -78,6 +79,8 @@ impl App {
let mut persistent_data = PersistentData::default();
persistent_data.load_from_disk();
let desktop_wrapper = DesktopWrapper::new(rand::rng().random());
Self {
render_state: None,
wgpu_context,
@ -91,7 +94,7 @@ impl App {
ui_scale: 1.,
app_event_receiver,
app_event_scheduler,
desktop_wrapper: DesktopWrapper::new(),
desktop_wrapper,
last_ui_update: Instant::now(),
cef_context,
cef_schedule: Some(Instant::now()),
@ -478,14 +481,6 @@ impl ApplicationHandler for App {
self.resize();
self.desktop_wrapper.init(self.wgpu_context.clone());
#[cfg(target_os = "windows")]
let platform = Platform::Windows;
#[cfg(target_os = "macos")]
let platform = Platform::Mac;
#[cfg(target_os = "linux")]
let platform = Platform::Linux;
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::UpdatePlatform(platform));
}
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {

View File

@ -66,6 +66,7 @@ pub fn start() {
}
};
// Must be called before event loop initialization or native window integrations will break
App::init();
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
@ -78,9 +79,9 @@ pub fn start() {
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver);
let cef_context = match cef_context_builder.initialize(cef_handler, cli.disable_ui_acceleration) {
Ok(c) => {
Ok(context) => {
tracing::info!("CEF initialized successfully");
c
context
}
Err(cef::InitError::AlreadyRunning) => {
tracing::error!("Another instance is already running, Exiting.");

View File

@ -1,11 +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::clipboard::utility_types::ClipboardContentRaw;
use graphite_editor::messages::prelude::*;
use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, Platform, SaveFileDialogContext};
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext};
pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) {
match message {
@ -111,15 +110,6 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
DesktopWrapperMessage::UpdatePlatform(platform) => {
let platform = match platform {
Platform::Windows => AppWindowPlatform::Windows,
Platform::Mac => AppWindowPlatform::Mac,
Platform::Linux => AppWindowPlatform::Linux,
};
let message = AppWindowMessage::UpdatePlatform { platform };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::UpdateMaximized { maximized } => {
let message = FrontendMessage::UpdateMaximized { maximized };
dispatcher.queue_editor_message(message);

View File

@ -1,5 +1,5 @@
use graph_craft::wasm_application_io::WasmApplicationIo;
use graphite_editor::application::Editor;
use graphite_editor::application::{Editor, Environment, Host, Platform};
use graphite_editor::messages::prelude::{FrontendMessage, Message};
// TODO: Remove usage of this reexport in desktop create and remove this line
@ -27,8 +27,18 @@ pub struct DesktopWrapper {
}
impl DesktopWrapper {
pub fn new() -> Self {
Self { editor: Editor::new() }
pub fn new(uuid_random_seed: u64) -> Self {
#[cfg(target_os = "windows")]
let host = Host::Windows;
#[cfg(target_os = "macos")]
let host = Host::Mac;
#[cfg(target_os = "linux")]
let host = Host::Linux;
let env = Environment { platform: Platform::Desktop, host };
Self {
editor: Editor::new(env, uuid_random_seed),
}
}
pub fn init(&self, wgpu_context: WgpuContext) {
@ -51,12 +61,6 @@ impl DesktopWrapper {
}
}
impl Default for DesktopWrapper {
fn default() -> Self {
Self::new()
}
}
pub enum NodeGraphExecutionResult {
HasRun(Option<wgpu::Texture>),
NotRun,

View File

@ -109,7 +109,6 @@ pub enum DesktopWrapperMessage {
content: Vec<u8>,
},
PollNodeGraphEvaluation,
UpdatePlatform(Platform),
UpdateMaximized {
maximized: bool,
},
@ -163,12 +162,6 @@ pub enum SaveFileDialogContext {
File { content: Vec<u8> },
}
pub enum Platform {
Windows,
Mac,
Linux,
}
pub enum MenuItem {
Action {
id: String,

View File

@ -1,24 +1,31 @@
use crate::dispatcher::Dispatcher;
use crate::messages::prelude::*;
pub use graphene_std::uuid::*;
use std::sync::OnceLock;
// TODO: serialize with serde to save the current editor state
pub struct Editor {
pub dispatcher: Dispatcher,
}
impl Editor {
/// Construct the editor.
/// Remember to provide a random seed with `editor::set_uuid_seed(seed)` before any editors can be used.
pub fn new() -> Self {
pub fn new(environment: Environment, uuid_random_seed: u64) -> Self {
ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once");
graphene_std::uuid::set_uuid_seed(uuid_random_seed);
Self { dispatcher: Dispatcher::new() }
}
#[cfg(test)]
pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) {
let _ = ENVIRONMENT.set(*Editor::environment());
graphene_std::uuid::set_uuid_seed(0);
let (runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime();
let dispatcher = Dispatcher::with_executor(executor);
(Self { dispatcher }, runtime)
let editor = Self {
dispatcher: Dispatcher::with_executor(executor),
};
(editor, runtime)
}
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Vec<FrontendMessage> {
@ -32,9 +39,53 @@ impl Editor {
}
}
impl Default for Editor {
fn default() -> Self {
Self::new()
static ENVIRONMENT: OnceLock<Environment> = OnceLock::new();
impl Editor {
#[cfg(not(test))]
pub fn environment() -> &'static Environment {
ENVIRONMENT.get().expect("Editor environment accessed before initialization")
}
#[cfg(test)]
pub fn environment() -> &'static Environment {
&Environment {
platform: Platform::Desktop,
host: Host::Linux,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Environment {
pub platform: Platform,
pub host: Host,
}
#[derive(Clone, Copy, Debug)]
pub enum Platform {
Desktop,
Web,
}
#[derive(Clone, Copy, Debug)]
pub enum Host {
Windows,
Mac,
Linux,
}
impl Environment {
pub fn is_desktop(&self) -> bool {
matches!(self.platform, Platform::Desktop)
}
pub fn is_web(&self) -> bool {
matches!(self.platform, Platform::Web)
}
pub fn is_windows(&self) -> bool {
matches!(self.host, Host::Windows)
}
pub fn is_mac(&self) -> bool {
matches!(self.host, Host::Mac)
}
pub fn is_linux(&self) -> bool {
matches!(self.host, Host::Linux)
}
}

View File

@ -23,12 +23,11 @@ pub struct DispatcherMessageHandlers {
debug_message_handler: DebugMessageHandler,
defer_message_handler: DeferMessageHandler,
dialog_message_handler: DialogMessageHandler,
globals_message_handler: GlobalsMessageHandler,
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
key_mapping_message_handler: KeyMappingMessageHandler,
layout_message_handler: LayoutMessageHandler,
menu_bar_message_handler: MenuBarMessageHandler,
pub portfolio_message_handler: PortfolioMessageHandler,
pub(crate) portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler,
tool_message_handler: ToolMessageHandler,
viewport_message_handler: ViewportMessageHandler,
@ -192,17 +191,11 @@ impl Dispatcher {
self.responses.push(message);
}
}
Message::Globals(message) => {
self.message_handlers.globals_message_handler.process_message(message, &mut queue, ());
}
Message::InputPreprocessor(message) => {
let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
self.message_handlers.input_preprocessor_message_handler.process_message(
message,
&mut queue,
InputPreprocessorMessageContext {
keyboard_platform,
viewport: &self.message_handlers.viewport_message_handler,
},
);

View File

@ -1,11 +1,8 @@
use crate::messages::prelude::*;
use super::app_window_message_handler::AppWindowPlatform;
#[impl_message(Message, AppWindow)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum AppWindowMessage {
UpdatePlatform { platform: AppWindowPlatform },
PointerLock,
PointerLockMove { x: f64, y: f64 },
Close,

View File

@ -1,20 +1,15 @@
use crate::messages::app_window::AppWindowMessage;
use crate::application::{Environment, Platform};
use crate::messages::prelude::*;
use crate::{application::Host, messages::app_window::AppWindowMessage};
use graphite_proc_macros::{ExtractField, message_handler_data};
#[derive(Debug, Clone, Default, ExtractField)]
pub struct AppWindowMessageHandler {
platform: AppWindowPlatform,
}
pub struct AppWindowMessageHandler {}
#[message_handler_data]
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
match message {
AppWindowMessage::UpdatePlatform { platform } => {
self.platform = platform;
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
}
AppWindowMessage::PointerLock => {
responses.add(FrontendMessage::WindowPointerLock);
}
@ -66,3 +61,14 @@ pub enum AppWindowPlatform {
Mac,
Linux,
}
impl From<&Environment> for AppWindowPlatform {
fn from(environment: &Environment) -> Self {
match (environment.platform, environment.host) {
(Platform::Web, _) => AppWindowPlatform::Web,
(Platform::Desktop, Host::Linux) => AppWindowPlatform::Linux,
(Platform::Desktop, Host::Mac) => AppWindowPlatform::Mac,
(Platform::Desktop, Host::Windows) => AppWindowPlatform::Windows,
}
}
}

View File

@ -1,4 +0,0 @@
use crate::messages::portfolio::utility_types::Platform;
use std::sync::OnceLock;
pub static GLOBAL_PLATFORM: OnceLock<Platform> = OnceLock::new();

View File

@ -1,8 +0,0 @@
use crate::messages::portfolio::utility_types::Platform;
use crate::messages::prelude::*;
#[impl_message(Message, Globals)]
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum GlobalsMessage {
SetPlatform { platform: Platform },
}

View File

@ -1,20 +0,0 @@
use crate::messages::prelude::*;
#[derive(Debug, Default, ExtractField)]
pub struct GlobalsMessageHandler {}
#[message_handler_data]
impl MessageHandler<GlobalsMessage, ()> for GlobalsMessageHandler {
fn process_message(&mut self, message: GlobalsMessage, _responses: &mut VecDeque<Message>, _: ()) {
match message {
GlobalsMessage::SetPlatform { platform } => {
if GLOBAL_PLATFORM.get() != Some(&platform) {
GLOBAL_PLATFORM.set(platform).expect("Failed to set GLOBAL_PLATFORM");
}
}
}
}
advertise_actions!(GlobalsMessageDiscriminant;
);
}

View File

@ -1,9 +0,0 @@
mod globals_message;
mod globals_message_handler;
pub mod global_variables;
#[doc(inline)]
pub use globals_message::{GlobalsMessage, GlobalsMessageDiscriminant};
#[doc(inline)]
pub use globals_message_handler::GlobalsMessageHandler;

View File

@ -1,8 +1,8 @@
use super::utility_types::input_keyboard::KeysGroup;
use super::utility_types::misc::Mapping;
use crate::application::Editor;
use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
#[derive(ExtractField)]
@ -48,11 +48,7 @@ impl InputMapperMessageHandler {
let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find);
// Get the `Key` for this platform's accelerator key
let keyboard_layout = || GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let platform_accel_key = match keyboard_layout() {
KeyboardPlatformLayout::Standard => Key::Control,
KeyboardPlatformLayout::Mac => Key::Command,
};
let platform_accel_key = if Editor::environment().is_mac() { Key::Command } else { Key::Control };
let entry_to_key = |entry: &MappingEntry| {
// Get the modifier keys for the entry (and convert them to Key)

View File

@ -1,3 +1,4 @@
use crate::application::Editor;
use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT};
use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
@ -8,7 +9,6 @@ use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapp
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate;
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
@ -27,8 +27,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
use InputMapperMessage::*;
use Key::*;
// TODO: Fix this failing to load the correct data (and throwing a console warning) because it's occurring before the value has been supplied during initialization from the JS `initAfterFrontendReady`
let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let is_mac = Editor::environment().is_mac();
// NOTICE:
// If a new mapping you added here isn't working (and perhaps another lower-precedence one is instead), make sure to advertise
@ -58,8 +57,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop),
//
// AppWindowMessage
entry!(KeyDown(F11); disabled=(keyboard_platform == KeyboardPlatformLayout::Mac), action_dispatch=AppWindowMessage::Fullscreen),
entry!(KeyDown(KeyF); modifiers=[Command, Control], disabled=(keyboard_platform != KeyboardPlatformLayout::Mac), action_dispatch=AppWindowMessage::Fullscreen),
entry!(KeyDown(F11); disabled=is_mac, action_dispatch=AppWindowMessage::Fullscreen),
entry!(KeyDown(KeyF); modifiers=[Command, Control], disabled=!is_mac, action_dispatch=AppWindowMessage::Fullscreen),
entry!(KeyDown(KeyQ); modifiers=[Command], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Close),
//
// ClipboardMessage
@ -429,8 +428,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
entry!(WheelScroll; modifiers=[Shift], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
entry!(WheelScroll; disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }),
// On Mac, the OS already converts Shift+scroll into horizontal scrolling so we have to reverse the behavior from normal to produce the same outcome
entry!(WheelScroll; modifiers=[Control], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform == KeyboardPlatformLayout::Mac }),
entry!(WheelScroll; modifiers=[Shift], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform != KeyboardPlatformLayout::Mac }),
entry!(WheelScroll; modifiers=[Control], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: is_mac }),
entry!(WheelScroll; modifiers=[Shift], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: !is_mac }),
entry!(WheelScroll; disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
entry!(KeyDown(PageUp); modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(1., 0.) }),
entry!(KeyDown(PageDown); modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(-1., 0.) }),

View File

@ -1,4 +1,4 @@
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::application::Editor;
use crate::messages::prelude::*;
use bitflags::bitflags;
use std::fmt::{self, Display, Formatter};
@ -258,7 +258,7 @@ impl fmt::Display for Key {
return write!(f, "{}", key_name.chars().skip(KEY_PREFIX.len()).collect::<String>());
}
let keyboard_layout = || GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let is_mac = Editor::environment().is_mac();
let name = match self {
// Writing system keys
@ -275,21 +275,21 @@ impl fmt::Display for Key {
Self::Slash => "/",
// Functional keys
Self::Alt => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Alt",
KeyboardPlatformLayout::Mac => "",
Self::Alt => match is_mac {
true => "",
false => "Alt",
},
Self::Meta => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "",
KeyboardPlatformLayout::Mac => "",
Self::Meta => match is_mac {
true => "",
false => "",
},
Self::Shift => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Shift",
KeyboardPlatformLayout::Mac => "",
Self::Shift => match is_mac {
true => "",
false => "Shift",
},
Self::Control => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Ctrl",
KeyboardPlatformLayout::Mac => "",
Self::Control => match is_mac {
true => "",
false => "Ctrl",
},
Self::Backspace => "",
@ -317,9 +317,9 @@ impl fmt::Display for Key {
// Other keys that aren't part of the W3C spec
Self::Command => "",
Self::Accel => match keyboard_layout() {
KeyboardPlatformLayout::Standard => "Ctrl",
KeyboardPlatformLayout::Mac => "",
Self::Accel => match is_mac {
true => "",
false => "Ctrl",
},
Self::MouseLeft => "Click",
Self::MouseRight => "R.Click",
@ -356,10 +356,9 @@ impl fmt::Display for KeysGroup {
.0
.iter()
.map(|key| {
let keyboard_layout = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let key_is_modifier = matches!(*key, Key::Control | Key::Command | Key::Alt | Key::Shift | Key::Meta | Key::Accel);
if keyboard_layout == KeyboardPlatformLayout::Mac && key_is_modifier {
if Editor::environment().is_mac() && key_is_modifier {
key.to_string()
} else {
key.to_string() + JOINER_MARK

View File

@ -1,13 +1,12 @@
use crate::application::Editor;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{MouseButton, MouseKeys, MouseState};
use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo;
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use std::time::Duration;
#[derive(ExtractField)]
pub struct InputPreprocessorMessageContext<'a> {
pub keyboard_platform: KeyboardPlatformLayout,
pub viewport: &'a ViewportMessageHandler,
}
@ -22,11 +21,11 @@ pub struct InputPreprocessorMessageHandler {
#[message_handler_data]
impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext<'a>> for InputPreprocessorMessageHandler {
fn process_message(&mut self, message: InputPreprocessorMessage, responses: &mut VecDeque<Message>, context: InputPreprocessorMessageContext<'a>) {
let InputPreprocessorMessageContext { keyboard_platform, viewport } = context;
let InputPreprocessorMessageContext { viewport } = context;
match message {
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
@ -43,7 +42,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
}
}
InputPreprocessorMessage::KeyDown { key, key_repeat, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
self.keyboard.set(key as usize);
if !key_repeat {
responses.add(InputMapperMessage::KeyDownNoRepeat(key));
@ -51,7 +50,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
responses.add(InputMapperMessage::KeyDown(key));
}
InputPreprocessorMessage::KeyUp { key, key_repeat, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
self.keyboard.unset(key as usize);
if !key_repeat {
responses.add(InputMapperMessage::KeyUpNoRepeat(key));
@ -59,7 +58,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
responses.add(InputMapperMessage::KeyUp(key));
}
InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
@ -67,7 +66,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
self.translate_mouse_event(mouse_state, true, responses);
}
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
@ -78,7 +77,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
@ -86,7 +85,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
@ -99,7 +98,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
self.frame_time.advance_timestamp(Duration::from_millis(timestamp));
}
InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
self.update_states_of_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
self.mouse.position = mouse_state.position;
@ -148,7 +147,7 @@ impl InputPreprocessorMessageHandler {
self.mouse = new_state;
}
fn update_states_of_modifier_keys(&mut self, pressed_modifier_keys: ModifierKeys, keyboard_platform: KeyboardPlatformLayout, responses: &mut VecDeque<Message>) {
fn update_states_of_modifier_keys(&mut self, pressed_modifier_keys: ModifierKeys, responses: &mut VecDeque<Message>) {
let is_key_pressed = |key_to_check: ModifierKeys| pressed_modifier_keys.contains(key_to_check);
// Update the state of the concrete modifier keys based on the source state
@ -157,16 +156,16 @@ impl InputPreprocessorMessageHandler {
self.update_modifier_key(Key::Control, is_key_pressed(ModifierKeys::CONTROL), responses);
// Update the state of either the concrete Meta or the Command keys based on which one is applicable for this platform
let meta_or_command = match keyboard_platform {
KeyboardPlatformLayout::Mac => Key::Command,
KeyboardPlatformLayout::Standard => Key::Meta,
let meta_or_command = match Editor::environment().is_mac() {
true => Key::Command,
false => Key::Meta,
};
self.update_modifier_key(meta_or_command, is_key_pressed(ModifierKeys::META_OR_COMMAND), responses);
// Update the state of the virtual Accel key (the primary accelerator key) based on the source state of the Control or Command key, whichever is relevant on this platform
let accel_virtual_key_state = match keyboard_platform {
KeyboardPlatformLayout::Mac => is_key_pressed(ModifierKeys::META_OR_COMMAND),
KeyboardPlatformLayout::Standard => is_key_pressed(ModifierKeys::CONTROL),
let accel_virtual_key_state = match Editor::environment().is_mac() {
true => is_key_pressed(ModifierKeys::META_OR_COMMAND),
false => is_key_pressed(ModifierKeys::CONTROL),
};
self.update_modifier_key(Key::Accel, accel_virtual_key_state, responses);
}
@ -188,7 +187,6 @@ impl InputPreprocessorMessageHandler {
mod test {
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta};
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
#[test]
@ -206,7 +204,6 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@ -226,7 +223,6 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@ -246,7 +242,6 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@ -268,7 +263,6 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);
@ -289,7 +283,6 @@ mod test {
let mut responses = VecDeque::new();
let context = InputPreprocessorMessageContext {
keyboard_platform: KeyboardPlatformLayout::Standard,
viewport: &ViewportMessageHandler::default(),
};
input_preprocessor.process_message(message, &mut responses, context);

View File

@ -22,8 +22,6 @@ pub enum Message {
#[child]
Frontend(FrontendMessage),
#[child]
Globals(GlobalsMessage),
#[child]
InputPreprocessor(InputPreprocessorMessage),
#[child]
KeyMapping(KeyMappingMessage),

View File

@ -8,7 +8,6 @@ pub mod debug;
pub mod defer;
pub mod dialog;
pub mod frontend;
pub mod globals;
pub mod input_mapper;
pub mod input_preprocessor;
pub mod layout;

View File

@ -1,3 +1,4 @@
use crate::application::Editor;
use crate::consts::{
VIEWPORT_ROTATE_SNAP_INTERVAL, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_LEVELS, VIEWPORT_ZOOM_MIN_FRACTION_COVER, VIEWPORT_ZOOM_MOUSE_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN,
VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
@ -8,7 +9,6 @@ use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::navigation::utility_types::NavigationOperation;
use crate::messages::portfolio::document::utility_types::misc::PTZ;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use glam::{DAffine2, DVec2};
@ -176,9 +176,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
}
NavigationMessage::CanvasPanMouseWheel { use_y_as_x } => {
// On Mac, the OS already converts Shift+scroll into horizontal scrolling
let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
let delta = if use_y_as_x && keyboard_platform == KeyboardPlatformLayout::Standard {
let delta = if use_y_as_x && !Editor::environment().is_mac() {
(ipp.mouse.scroll_delta.y, 0.).into()
} else {
ipp.mouse.scroll_delta.as_dvec2()

View File

@ -1,7 +1,7 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface;
use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid;
use crate::application::{Editor, generate_uuid};
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
use crate::messages::animation::TimingInformation;
use crate::messages::clipboard::utility_types::ClipboardContent;
@ -101,9 +101,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Messages
PortfolioMessage::Init => {
// Initialize the frontend with environment information
responses.add(FrontendMessage::UpdatePlatform {
platform: Editor::environment().into(),
});
// Tell frontend to load persistent preferences
responses.add(FrontendMessage::TriggerLoadPreferences);
// Before loading any documents, initially prepare the welcome screen buttons layout
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
// Tell frontend to load the current document
responses.add(FrontendMessage::TriggerLoadFirstAutoSaveDocument);
@ -128,15 +136,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
shortcut: action_shortcut_manual!(Key::Shift, Key::MouseLeft),
});
// Before loading any documents, initially prepare the welcome screen buttons layout
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
// Request status bar info layout
responses.add(PortfolioMessage::RequestStatusBarInfoLayout);
// Tell frontend to finish loading persistent documents
responses.add(FrontendMessage::TriggerLoadRestAutoSaveDocuments);
// Tell frontend to load documented passed in as launch arguments
responses.add(FrontendMessage::TriggerOpenLaunchDocuments);
}
PortfolioMessage::DocumentPassMessage { document_id, message } => {

View File

@ -82,37 +82,6 @@ impl FontCatalogStyle {
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum Platform {
#[default]
Unknown,
Windows,
Mac,
Linux,
}
impl Platform {
pub fn as_keyboard_platform_layout(&self) -> KeyboardPlatformLayout {
match self {
Platform::Mac => KeyboardPlatformLayout::Mac,
Platform::Windows | Platform::Linux => KeyboardPlatformLayout::Standard,
Platform::Unknown => {
warn!("The platform has not been set, remember to send `GlobalsMessage::SetPlatform` during editor initialization.");
KeyboardPlatformLayout::Standard
}
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum KeyboardPlatformLayout {
/// Standard keyboard mapping used by Windows and Linux
#[default]
Standard,
/// Keyboard mapping used by Macs where Command is sometimes used in favor of Control
Mac,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum PanelType {
#[default]

View File

@ -15,7 +15,6 @@ pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage,
pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
pub use crate::messages::dialog::{DialogMessage, DialogMessageContext, DialogMessageDiscriminant, DialogMessageHandler};
pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant};
pub use crate::messages::globals::{GlobalsMessage, GlobalsMessageDiscriminant, GlobalsMessageHandler};
pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappingMessageContext, KeyMappingMessageDiscriminant, KeyMappingMessageHandler};
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler};
pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
@ -51,7 +50,6 @@ pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, S
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
// Helper/miscellaneous
pub use crate::messages::globals::global_variables::*;
pub use crate::messages::portfolio::document::utility_types::misc::DocumentId;
pub use graphite_proc_macros::*;
pub use std::collections::{HashMap, HashSet, VecDeque};

View File

@ -1,9 +1,7 @@
use crate::application::Editor;
use crate::application::set_uuid_seed;
use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta, ViewportPosition};
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::portfolio::utility_types::Platform;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType;
@ -25,15 +23,11 @@ pub struct EditorTestUtils {
impl EditorTestUtils {
pub fn create() -> Self {
let _ = env_logger::builder().is_test(true).try_init();
set_uuid_seed(0);
let (mut editor, runtime) = Editor::new_local_executor();
// We have to set this directly instead of using `GlobalsMessage::SetPlatform` because race conditions with multiple tests can cause that message handler to set it more than once, which is a failure.
// It isn't sufficient to guard the message dispatch here with a check if the once_cell is empty, because that isn't atomic and the time between checking and handling the dispatch can let multiple through.
let _ = GLOBAL_PLATFORM.set(Platform::Windows).is_ok();
editor.handle_message(PortfolioMessage::Init);
Self { editor, runtime }
}

View File

@ -16,7 +16,6 @@
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
import { createPortfolioState } from "@graphite/state-providers/portfolio";
import { createTooltipState } from "@graphite/state-providers/tooltip";
import { operatingSystem } from "@graphite/utility-functions/platform";
import MainWindow from "@graphite/components/window/MainWindow.svelte";
@ -51,7 +50,7 @@
onMount(() => {
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
editor.handle.initAfterFrontendReady(operatingSystem());
editor.handle.initAfterFrontendReady();
});
onDestroy(() => {

View File

@ -1,8 +1,12 @@
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
import init, { setRandomSeed, wasmMemory, EditorHandle, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
import { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import init, { wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
import { type JsMessageType } from "@graphite/messages";
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/subscription-router";
import { operatingSystem } from "@graphite/utility-functions/platform";
// TODO: Remove `raw`, split out `subscriptions`, and unwrap the remaining `handle` so `EditorHandle` can replace `Editor` and then it can also be renamed to `Editor` to fully remove `EditorHandle`.
export type Editor = {
raw: WebAssembly.Memory;
handle: EditorHandle;
@ -14,10 +18,10 @@ let wasmImport: WebAssembly.Memory | undefined;
// Should be called asynchronously before `createEditor()`.
export async function initWasm() {
// Skip if the WASM module is already initialized
// Skip if the Wasm module is already initialized
if (wasmImport !== undefined) return;
// Import the WASM module JS bindings and wrap them in the panic proxy
// Import the Wasm module JS bindings and wrap them in the panic proxy
const wasm = await init();
for (const [name, f] of Object.entries(wasm)) {
if (name.startsWith("__node_registry")) f();
@ -28,28 +32,27 @@ export async function initWasm() {
(window as any).imageCanvases = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).receiveNativeMessage = receiveNativeMessage;
// Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const randomSeed = BigInt(randomSeedFloat);
setRandomSeed(randomSeed);
}
// Should be called after running `initWasm()` and its promise resolving.
export function createEditor(): Editor {
// Raw: object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the `EditorHandle` struct (generated by wasm-bindgen)
if (!wasmImport) throw new Error("Editor WASM backend was not initialized at application startup");
// Raw: object containing several callable functions from `editor_api.rs` defined directly on the Wasm module, not the `EditorHandle` struct (generated by wasm-bindgen)
if (!wasmImport) throw new Error("Editor Wasm backend was not initialized at application startup");
const raw: WebAssembly.Memory = wasmImport;
// Provide a random starter seed which must occur after initializing the Wasm module, since Wasm can't generate its own random numbers
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const randomSeed = BigInt(randomSeedFloat);
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
const handle: EditorHandle = new EditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>) => {
// This callback is called by WASM when a FrontendMessage is received from the WASM wrapper `EditorHandle`
const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: JsMessageType, messageData: Record<string, unknown>) => {
// This callback is called by Wasm when a FrontendMessage is received from the Wasm wrapper `EditorHandle`
// We pass along the first two arguments then add our own `raw` and `handle` context for the last two arguments
subscriptions.handleJsMessage(messageType, messageData, raw, handle);
});
// Subscriptions: allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter();
// Subscriptions: allows subscribing to messages in JS that are sent from the Wasm backend
const subscriptions = createSubscriptionRouter();
// Check if the URL hash fragment has any demo artwork to be loaded
(async () => {

View File

@ -40,7 +40,7 @@ export function githubUrl(panicDetails: string): string {
Provide any further information or context that you think would be helpful in fixing the issue. Screenshots or video can be linked or attached to this issue.
**Browser and OS**
${browserVersion()}, ${operatingSystem().replace("Unknown", "YOUR OPERATING SYSTEM")}
${browserVersion()}, ${operatingSystem()}
**Stack Trace**
Copied from the crash dialog in the Graphite editor:

View File

@ -25,18 +25,17 @@ export function browserVersion(): string {
return `${match[0]} ${match[1]}`;
}
export type OperatingSystem = "Windows" | "Mac" | "Linux" | "Unknown";
export type OperatingSystem = "Windows" | "Mac" | "Linux";
export function operatingSystem(): OperatingSystem {
const osTable: Record<string, OperatingSystem> = {
Windows: "Windows",
Mac: "Mac",
Linux: "Linux",
Unknown: "Unknown",
};
const userAgentOS = Object.keys(osTable).find((key) => window.navigator.userAgent.includes(key));
return osTable[userAgentOS || "Unknown"];
return osTable[userAgentOS || "Windows"];
}
export function isDesktop(): boolean {

View File

@ -12,7 +12,7 @@ use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily, Platform};
use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily};
use editor::messages::prelude::*;
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
use graph_craft::document::NodeId;
@ -31,7 +31,7 @@ use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, window};
#[cfg(not(feature = "native"))]
use crate::EDITOR;
#[cfg(not(feature = "native"))]
use editor::application::Editor;
use editor::application::{Editor, Environment, Host, Platform};
static IMAGE_DATA_HASH: AtomicU64 = AtomicU64::new(0);
@ -43,14 +43,7 @@ fn calculate_hash<T: std::hash::Hash>(t: &T) -> u64 {
hasher.finish()
}
/// Set the random seed used by the editor by calling this from JS upon initialization.
/// This is necessary because WASM doesn't have a random number generator.
#[wasm_bindgen(js_name = setRandomSeed)]
pub fn set_random_seed(seed: u64) {
editor::application::set_uuid_seed(seed);
}
/// Provides a handle to access the raw WASM memory.
/// Provides a handle to access the raw Wasm memory.
#[wasm_bindgen(js_name = wasmMemory)]
pub fn wasm_memory() -> JsValue {
wasm_bindgen::memory()
@ -89,9 +82,20 @@ impl EditorHandle {
#[wasm_bindgen]
impl EditorHandle {
#[cfg(not(feature = "native"))]
#[wasm_bindgen(constructor)]
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor = Editor::new();
pub fn create(platform: String, uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorHandle {
let editor = Editor::new(
Environment {
platform: Platform::Web,
host: match platform.as_str() {
"Linux" => Host::Linux,
"Mac" => Host::Mac,
"Windows" => Host::Windows,
_ => unreachable!(),
},
},
uuid_random_seed,
);
let editor_handle = EditorHandle { frontend_message_handler_callback };
if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() {
log::error!("Attempted to initialize the editor more than once");
@ -103,8 +107,7 @@ impl EditorHandle {
}
#[cfg(feature = "native")]
#[wasm_bindgen(constructor)]
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
pub fn create(_platform: String, _uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorHandle {
let editor_handle = EditorHandle { frontend_message_handler_callback };
if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() {
log::error!("Attempted to initialize the editor handle more than once");
@ -184,18 +187,10 @@ impl EditorHandle {
// ========================================================================
#[wasm_bindgen(js_name = initAfterFrontendReady)]
pub fn init_after_frontend_ready(&self, platform: String) {
pub fn init_after_frontend_ready(&self) {
#[cfg(feature = "native")]
crate::native_communcation::initialize_native_communication();
// Send initialization messages
let platform = match platform.as_str() {
"Windows" => Platform::Windows,
"Mac" => Platform::Mac,
"Linux" => Platform::Linux,
_ => Platform::Unknown,
};
self.dispatch(GlobalsMessage::SetPlatform { platform });
self.dispatch(PortfolioMessage::Init);
// Poll node graph evaluation on `requestAnimationFrame`