mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Merge branch 'GraphiteEditor:master' into branching-mesh-fill
This commit is contained in:
commit
6716833bc7
80 changed files with 2231 additions and 1564 deletions
9
.github/workflows/library-rawkit.yml
vendored
9
.github/workflows/library-rawkit.yml
vendored
|
|
@ -38,6 +38,13 @@ jobs:
|
|||
|
||||
- name: 📦 Run sccache-cache
|
||||
uses: mozilla-actions/sccache-action@v0.0.6
|
||||
continue-on-error: true
|
||||
|
||||
- name: 🔧 Fallback if sccache fails
|
||||
if: failure()
|
||||
run: |
|
||||
echo "sccache failed, disabling it"
|
||||
echo "RUSTC_WRAPPER=" >> $GITHUB_ENV
|
||||
|
||||
- name: 🔬 Check Rust formatting
|
||||
run: |
|
||||
|
|
@ -56,4 +63,4 @@ jobs:
|
|||
|
||||
- name: 📈 Run sccache stat for check
|
||||
shell: bash
|
||||
run: sccache --show-stats
|
||||
run: sccache --show-stats || echo "sccache stats unavailable"
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@ db-urls = ["https://github.com/rustsec/advisory-db"]
|
|||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0370", # Unmaintained but still fully functional crate `proc-macro-error`
|
||||
"RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative`
|
||||
"RUSTSEC-2024-0436", # Unmaintained but still fully functional crate `paste`
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
|
|
|
|||
|
|
@ -54,9 +54,11 @@ windows = { version = "0.58.0", features = [
|
|||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
|
||||
# macOS-specific dependencies
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ fn main() {
|
|||
// TODO: Replace with actual version
|
||||
res.set_version_info(winres::VersionInfo::FILEVERSION, {
|
||||
const MAJOR: u64 = 0;
|
||||
const MINOR: u64 = 999;
|
||||
const MINOR: u64 = 0;
|
||||
const PATCH: u64 = 0;
|
||||
const RELEASE: u64 = 0;
|
||||
(MAJOR << 48) | (MINOR << 32) | (PATCH << 16) | RELEASE
|
||||
});
|
||||
res.set("FileVersion", "0.999.0.0");
|
||||
res.set("ProductVersion", "0.999.0.0");
|
||||
res.set("FileVersion", "0.0.0.0");
|
||||
res.set("ProductVersion", "0.0.0.0");
|
||||
|
||||
res.set("OriginalFilename", "Graphite.exe");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
use rfd::AsyncFileDialog;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::mpsc::{Receiver, Sender, SyncSender};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::event_loop::ControlFlow;
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use crate::cef;
|
||||
|
|
@ -20,7 +16,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, Platform};
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Platform};
|
||||
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
pub(crate) struct App {
|
||||
|
|
@ -31,6 +27,7 @@ pub(crate) struct App {
|
|||
window_size: PhysicalSize<u32>,
|
||||
window_maximized: bool,
|
||||
window_fullscreen: bool,
|
||||
ui_scale: f64,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
desktop_wrapper: DesktopWrapper,
|
||||
|
|
@ -87,6 +84,7 @@ impl App {
|
|||
window_size: PhysicalSize { width: 0, height: 0 },
|
||||
window_maximized: false,
|
||||
window_fullscreen: false,
|
||||
ui_scale: 1.,
|
||||
app_event_receiver,
|
||||
app_event_scheduler,
|
||||
desktop_wrapper: DesktopWrapper::new(),
|
||||
|
|
@ -123,7 +121,7 @@ impl App {
|
|||
}
|
||||
|
||||
let size = window.surface_size();
|
||||
let scale = window.scale_factor();
|
||||
let scale = window.scale_factor() * self.ui_scale;
|
||||
let is_new_size = size != self.window_size;
|
||||
let is_new_scale = scale != self.window_scale;
|
||||
|
||||
|
|
@ -232,6 +230,10 @@ impl App {
|
|||
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::UpdateUIScale { scale } => {
|
||||
self.ui_scale = scale;
|
||||
self.resize();
|
||||
}
|
||||
DesktopFrontendMessage::UpdateOverlays(scene) => {
|
||||
if let Some(render_state) = &mut self.render_state {
|
||||
render_state.set_overlays_scene(scene);
|
||||
|
|
@ -454,6 +456,10 @@ impl ApplicationHandler for App {
|
|||
let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone());
|
||||
self.render_state = Some(render_state);
|
||||
|
||||
if let Some(window) = &self.window.as_ref() {
|
||||
window.show();
|
||||
}
|
||||
|
||||
self.resize();
|
||||
|
||||
self.desktop_wrapper.init(self.wgpu_context.clone());
|
||||
|
|
@ -489,6 +495,10 @@ impl ApplicationHandler for App {
|
|||
|
||||
let Some(render_state) = &mut self.render_state else { return };
|
||||
if let Some(window) = &self.window {
|
||||
if !window.can_render() {
|
||||
return;
|
||||
}
|
||||
|
||||
match render_state.render(window) {
|
||||
Ok(_) => {}
|
||||
Err(RenderError::OutdatedUITextureError) => {
|
||||
|
|
@ -520,6 +530,32 @@ impl ApplicationHandler for App {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
|
||||
WindowEvent::PointerButton {
|
||||
button: ButtonSource::Mouse(button),
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
} => {
|
||||
let mouse_keys = match button {
|
||||
MouseButton::Back => Some(MouseKeys::BACK),
|
||||
MouseButton::Forward => Some(MouseKeys::FORWARD),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(mouse_keys) = mouse_keys {
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
|
||||
editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
|
||||
editor_mouse_state: Default::default(),
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat
|
|||
MouseButton::Left => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT),
|
||||
MouseButton::Right => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT),
|
||||
MouseButton::Middle => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE),
|
||||
_ => return, //TODO: Handle Forward and Back button
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let Some(host) = browser.host() else { return };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub(crate) const APP_NAME: &str = "Graphite";
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub(crate) const APP_ID: &str = "art.graphite.Graphite";
|
||||
|
||||
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ pub(crate) trait NativeWindow {
|
|||
fn init() {}
|
||||
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
|
||||
fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self;
|
||||
fn can_render(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn update_menu(&self, _entries: Vec<MenuItem>) {}
|
||||
fn hide(&self) {}
|
||||
fn hide_others(&self) {}
|
||||
|
|
@ -52,6 +55,7 @@ impl Window {
|
|||
.with_min_surface_size(winit::dpi::LogicalSize::new(400, 300))
|
||||
.with_surface_size(winit::dpi::LogicalSize::new(1200, 800))
|
||||
.with_resizable(true)
|
||||
.with_visible(false)
|
||||
.with_theme(Some(winit::window::Theme::Dark));
|
||||
|
||||
attributes = native::NativeWindowImpl::configure(attributes, event_loop);
|
||||
|
|
@ -67,6 +71,11 @@ impl Window {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn show(&self) {
|
||||
self.winit_window.set_visible(true);
|
||||
self.winit_window.focus_window();
|
||||
}
|
||||
|
||||
pub(crate) fn request_redraw(&self) {
|
||||
self.winit_window.request_redraw();
|
||||
}
|
||||
|
|
@ -79,6 +88,10 @@ impl Window {
|
|||
self.winit_window.pre_present_notify();
|
||||
}
|
||||
|
||||
pub(crate) fn can_render(&self) -> bool {
|
||||
self.native_handle.can_render()
|
||||
}
|
||||
|
||||
pub(crate) fn surface_size(&self) -> winit::dpi::PhysicalSize<u32> {
|
||||
self.winit_window.surface_size()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use winit::dpi::PhysicalSize;
|
||||
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
|
||||
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||
use windows::core::HSTRING;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::icon::Icon;
|
||||
use winit::platform::windows::{WinIcon, WindowAttributesWindows};
|
||||
use winit::window::{Window, WindowAttributes};
|
||||
|
||||
use crate::consts::APP_ID;
|
||||
use crate::event::AppEventScheduler;
|
||||
|
||||
pub(super) struct NativeWindowImpl {
|
||||
|
|
@ -11,17 +12,26 @@ pub(super) struct NativeWindowImpl {
|
|||
}
|
||||
|
||||
impl super::NativeWindow for NativeWindowImpl {
|
||||
fn init() {
|
||||
let app_id = HSTRING::from(APP_ID);
|
||||
unsafe {
|
||||
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok();
|
||||
SetCurrentProcessExplicitAppUserModelID(&app_id).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
|
||||
let icon = WinIcon::from_resource(1, Some(PhysicalSize::new(256, 256))).ok().map(|icon| Icon(std::sync::Arc::new(icon)));
|
||||
let win_window = WindowAttributesWindows::default().with_taskbar_icon(icon);
|
||||
let icon = WinIcon::from_resource(1, None).ok().map(|icon| Icon(std::sync::Arc::new(icon)));
|
||||
attributes.with_window_icon(icon).with_platform_attributes(Box::new(win_window))
|
||||
attributes
|
||||
}
|
||||
|
||||
fn new(window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self {
|
||||
let native_handle = native_handle::NativeWindowHandle::new(window);
|
||||
NativeWindowImpl { native_handle }
|
||||
}
|
||||
|
||||
fn can_render(&self) -> bool {
|
||||
self.native_handle.can_render()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeWindowImpl {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
//! - The helper window is a invisible window that never activates, so it doesn't steal focus from the main window.
|
||||
//! - The main window needs to update the helper window's position and size whenever it moves or resizes.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::time::Instant;
|
||||
use wgpu::rwh::{HasWindowHandle, RawWindowHandle};
|
||||
use windows::Win32::Foundation::*;
|
||||
use windows::Win32::Graphics::Dwm::*;
|
||||
|
|
@ -21,16 +22,23 @@ use windows::Win32::UI::WindowsAndMessaging::*;
|
|||
use windows::core::PCWSTR;
|
||||
use winit::window::Window;
|
||||
|
||||
#[derive(Default)]
|
||||
struct NativeWindowState {
|
||||
can_render: bool,
|
||||
can_render_since: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct NativeWindowHandle {
|
||||
main: HWND,
|
||||
helper: HWND,
|
||||
prev_window_message_handler: isize,
|
||||
state: Arc<Mutex<NativeWindowState>>,
|
||||
}
|
||||
impl NativeWindowHandle {
|
||||
pub(super) fn new(window: &dyn Window) -> NativeWindowHandle {
|
||||
// Extract Win32 HWND from winit.
|
||||
let hwnd = match window.window_handle().expect("No window handle").as_raw() {
|
||||
let main = match window.window_handle().expect("No window handle").as_raw() {
|
||||
RawWindowHandle::Win32(h) => HWND(h.hwnd.get() as *mut std::ffi::c_void),
|
||||
_ => panic!("Not a Win32 window"),
|
||||
};
|
||||
|
|
@ -57,47 +65,62 @@ impl NativeWindowHandle {
|
|||
None,
|
||||
HINSTANCE(std::ptr::null_mut()),
|
||||
// Pass the main window's HWND to WM_NCCREATE so the helper can store it.
|
||||
Some(&hwnd as *const _ as _),
|
||||
Some(&main as *const _ as _),
|
||||
)
|
||||
}
|
||||
.expect("CreateWindowExW failed");
|
||||
|
||||
// Subclass the main window.
|
||||
// https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowlongptra
|
||||
let prev_window_message_handler = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, main_window_handle_message as isize) };
|
||||
let prev_window_message_handler = unsafe { SetWindowLongPtrW(main, GWLP_WNDPROC, main_window_handle_message as isize) };
|
||||
if prev_window_message_handler == 0 {
|
||||
let _ = unsafe { DestroyWindow(helper) };
|
||||
panic!("SetWindowLongPtrW failed");
|
||||
}
|
||||
|
||||
let inner = NativeWindowHandle {
|
||||
main: hwnd,
|
||||
let native_handle = NativeWindowHandle {
|
||||
main,
|
||||
helper,
|
||||
prev_window_message_handler,
|
||||
state: Arc::new(Mutex::new(NativeWindowState::default())),
|
||||
};
|
||||
registry::insert(&inner);
|
||||
registry::insert(&native_handle);
|
||||
|
||||
// Place the helper over the main window and show it without activation.
|
||||
unsafe { position_helper(hwnd, helper) };
|
||||
unsafe { position_helper(main, helper) };
|
||||
let _ = unsafe { ShowWindow(helper, SW_SHOWNOACTIVATE) };
|
||||
|
||||
// DwmExtendFrameIntoClientArea is needed to keep native window frame (but no titlebar).
|
||||
// https://learn.microsoft.com/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea
|
||||
// https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
let mut boarder_size: u32 = 1;
|
||||
let _ = unsafe { DwmGetWindowAttribute(hwnd, DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, &mut boarder_size as *mut _ as *mut _, size_of::<u32>() as u32) };
|
||||
let _ = unsafe { DwmGetWindowAttribute(main, DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, &mut boarder_size as *mut _ as *mut _, size_of::<u32>() as u32) };
|
||||
let margins = MARGINS {
|
||||
cxLeftWidth: 0,
|
||||
cxRightWidth: 0,
|
||||
cyBottomHeight: 0,
|
||||
cyTopHeight: boarder_size as i32,
|
||||
};
|
||||
let _ = unsafe { DwmExtendFrameIntoClientArea(hwnd, &margins) };
|
||||
let _ = unsafe { DwmExtendFrameIntoClientArea(main, &margins) };
|
||||
|
||||
let hinst = unsafe { GetModuleHandleW(None) }.unwrap();
|
||||
|
||||
// Set taskbar icon
|
||||
if let Ok(big) = unsafe { LoadImageW(hinst, PCWSTR(1usize as *const u16), IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_SHARED) } {
|
||||
unsafe { SetClassLongPtrW(main, GCLP_HICON, big.0 as isize) };
|
||||
unsafe { SendMessageW(main, WM_SETICON, WPARAM(ICON_BIG as usize), LPARAM(big.0 as isize)) };
|
||||
}
|
||||
|
||||
// Set window icon
|
||||
if let Ok(small) = unsafe { LoadImageW(hinst, PCWSTR(1usize as *const u16), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_SHARED) } {
|
||||
unsafe { SetClassLongPtrW(main, GCLP_HICONSM, small.0 as isize) };
|
||||
unsafe { SendMessageW(main, WM_SETICON, WPARAM(ICON_SMALL as usize), LPARAM(small.0 as isize)) };
|
||||
}
|
||||
|
||||
// Force window update
|
||||
let _ = unsafe { SetWindowPos(hwnd, None, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER) };
|
||||
let _ = unsafe { SetWindowPos(main, None, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER) };
|
||||
|
||||
inner
|
||||
native_handle
|
||||
}
|
||||
|
||||
pub(super) fn destroy(&self) {
|
||||
|
|
@ -105,10 +128,36 @@ impl NativeWindowHandle {
|
|||
|
||||
// Undo subclassing and destroy the helper window.
|
||||
let _ = unsafe { SetWindowLongPtrW(self.main, GWLP_WNDPROC, self.prev_window_message_handler) };
|
||||
if self.helper.0 != std::ptr::null_mut() {
|
||||
if !self.helper.is_invalid() {
|
||||
let _ = unsafe { DestroyWindow(self.helper) };
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering should be disabled when window is minimized
|
||||
// Rendering also needs to be disabled during minimize and restore animations
|
||||
// Reenabling rendering is done after a small delay to account for restore animation
|
||||
// TODO: Find a cleaner solution that doesn't depend on a timeout
|
||||
pub(super) fn can_render(&self) -> bool {
|
||||
let can_render = !unsafe { IsIconic(self.main).into() } && unsafe { IsWindowVisible(self.main).into() };
|
||||
let Ok(mut state) = self.state.lock() else {
|
||||
tracing::error!("Failed to lock NativeWindowState");
|
||||
return true;
|
||||
};
|
||||
match (can_render, state.can_render, state.can_render_since) {
|
||||
(true, false, None) => {
|
||||
state.can_render_since = Some(Instant::now());
|
||||
}
|
||||
(true, false, Some(can_render_since)) if can_render_since.elapsed().as_millis() > 50 => {
|
||||
state.can_render = true;
|
||||
state.can_render_since = None;
|
||||
}
|
||||
(false, true, _) => {
|
||||
state.can_render = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
state.can_render
|
||||
}
|
||||
}
|
||||
|
||||
mod registry {
|
||||
|
|
@ -212,7 +261,7 @@ unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wpara
|
|||
// Call the previous window message handler, this is a standard subclassing pattern.
|
||||
let prev_window_message_handler_fn_ptr: *const () = std::ptr::without_provenance(handle.prev_window_message_handler as usize);
|
||||
let prev_window_message_handler_fn = unsafe { std::mem::transmute::<_, _>(prev_window_message_handler_fn_ptr) };
|
||||
return unsafe { CallWindowProcW(Some(prev_window_message_handler_fn), hwnd, msg, wparam, lparam) };
|
||||
unsafe { CallWindowProcW(Some(prev_window_message_handler_fn), hwnd, msg, wparam, lparam) }
|
||||
}
|
||||
|
||||
// Helper window message handler, called on the UI thread for every message the helper window receives.
|
||||
|
|
@ -264,18 +313,19 @@ unsafe extern "system" fn helper_window_handle_message(hwnd: HWND, msg: u32, wpa
|
|||
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
|
||||
}
|
||||
|
||||
const RESIZE_BAND_THICKNESS: i32 = 8;
|
||||
|
||||
// Position the helper window to match the main window's location and size (plus the resize band size).
|
||||
unsafe fn position_helper(main: HWND, helper: HWND) {
|
||||
let mut r = RECT::default();
|
||||
let _ = unsafe { GetWindowRect(main, &mut r) };
|
||||
|
||||
const RESIZE_BAND_SIZE: i32 = 8;
|
||||
let x = r.left - RESIZE_BAND_SIZE;
|
||||
let y = r.top - RESIZE_BAND_SIZE;
|
||||
let w = (r.right - r.left) + RESIZE_BAND_SIZE * 2;
|
||||
let h = (r.bottom - r.top) + RESIZE_BAND_SIZE * 2;
|
||||
let x = r.left - RESIZE_BAND_THICKNESS;
|
||||
let y = r.top - RESIZE_BAND_THICKNESS;
|
||||
let w = (r.right - r.left) + RESIZE_BAND_THICKNESS * 2;
|
||||
let h = (r.bottom - r.top) + RESIZE_BAND_THICKNESS * 2;
|
||||
|
||||
let _ = unsafe { SetWindowPos(helper, main, x, y, w, h, SWP_NOACTIVATE) };
|
||||
let _ = unsafe { SetWindowPos(helper, main, x, y, w, h, SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING) };
|
||||
}
|
||||
|
||||
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
||||
|
|
@ -285,7 +335,6 @@ unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
|||
let mut r = RECT::default();
|
||||
let _ = unsafe { GetWindowRect(helper, &mut r) };
|
||||
|
||||
const RESIZE_BAND_THICKNESS: i32 = 8;
|
||||
let on_top = y < (r.top + RESIZE_BAND_THICKNESS) as u32;
|
||||
let on_right = x >= (r.right - RESIZE_BAND_THICKNESS) as u32;
|
||||
let on_bottom = y >= (r.bottom - RESIZE_BAND_THICKNESS) as u32;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
|||
DesktopWrapperMessage::FromWeb(message) => {
|
||||
dispatcher.queue_editor_message(*message);
|
||||
}
|
||||
DesktopWrapperMessage::Input(message) => {
|
||||
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
|
||||
}
|
||||
DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context {
|
||||
OpenFileDialogContext::Document => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||
FrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height });
|
||||
}
|
||||
FrontendMessage::UpdateUIScale { scale } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::UpdateUIScale { scale });
|
||||
return Some(FrontendMessage::UpdateUIScale { scale });
|
||||
}
|
||||
FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
|
||||
id: document_id,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ use std::path::PathBuf;
|
|||
|
||||
pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;
|
||||
|
||||
pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
||||
pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys};
|
||||
pub use graphite_editor::messages::prelude::InputPreprocessorMessage as InputMessage;
|
||||
|
||||
pub use graphite_editor::messages::prelude::DocumentId;
|
||||
pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences;
|
||||
pub enum DesktopFrontendMessage {
|
||||
|
|
@ -31,6 +35,9 @@ pub enum DesktopFrontendMessage {
|
|||
width: f64,
|
||||
height: f64,
|
||||
},
|
||||
UpdateUIScale {
|
||||
scale: f64,
|
||||
},
|
||||
UpdateOverlays(vello::Scene),
|
||||
PersistenceWriteDocument {
|
||||
id: DocumentId,
|
||||
|
|
@ -69,6 +76,7 @@ pub enum DesktopFrontendMessage {
|
|||
|
||||
pub enum DesktopWrapperMessage {
|
||||
FromWeb(Box<EditorMessage>),
|
||||
Input(InputMessage),
|
||||
OpenFileDialogResult {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
|
|
|
|||
|
|
@ -158,3 +158,8 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
|
|||
|
||||
// INPUT
|
||||
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
|
||||
|
||||
// UI
|
||||
pub const UI_SCALE_DEFAULT: f64 = 1.;
|
||||
pub const UI_SCALE_MIN: f64 = 0.5;
|
||||
pub const UI_SCALE_MAX: f64 = 3.;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::messages::debug::utility_types::MessageLoggingVerbosity;
|
|||
use crate::messages::defer::DeferMessageContext;
|
||||
use crate::messages::dialog::DialogMessageContext;
|
||||
use crate::messages::layout::layout_message_handler::LayoutMessageContext;
|
||||
use crate::messages::preferences::preferences_message_handler::PreferencesMessageContext;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
|
||||
|
||||
|
|
@ -51,7 +52,8 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
|||
NodeGraphMessageDiscriminant::RunDocumentGraph,
|
||||
))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
|
||||
];
|
||||
/// Since we don't need to update the frontend multiple times per frame,
|
||||
/// we have a set of messages which we will buffer until the next frame is requested.
|
||||
|
|
@ -68,6 +70,7 @@ const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[
|
|||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
|
||||
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::AutoSaveAllDocuments),
|
||||
];
|
||||
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
|
||||
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
|
||||
|
|
@ -178,7 +181,7 @@ impl Dispatcher {
|
|||
}
|
||||
Message::Frontend(message) => {
|
||||
// Handle these messages immediately by returning early
|
||||
if let FrontendMessage::TriggerFontLoad { .. } = message {
|
||||
if let FrontendMessage::TriggerFontDataLoad { .. } | FrontendMessage::TriggerFontCatalogLoad = message {
|
||||
self.responses.push(message);
|
||||
self.cleanup_queues(false);
|
||||
|
||||
|
|
@ -275,7 +278,11 @@ impl Dispatcher {
|
|||
menu_bar_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Preferences(message) => {
|
||||
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ());
|
||||
let context = PreferencesMessageContext {
|
||||
tool_message_handler: &self.message_handlers.tool_message_handler,
|
||||
};
|
||||
|
||||
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, context);
|
||||
}
|
||||
Message::Tool(message) => {
|
||||
let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else {
|
||||
|
|
@ -330,7 +337,7 @@ impl Dispatcher {
|
|||
if let Some(document) = self.message_handlers.portfolio_message_handler.active_document()
|
||||
&& !document.graph_view_overlay_open
|
||||
{
|
||||
list.extend(self.message_handlers.tool_message_handler.actions());
|
||||
list.extend(self.message_handlers.tool_message_handler.actions_with_preferences(&self.message_handlers.preferences_message_handler));
|
||||
}
|
||||
list.extend(self.message_handlers.portfolio_message_handler.actions());
|
||||
list
|
||||
|
|
@ -358,8 +365,9 @@ impl Dispatcher {
|
|||
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
|
||||
let discriminant = MessageDiscriminant::from(message);
|
||||
let is_blocked = DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
|
||||
let is_empty_batched = if let Message::Batched { messages } = message { messages.is_empty() } else { false };
|
||||
|
||||
if !is_blocked {
|
||||
if !is_blocked && !is_empty_batched {
|
||||
match message_logging_verbosity {
|
||||
MessageLoggingVerbosity::Off => {}
|
||||
MessageLoggingVerbosity::Names => {
|
||||
|
|
|
|||
|
|
@ -36,250 +36,278 @@ impl PreferencesDialogMessageHandler {
|
|||
const TITLE: &'static str = "Editor Preferences";
|
||||
|
||||
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
// ==========
|
||||
// NAVIGATION
|
||||
// ==========
|
||||
{
|
||||
let header = vec![TextLabel::new("Navigation").italic(true).widget_instance()];
|
||||
|
||||
let navigation_header = vec![TextLabel::new("Navigation").italic(true).widget_instance()];
|
||||
let zoom_rate_description = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50).";
|
||||
let zoom_rate_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(),
|
||||
];
|
||||
let zoom_rate = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
|
||||
.tooltip_label("Zoom Rate")
|
||||
.tooltip_description(zoom_rate_description)
|
||||
.mode_range()
|
||||
.int()
|
||||
.min(1.)
|
||||
.max(100.)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
if let Some(display_value) = number_input.value {
|
||||
let actual_rate = map_display_to_zoom_rate(display_value);
|
||||
PreferencesMessage::ViewportZoomWheelRate { rate: actual_rate }.into()
|
||||
} else {
|
||||
PreferencesMessage::ViewportZoomWheelRate { rate: VIEWPORT_ZOOM_WHEEL_RATE }.into()
|
||||
}
|
||||
})
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
let zoom_rate_description = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50).";
|
||||
let zoom_rate_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(),
|
||||
];
|
||||
let zoom_rate = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
|
||||
.tooltip_label("Zoom Rate")
|
||||
.tooltip_description(zoom_rate_description)
|
||||
.mode_range()
|
||||
.int()
|
||||
.min(1.)
|
||||
.max(100.)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
if let Some(display_value) = number_input.value {
|
||||
let actual_rate = map_display_to_zoom_rate(display_value);
|
||||
PreferencesMessage::ViewportZoomWheelRate { rate: actual_rate }.into()
|
||||
} else {
|
||||
PreferencesMessage::ViewportZoomWheelRate { rate: VIEWPORT_ZOOM_WHEEL_RATE }.into()
|
||||
}
|
||||
})
|
||||
.widget_instance(),
|
||||
];
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let zoom_with_scroll_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads).";
|
||||
let zoom_with_scroll = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.zoom_with_scroll)
|
||||
.tooltip_label("Zoom with Scroll")
|
||||
.tooltip_description(zoom_with_scroll_description)
|
||||
.on_update(|checkbox_input: &CheckboxInput| {
|
||||
PreferencesMessage::ModifyLayout {
|
||||
zoom_with_scroll: checkbox_input.checked,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Zoom with Scroll")
|
||||
.tooltip_label("Zoom with Scroll")
|
||||
.tooltip_description(zoom_with_scroll_description)
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let zoom_with_scroll_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads).";
|
||||
let zoom_with_scroll = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.zoom_with_scroll)
|
||||
.tooltip_label("Zoom with Scroll")
|
||||
.tooltip_description(zoom_with_scroll_description)
|
||||
.on_update(|checkbox_input: &CheckboxInput| {
|
||||
PreferencesMessage::ModifyLayout {
|
||||
zoom_with_scroll: checkbox_input.checked,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Zoom with Scroll")
|
||||
.tooltip_label("Zoom with Scroll")
|
||||
.tooltip_description(zoom_with_scroll_description)
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
rows.extend_from_slice(&[header, zoom_rate_label, zoom_rate, zoom_with_scroll]);
|
||||
}
|
||||
|
||||
// =======
|
||||
// EDITING
|
||||
// =======
|
||||
{
|
||||
let header = vec![TextLabel::new("Editing").italic(true).widget_instance()];
|
||||
|
||||
let editing_header = vec![TextLabel::new("Editing").italic(true).widget_instance()];
|
||||
let selection_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Selection")
|
||||
.tooltip_label("Selection")
|
||||
.tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.")
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
let selection_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Selection")
|
||||
.tooltip_label("Selection")
|
||||
.tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.")
|
||||
.widget_instance(),
|
||||
];
|
||||
let selection_mode = RadioInput::new(vec![
|
||||
RadioEntryData::new(SelectionMode::Touched.to_string())
|
||||
.label(SelectionMode::Touched.to_string())
|
||||
.tooltip_label(SelectionMode::Touched.to_string())
|
||||
.tooltip_description(SelectionMode::Touched.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Touched,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new(SelectionMode::Enclosed.to_string())
|
||||
.label(SelectionMode::Enclosed.to_string())
|
||||
.tooltip_label(SelectionMode::Enclosed.to_string())
|
||||
.tooltip_description(SelectionMode::Enclosed.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Enclosed,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new(SelectionMode::Directional.to_string())
|
||||
.label(SelectionMode::Directional.to_string())
|
||||
.tooltip_label(SelectionMode::Directional.to_string())
|
||||
.tooltip_description(SelectionMode::Directional.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Directional,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(preferences.selection_mode as u32))
|
||||
.widget_instance();
|
||||
let selection_mode = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
selection_mode,
|
||||
];
|
||||
|
||||
let selection_mode = RadioInput::new(vec![
|
||||
RadioEntryData::new(SelectionMode::Touched.to_string())
|
||||
.label(SelectionMode::Touched.to_string())
|
||||
.tooltip_label(SelectionMode::Touched.to_string())
|
||||
.tooltip_description(SelectionMode::Touched.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Touched,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new(SelectionMode::Enclosed.to_string())
|
||||
.label(SelectionMode::Enclosed.to_string())
|
||||
.tooltip_label(SelectionMode::Enclosed.to_string())
|
||||
.tooltip_description(SelectionMode::Enclosed.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Enclosed,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new(SelectionMode::Directional.to_string())
|
||||
.label(SelectionMode::Directional.to_string())
|
||||
.tooltip_label(SelectionMode::Directional.to_string())
|
||||
.tooltip_description(SelectionMode::Directional.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Directional,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(preferences.selection_mode as u32))
|
||||
.widget_instance();
|
||||
let selection_mode = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
selection_mode,
|
||||
];
|
||||
rows.extend_from_slice(&[header, selection_label, selection_mode]);
|
||||
}
|
||||
|
||||
// =========
|
||||
// INTERFACE
|
||||
// =========
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let header = vec![TextLabel::new("Interface").italic(true).widget_instance()];
|
||||
|
||||
let scale_description = "Adjust the scale of the entire user interface (100% is default).";
|
||||
let scale_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(),
|
||||
];
|
||||
let scale = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(ui_scale_to_display(preferences.ui_scale)))
|
||||
.tooltip_label("Scale")
|
||||
.tooltip_description(scale_description)
|
||||
.mode_range()
|
||||
.int()
|
||||
.min(ui_scale_to_display(crate::consts::UI_SCALE_MIN))
|
||||
.max(ui_scale_to_display(crate::consts::UI_SCALE_MAX))
|
||||
.unit("%")
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
if let Some(display_value) = number_input.value {
|
||||
let scale = map_display_to_ui_scale(display_value);
|
||||
PreferencesMessage::UIScale { scale }.into()
|
||||
} else {
|
||||
PreferencesMessage::UIScale {
|
||||
scale: crate::consts::UI_SCALE_DEFAULT,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
rows.extend_from_slice(&[header, scale_label, scale]);
|
||||
}
|
||||
|
||||
// ============
|
||||
// EXPERIMENTAL
|
||||
// ============
|
||||
{
|
||||
let header = vec![TextLabel::new("Experimental").italic(true).widget_instance()];
|
||||
|
||||
let experimental_header = vec![TextLabel::new("Experimental").italic(true).widget_instance()];
|
||||
let node_graph_section_description = "Configure the appearance of the wires running between node connections in the graph.";
|
||||
let node_graph_wires_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Node Graph Wires")
|
||||
.tooltip_label("Node Graph Wires")
|
||||
.tooltip_description(node_graph_section_description)
|
||||
.widget_instance(),
|
||||
];
|
||||
let graph_wire_style = RadioInput::new(vec![
|
||||
RadioEntryData::new(GraphWireStyle::Direct.to_string())
|
||||
.label(GraphWireStyle::Direct.to_string())
|
||||
.tooltip_label(GraphWireStyle::Direct.to_string())
|
||||
.tooltip_description(GraphWireStyle::Direct.tooltip_description())
|
||||
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::Direct }.into()),
|
||||
RadioEntryData::new(GraphWireStyle::GridAligned.to_string())
|
||||
.label(GraphWireStyle::GridAligned.to_string())
|
||||
.tooltip_label(GraphWireStyle::GridAligned.to_string())
|
||||
.tooltip_description(GraphWireStyle::GridAligned.tooltip_description())
|
||||
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::GridAligned }.into()),
|
||||
])
|
||||
.selected_index(Some(preferences.graph_wire_style as u32))
|
||||
.widget_instance();
|
||||
let graph_wire_style = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
graph_wire_style,
|
||||
];
|
||||
|
||||
let node_graph_section_description = "Configure the appearance of the wires running between node connections in the graph.";
|
||||
let node_graph_wires_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Node Graph Wires")
|
||||
.tooltip_label("Node Graph Wires")
|
||||
.tooltip_description(node_graph_section_description)
|
||||
.widget_instance(),
|
||||
];
|
||||
let graph_wire_style = RadioInput::new(vec![
|
||||
RadioEntryData::new(GraphWireStyle::Direct.to_string())
|
||||
.label(GraphWireStyle::Direct.to_string())
|
||||
.tooltip_label(GraphWireStyle::Direct.to_string())
|
||||
.tooltip_description(GraphWireStyle::Direct.tooltip_description())
|
||||
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::Direct }.into()),
|
||||
RadioEntryData::new(GraphWireStyle::GridAligned.to_string())
|
||||
.label(GraphWireStyle::GridAligned.to_string())
|
||||
.tooltip_label(GraphWireStyle::GridAligned.to_string())
|
||||
.tooltip_description(GraphWireStyle::GridAligned.tooltip_description())
|
||||
.on_update(move |_| PreferencesMessage::GraphWireStyle { style: GraphWireStyle::GridAligned }.into()),
|
||||
])
|
||||
.selected_index(Some(preferences.graph_wire_style as u32))
|
||||
.widget_instance();
|
||||
let graph_wire_style = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
graph_wire_style,
|
||||
];
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let vello_description = "Use the experimental Vello renderer instead of SVG-based rendering.".to_string();
|
||||
#[cfg(target_family = "wasm")]
|
||||
let mut vello_description = vello_description;
|
||||
#[cfg(target_family = "wasm")]
|
||||
vello_description.push_str("\n\n(Your browser must support WebGPU.)");
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let vello_description = "Use the experimental Vello renderer instead of SVG-based rendering.".to_string();
|
||||
#[cfg(target_family = "wasm")]
|
||||
let mut vello_description = vello_description;
|
||||
#[cfg(target_family = "wasm")]
|
||||
vello_description.push_str("\n\n(Your browser must support WebGPU.)");
|
||||
let use_vello = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
|
||||
.tooltip_label("Vello Renderer")
|
||||
.tooltip_description(vello_description.clone())
|
||||
.disabled(!preferences.supports_wgpu())
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Vello Renderer")
|
||||
.tooltip_label("Vello Renderer")
|
||||
.tooltip_description(vello_description)
|
||||
.disabled(!preferences.supports_wgpu())
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
let use_vello = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
|
||||
.tooltip_label("Vello Renderer")
|
||||
.tooltip_description(vello_description.clone())
|
||||
.disabled(!preferences.supports_wgpu())
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Vello Renderer")
|
||||
.tooltip_label("Vello Renderer")
|
||||
.tooltip_description(vello_description)
|
||||
.disabled(!preferences.supports_wgpu())
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let vector_mesh_description = "
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let vector_mesh_description = "
|
||||
Allow the Pen tool to produce branching geometry, where more than two segments may be connected to one anchor point.\n\
|
||||
\n\
|
||||
Currently, vector meshes do not properly render strokes (branching joins) and fills (multiple regions).
|
||||
"
|
||||
.trim();
|
||||
let vector_meshes = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.vector_meshes)
|
||||
.tooltip_label("Vector Meshes")
|
||||
.tooltip_description(vector_mesh_description)
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Vector Meshes")
|
||||
.tooltip_label("Vector Meshes")
|
||||
.tooltip_description(vector_mesh_description)
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
.trim();
|
||||
let vector_meshes = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.vector_meshes)
|
||||
.tooltip_label("Vector Meshes")
|
||||
.tooltip_description(vector_mesh_description)
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Vector Meshes")
|
||||
.tooltip_label("Vector Meshes")
|
||||
.tooltip_description(vector_mesh_description)
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let brush_tool_description = "
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let brush_tool_description = "
|
||||
Enable the Brush tool to support basic raster-based layer painting.\n\
|
||||
\n\
|
||||
This legacy tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will focus on raster graphics editing.
|
||||
This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will focus on raster graphics editing.\n\
|
||||
\n\
|
||||
Content created with the Brush tool may not be compatible with future versions of Graphite.
|
||||
"
|
||||
.trim();
|
||||
let brush_tool = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.brush_tool)
|
||||
.tooltip_label("Brush Tool")
|
||||
.tooltip_description(brush_tool_description)
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::BrushTool { enabled: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Brush Tool")
|
||||
.tooltip_label("Brush Tool")
|
||||
.tooltip_description(brush_tool_description)
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
.trim();
|
||||
let brush_tool = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.brush_tool)
|
||||
.tooltip_label("Brush Tool")
|
||||
.tooltip_description(brush_tool_description)
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::BrushTool { enabled: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Brush Tool")
|
||||
.tooltip_label("Brush Tool")
|
||||
.tooltip_description(brush_tool_description)
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
Layout(vec![
|
||||
// NAVIGATION
|
||||
LayoutGroup::Row { widgets: navigation_header },
|
||||
// Navigation: Zoom Rate
|
||||
LayoutGroup::Row { widgets: zoom_rate_label },
|
||||
LayoutGroup::Row { widgets: zoom_rate },
|
||||
// Navigation: Zoom with Scroll
|
||||
LayoutGroup::Row { widgets: zoom_with_scroll },
|
||||
//
|
||||
// EDITING
|
||||
LayoutGroup::Row { widgets: editing_header },
|
||||
// Editing: Selection
|
||||
LayoutGroup::Row { widgets: selection_label },
|
||||
LayoutGroup::Row { widgets: selection_mode },
|
||||
//
|
||||
// EXPERIMENTAL
|
||||
LayoutGroup::Row { widgets: experimental_header },
|
||||
// Experimental: Node Graph Wires
|
||||
LayoutGroup::Row { widgets: node_graph_wires_label },
|
||||
LayoutGroup::Row { widgets: graph_wire_style },
|
||||
// Experimental: Vello Renderer
|
||||
LayoutGroup::Row { widgets: use_vello },
|
||||
// Experimental: Vector Meshes
|
||||
LayoutGroup::Row { widgets: vector_meshes },
|
||||
// Experimental: Brush Tool
|
||||
LayoutGroup::Row { widgets: brush_tool },
|
||||
])
|
||||
rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, use_vello, vector_meshes, brush_tool]);
|
||||
}
|
||||
|
||||
Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect())
|
||||
}
|
||||
|
||||
pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
|
||||
|
|
@ -351,3 +379,15 @@ fn map_zoom_rate_to_display(rate: f64) -> f64 {
|
|||
let display = 50. + distance_from_reference;
|
||||
display.clamp(1., 100.).round()
|
||||
}
|
||||
|
||||
/// Maps display values in percent to actual ui scale.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn map_display_to_ui_scale(display: f64) -> f64 {
|
||||
display / 100.
|
||||
}
|
||||
|
||||
/// Maps actual ui scale back to display values in percent.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn ui_scale_to_display(scale: f64) -> f64 {
|
||||
scale * 100.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "fontSize")]
|
||||
font_size: f64,
|
||||
color: Color,
|
||||
url: String,
|
||||
#[serde(rename = "fontData")]
|
||||
font_data: Vec<u8>,
|
||||
transform: [f64; 6],
|
||||
#[serde(rename = "maxWidth")]
|
||||
max_width: Option<f64>,
|
||||
|
|
@ -47,6 +48,10 @@ pub enum FrontendMessage {
|
|||
max_height: Option<f64>,
|
||||
align: TextAlign,
|
||||
},
|
||||
DisplayEditableTextboxUpdateFontData {
|
||||
#[serde(rename = "fontData")]
|
||||
font_data: Vec<u8>,
|
||||
},
|
||||
DisplayEditableTextboxTransform {
|
||||
transform: [f64; 6],
|
||||
},
|
||||
|
|
@ -65,6 +70,9 @@ pub enum FrontendMessage {
|
|||
SendShortcutAltClick {
|
||||
shortcut: Option<ActionShortcut>,
|
||||
},
|
||||
SendShortcutShiftClick {
|
||||
shortcut: Option<ActionShortcut>,
|
||||
},
|
||||
|
||||
// Trigger prefix: cause a frontend specific API to do something
|
||||
TriggerAboutGraphiteLocalizedCommitDate {
|
||||
|
|
@ -92,8 +100,10 @@ pub enum FrontendMessage {
|
|||
name: String,
|
||||
filename: String,
|
||||
},
|
||||
TriggerFontLoad {
|
||||
TriggerFontCatalogLoad,
|
||||
TriggerFontDataLoad {
|
||||
font: Font,
|
||||
url: String,
|
||||
},
|
||||
TriggerImport,
|
||||
TriggerPersistenceRemoveDocument {
|
||||
|
|
@ -338,6 +348,9 @@ pub enum FrontendMessage {
|
|||
width: f64,
|
||||
height: f64,
|
||||
},
|
||||
UpdateUIScale {
|
||||
scale: f64,
|
||||
},
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
RenderOverlays {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
|||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::text::Font;
|
||||
use graphene_std::vector::style::{FillChoice, GradientStops};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -48,7 +47,6 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
|
|||
}
|
||||
LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value } => {
|
||||
self.handle_widget_callback(layout_target, widget_id, value, WidgetValueAction::Update, responses);
|
||||
responses.add(LayoutMessage::ResendActiveWidget { layout_target, widget_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -264,44 +262,6 @@ impl LayoutMessageHandler {
|
|||
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::FontInput(font_input) => {
|
||||
let callback_message = match action {
|
||||
WidgetValueAction::Commit => (font_input.on_commit.callback)(&()),
|
||||
WidgetValueAction::Update => {
|
||||
let Some(update_value) = value.as_object() else {
|
||||
error!("FontInput update was not of type: object");
|
||||
return;
|
||||
};
|
||||
let Some(font_family_value) = update_value.get("fontFamily") else {
|
||||
error!("FontInput update does not have a fontFamily");
|
||||
return;
|
||||
};
|
||||
let Some(font_style_value) = update_value.get("fontStyle") else {
|
||||
error!("FontInput update does not have a fontStyle");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(font_family) = font_family_value.as_str() else {
|
||||
error!("FontInput update fontFamily was not of type: string");
|
||||
return;
|
||||
};
|
||||
let Some(font_style) = font_style_value.as_str() else {
|
||||
error!("FontInput update fontStyle was not of type: string");
|
||||
return;
|
||||
};
|
||||
|
||||
font_input.font_family = font_family.into();
|
||||
font_input.font_style = font_style.into();
|
||||
|
||||
responses.add(PortfolioMessage::LoadFont {
|
||||
font: Font::new(font_family.into(), font_style.into()),
|
||||
});
|
||||
(font_input.on_update.callback)(font_input)
|
||||
}
|
||||
};
|
||||
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::IconButton(icon_button) => {
|
||||
let callback_message = match action {
|
||||
WidgetValueAction::Commit => (icon_button.on_commit.callback)(&()),
|
||||
|
|
|
|||
|
|
@ -353,43 +353,7 @@ impl From<Vec<WidgetInstance>> for LayoutGroup {
|
|||
}
|
||||
|
||||
impl LayoutGroup {
|
||||
/// Applies a tooltip label to all widgets in this row or column without a tooltip.
|
||||
pub fn with_tooltip_label(self, label: impl Into<String>) -> Self {
|
||||
let (is_col, mut widgets) = match self {
|
||||
LayoutGroup::Column { widgets } => (true, widgets),
|
||||
LayoutGroup::Row { widgets } => (false, widgets),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
let label = label.into();
|
||||
for widget in &mut widgets {
|
||||
let val = match &mut widget.widget {
|
||||
Widget::CheckboxInput(x) => &mut x.tooltip_label,
|
||||
Widget::ColorInput(x) => &mut x.tooltip_label,
|
||||
Widget::CurveInput(x) => &mut x.tooltip_label,
|
||||
Widget::DropdownInput(x) => &mut x.tooltip_label,
|
||||
Widget::FontInput(x) => &mut x.tooltip_label,
|
||||
Widget::IconButton(x) => &mut x.tooltip_label,
|
||||
Widget::IconLabel(x) => &mut x.tooltip_label,
|
||||
Widget::ImageButton(x) => &mut x.tooltip_label,
|
||||
Widget::ImageLabel(x) => &mut x.tooltip_label,
|
||||
Widget::NumberInput(x) => &mut x.tooltip_label,
|
||||
Widget::ParameterExposeButton(x) => &mut x.tooltip_label,
|
||||
Widget::PopoverButton(x) => &mut x.tooltip_label,
|
||||
Widget::TextAreaInput(x) => &mut x.tooltip_label,
|
||||
Widget::TextButton(x) => &mut x.tooltip_label,
|
||||
Widget::TextInput(x) => &mut x.tooltip_label,
|
||||
Widget::TextLabel(x) => &mut x.tooltip_label,
|
||||
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_label,
|
||||
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::ShortcutLabel(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
|
||||
};
|
||||
if val.is_empty() {
|
||||
val.clone_from(&label);
|
||||
}
|
||||
}
|
||||
if is_col { Self::Column { widgets } } else { Self::Row { widgets } }
|
||||
}
|
||||
|
||||
/// Applies a tooltip description to all widgets in this row or column without a tooltip.
|
||||
/// Applies a tooltip description to all widgets without a tooltip in this row or column.
|
||||
pub fn with_tooltip_description(self, description: impl Into<String>) -> Self {
|
||||
let (is_col, mut widgets) = match self {
|
||||
LayoutGroup::Column { widgets } => (true, widgets),
|
||||
|
|
@ -403,20 +367,24 @@ impl LayoutGroup {
|
|||
Widget::ColorInput(x) => &mut x.tooltip_description,
|
||||
Widget::CurveInput(x) => &mut x.tooltip_description,
|
||||
Widget::DropdownInput(x) => &mut x.tooltip_description,
|
||||
Widget::FontInput(x) => &mut x.tooltip_description,
|
||||
Widget::IconButton(x) => &mut x.tooltip_description,
|
||||
Widget::IconLabel(x) => &mut x.tooltip_description,
|
||||
Widget::ImageButton(x) => &mut x.tooltip_description,
|
||||
Widget::ImageLabel(x) => &mut x.tooltip_description,
|
||||
Widget::NumberInput(x) => &mut x.tooltip_description,
|
||||
Widget::ParameterExposeButton(x) => &mut x.tooltip_description,
|
||||
Widget::PopoverButton(x) => &mut x.tooltip_description,
|
||||
Widget::TextAreaInput(x) => &mut x.tooltip_description,
|
||||
Widget::TextButton(x) => &mut x.tooltip_description,
|
||||
Widget::TextInput(x) => &mut x.tooltip_description,
|
||||
Widget::TextLabel(x) => &mut x.tooltip_description,
|
||||
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_description,
|
||||
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::ShortcutLabel(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
|
||||
Widget::ReferencePointInput(_)
|
||||
| Widget::RadioInput(_)
|
||||
| Widget::Separator(_)
|
||||
| Widget::ShortcutLabel(_)
|
||||
| Widget::WorkingColorsInput(_)
|
||||
| Widget::NodeCatalog(_)
|
||||
| Widget::ParameterExposeButton(_) => continue,
|
||||
};
|
||||
if val.is_empty() {
|
||||
val.clone_from(&description);
|
||||
|
|
@ -727,7 +695,6 @@ pub enum Widget {
|
|||
ColorInput(ColorInput),
|
||||
CurveInput(CurveInput),
|
||||
DropdownInput(DropdownInput),
|
||||
FontInput(FontInput),
|
||||
IconButton(IconButton),
|
||||
IconLabel(IconLabel),
|
||||
ImageButton(ImageButton),
|
||||
|
|
@ -782,7 +749,6 @@ impl DiffUpdate {
|
|||
Widget::CheckboxInput(widget) => widget.tooltip_shortcut.as_mut(),
|
||||
Widget::ColorInput(widget) => widget.tooltip_shortcut.as_mut(),
|
||||
Widget::DropdownInput(widget) => widget.tooltip_shortcut.as_mut(),
|
||||
Widget::FontInput(widget) => widget.tooltip_shortcut.as_mut(),
|
||||
Widget::IconButton(widget) => widget.tooltip_shortcut.as_mut(),
|
||||
Widget::NumberInput(widget) => widget.tooltip_shortcut.as_mut(),
|
||||
Widget::ParameterExposeButton(widget) => widget.tooltip_shortcut.as_mut(),
|
||||
|
|
@ -838,10 +804,38 @@ impl DiffUpdate {
|
|||
(recursive_wrapper.0)(entry_sections, &recursive_wrapper)
|
||||
};
|
||||
|
||||
// Hash the menu list entry sections for caching purposes
|
||||
let hash_menu_list_entry_sections = |entry_sections: &MenuListEntrySections| {
|
||||
struct RecursiveHasher<'a> {
|
||||
hasher: DefaultHasher,
|
||||
hash_fn: &'a dyn Fn(&mut RecursiveHasher, &MenuListEntrySections),
|
||||
}
|
||||
let mut recursive_hasher = RecursiveHasher {
|
||||
hasher: DefaultHasher::new(),
|
||||
hash_fn: &|recursive_hasher, entry_sections| {
|
||||
for (index, entries) in entry_sections.iter().enumerate() {
|
||||
index.hash(&mut recursive_hasher.hasher);
|
||||
for entry in entries {
|
||||
entry.hash(&mut recursive_hasher.hasher);
|
||||
(recursive_hasher.hash_fn)(recursive_hasher, &entry.children);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
(recursive_hasher.hash_fn)(&mut recursive_hasher, entry_sections);
|
||||
recursive_hasher.hasher.finish()
|
||||
};
|
||||
|
||||
// Apply shortcut conversions to all widgets that have menu lists
|
||||
let convert_menu_lists = |widget_instance: &mut WidgetInstance| match &mut widget_instance.widget {
|
||||
Widget::DropdownInput(dropdown_input) => apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries),
|
||||
Widget::TextButton(text_button) => apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children),
|
||||
Widget::DropdownInput(dropdown_input) => {
|
||||
apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries);
|
||||
dropdown_input.entries_hash = hash_menu_list_entry_sections(&dropdown_input.entries);
|
||||
}
|
||||
Widget::TextButton(text_button) => {
|
||||
apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children);
|
||||
text_button.menu_list_children_hash = hash_menu_list_entry_sections(&text_button.menu_list_children);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,10 @@ pub struct TextButton {
|
|||
#[serde(rename = "menuListChildren")]
|
||||
pub menu_list_children: MenuListEntrySections,
|
||||
|
||||
#[serde(rename = "menuListChildrenHash")]
|
||||
#[widget_builder(skip)]
|
||||
pub menu_list_children_hash: u64,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ pub struct DropdownInput {
|
|||
#[widget_builder(constructor)]
|
||||
pub entries: MenuListEntrySections,
|
||||
|
||||
#[serde(rename = "entriesHash")]
|
||||
#[widget_builder(skip)]
|
||||
pub entries_hash: u64,
|
||||
|
||||
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
|
||||
#[serde(rename = "selectedIndex")]
|
||||
pub selected_index: Option<u32>,
|
||||
|
|
@ -94,6 +98,9 @@ pub struct DropdownInput {
|
|||
|
||||
pub narrow: bool,
|
||||
|
||||
#[serde(rename = "virtualScrolling")]
|
||||
pub virtual_scrolling: bool,
|
||||
|
||||
#[serde(rename = "tooltipLabel")]
|
||||
pub tooltip_label: String,
|
||||
|
||||
|
|
@ -142,6 +149,10 @@ pub struct MenuListEntry {
|
|||
|
||||
pub children: MenuListEntrySections,
|
||||
|
||||
#[serde(rename = "childrenHash")]
|
||||
#[widget_builder(skip)]
|
||||
pub children_hash: u64,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
|
@ -152,39 +163,13 @@ pub struct MenuListEntry {
|
|||
pub on_commit: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct FontInput {
|
||||
#[serde(rename = "fontFamily")]
|
||||
#[widget_builder(constructor)]
|
||||
pub font_family: String,
|
||||
|
||||
#[serde(rename = "fontStyle")]
|
||||
#[widget_builder(constructor)]
|
||||
pub font_style: String,
|
||||
|
||||
#[serde(rename = "isStyle")]
|
||||
pub is_style_picker: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
#[serde(rename = "tooltipLabel")]
|
||||
pub tooltip_label: String,
|
||||
|
||||
#[serde(rename = "tooltipDescription")]
|
||||
pub tooltip_description: String,
|
||||
|
||||
#[serde(rename = "tooltipShortcut")]
|
||||
pub tooltip_shortcut: Option<ActionShortcut>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<FontInput>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_commit: WidgetCallback<()>,
|
||||
impl std::hash::Hash for MenuListEntry {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.value.hash(state);
|
||||
self.label.hash(state);
|
||||
self.icon.hash(state);
|
||||
self.disabled.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
|
|||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
|
||||
use crate::messages::portfolio::utility_types::PanelType;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::portfolio::utility_types::{FontCatalog, PanelType, PersistentData};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
|
||||
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
|
||||
|
|
@ -36,6 +35,7 @@ use graphene_std::raster::BlendMode;
|
|||
use graphene_std::raster_types::Raster;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::Font;
|
||||
use graphene_std::vector::PointId;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
|
||||
|
|
@ -2171,54 +2171,28 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
|
||||
/// Loads all of the fonts in the document.
|
||||
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut fonts = HashSet::new();
|
||||
for (_node_id, node, _) in self.document_network().recursive_nodes() {
|
||||
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>, font_catalog: &FontCatalog) {
|
||||
let mut fonts_to_load = HashSet::new();
|
||||
|
||||
for (_, node, _) in self.document_network().recursive_nodes() {
|
||||
for input in &node.inputs {
|
||||
if let Some(TaggedValue::Font(font)) = input.as_value() {
|
||||
fonts.insert(font.clone());
|
||||
fonts_to_load.insert(font.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for font in fonts {
|
||||
responses.add_front(FrontendMessage::TriggerFontLoad { font });
|
||||
|
||||
for font in fonts_to_load {
|
||||
if let Some(style) = font_catalog.find_font_style_in_catalog(&font) {
|
||||
responses.add_front(FrontendMessage::TriggerFontDataLoad {
|
||||
font: Font::new(font.font_family, style.to_named_style()),
|
||||
url: style.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>, animation_is_playing: bool, time: Duration) {
|
||||
// // Document mode (dropdown menu at the left of the bar above the viewport, before the tool options)
|
||||
// let layout = Layout(vec![LayoutGroup::Row {
|
||||
// widgets: vec![
|
||||
// DropdownInput::new(
|
||||
// vec![vec![
|
||||
// MenuListEntry::new(format!("{:?}", DocumentMode::DesignMode))
|
||||
// .label(DocumentMode::DesignMode.to_string())
|
||||
// .icon(DocumentMode::DesignMode.icon_name()),
|
||||
// // TODO: See issue #330
|
||||
// MenuListEntry::new(format!("{:?}", DocumentMode::SelectMode))
|
||||
// .label(DocumentMode::SelectMode.to_string())
|
||||
// .icon(DocumentMode::SelectMode.icon_name())
|
||||
// .on_commit(|_| todo!()),
|
||||
// // TODO: See issue #331
|
||||
// MenuListEntry::new(format!("{:?}", DocumentMode::GuideMode))
|
||||
// .label(DocumentMode::GuideMode.to_string())
|
||||
// .icon(DocumentMode::GuideMode.icon_name())
|
||||
// .on_commit(|_| todo!()),
|
||||
// ]])
|
||||
// .selected_index(Some(self.document_mode as u32))
|
||||
// .draw_icon(true)
|
||||
// .interactive(false)
|
||||
// .widget_instance(),
|
||||
// Separator::new(SeparatorType::Section).widget_instance(),
|
||||
// ],
|
||||
// }]);
|
||||
// responses.add(LayoutMessage::SendLayout {
|
||||
// layout,
|
||||
// layout_target: LayoutTarget::DocumentMode,
|
||||
// });
|
||||
|
||||
// Document bar (right portion of the bar above the viewport)
|
||||
|
||||
let mut snapping_state = self.snapping_state.clone();
|
||||
let mut snapping_state2 = self.snapping_state.clone();
|
||||
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
..Default::default()
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed("Merges new content as an entry into the graphic table that represents a layer compositing stack."),
|
||||
description: Cow::Borrowed("Merges the provided content as a new element in the graphic table that represents a layer compositing stack."),
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use super::document_node_definitions::{NODE_OVERRIDES, NodePropertiesContext};
|
|||
use super::utility_types::FrontendGraphDataType;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::portfolio::utility_types::{FontCatalogStyle, PersistentData};
|
||||
use crate::messages::prelude::*;
|
||||
use choice::enum_choice;
|
||||
use dyn_any::DynAny;
|
||||
|
|
@ -34,7 +35,7 @@ pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
|||
fn optionally_update_value<T>(value: impl Fn(&T) -> Option<TaggedValue> + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync {
|
||||
move |input_value: &T| match value(input_value) {
|
||||
Some(value) => NodeGraphMessage::SetInputValue { node_id, input_index, value }.into(),
|
||||
_ => Message::NoOp,
|
||||
None => Message::NoOp,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +87,7 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<Widget
|
|||
input_type,
|
||||
blank_assist,
|
||||
exposable,
|
||||
..
|
||||
} = parameter_widgets_info;
|
||||
|
||||
let Some(document_node) = document_node else {
|
||||
|
|
@ -759,36 +761,141 @@ pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_p
|
|||
}
|
||||
|
||||
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
let ParameterWidgetsInfo {
|
||||
persistent_data,
|
||||
document_node,
|
||||
node_id,
|
||||
index,
|
||||
..
|
||||
} = parameter_widgets_info;
|
||||
|
||||
let mut first_widgets = start_widgets(parameter_widgets_info);
|
||||
let mut second_widgets = None;
|
||||
|
||||
let from_font_input = |font: &FontInput| TaggedValue::Font(Font::new(font.font_family.clone(), font.font_style.clone()));
|
||||
|
||||
let Some(document_node) = document_node else { return (Vec::new(), None) };
|
||||
let Some(input) = document_node.inputs.get(index) else {
|
||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
return (vec![], None);
|
||||
};
|
||||
|
||||
if let Some(TaggedValue::Font(font)) = &input.as_non_exposed_value() {
|
||||
first_widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
FontInput::new(font.font_family.clone(), font.font_style.clone())
|
||||
.on_update(update_value(from_font_input, node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_instance(),
|
||||
DropdownInput::new(vec![
|
||||
persistent_data
|
||||
.font_catalog
|
||||
.0
|
||||
.iter()
|
||||
.map(|family| {
|
||||
MenuListEntry::new(family.name.clone())
|
||||
.label(family.name.clone())
|
||||
.font(family.closest_style(400, false).preview_url(&family.name))
|
||||
.on_update({
|
||||
// Construct the new font using the new family and the initial or previous style, although this style might not exist in the catalog
|
||||
let mut new_font = Font::new(family.name.clone(), font.font_style_to_restore.clone().unwrap_or_else(|| font.font_style.clone()));
|
||||
new_font.font_style_to_restore = font.font_style_to_restore.clone();
|
||||
|
||||
// If not already, store the initial style so it can be restored if the user switches to another family
|
||||
if new_font.font_style_to_restore.is_none() {
|
||||
new_font.font_style_to_restore = Some(new_font.font_style.clone());
|
||||
}
|
||||
|
||||
// Use the closest style available in the family for the new font to ensure the style exists
|
||||
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&new_font.font_style, "");
|
||||
new_font.font_style = family.closest_style(weight, italic).to_named_style();
|
||||
|
||||
move |_| {
|
||||
let new_font = new_font.clone();
|
||||
|
||||
Message::Batched {
|
||||
messages: Box::new([
|
||||
PortfolioMessage::LoadFontData { font: new_font.clone() }.into(),
|
||||
update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()),
|
||||
]),
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_commit({
|
||||
// Use the new value from the user selection
|
||||
let font_family = family.name.clone();
|
||||
|
||||
// Use the previous style selection and extract its weight and italic properties, then find the closest style in the new family
|
||||
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
|
||||
let font_style = family.closest_style(weight, italic).to_named_style();
|
||||
|
||||
move |_| {
|
||||
let new_font = Font::new(font_family.clone(), font_style.clone());
|
||||
|
||||
DeferMessage::AfterGraphRun {
|
||||
messages: vec![update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()), commit_value(&())],
|
||||
}
|
||||
.into()
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
])
|
||||
.selected_index(persistent_data.font_catalog.0.iter().position(|family| family.name == font.font_family).map(|i| i as u32))
|
||||
.virtual_scrolling(true)
|
||||
.widget_instance(),
|
||||
]);
|
||||
|
||||
let mut second_row = vec![TextLabel::new("").widget_instance()];
|
||||
add_blank_assist(&mut second_row);
|
||||
second_row.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
FontInput::new(font.font_family.clone(), font.font_style.clone())
|
||||
.is_style_picker(true)
|
||||
.on_update(update_value(from_font_input, node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_instance(),
|
||||
DropdownInput::new({
|
||||
persistent_data
|
||||
.font_catalog
|
||||
.0
|
||||
.iter()
|
||||
.find(|family| family.name == font.font_family)
|
||||
.map(|family| {
|
||||
let build_entry = |style: &FontCatalogStyle| {
|
||||
let font_style = style.to_named_style();
|
||||
MenuListEntry::new(font_style.clone())
|
||||
.label(font_style.clone())
|
||||
.on_update({
|
||||
let font_family = font.font_family.clone();
|
||||
let font_style = font_style.clone();
|
||||
|
||||
move |_| {
|
||||
// Keep the existing family
|
||||
let new_font = Font::new(font_family.clone(), font_style.clone());
|
||||
|
||||
Message::Batched {
|
||||
messages: Box::new([
|
||||
PortfolioMessage::LoadFontData { font: new_font.clone() }.into(),
|
||||
update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()),
|
||||
]),
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_commit(commit_value)
|
||||
};
|
||||
|
||||
vec![
|
||||
family.styles.iter().filter(|style| !style.italic).map(build_entry).collect::<Vec<_>>(),
|
||||
family.styles.iter().filter(|style| style.italic).map(build_entry).collect::<Vec<_>>(),
|
||||
]
|
||||
})
|
||||
.filter(|styles| !styles.is_empty())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.selected_index(
|
||||
persistent_data
|
||||
.font_catalog
|
||||
.0
|
||||
.iter()
|
||||
.find(|family| family.name == font.font_family)
|
||||
.and_then(|family| {
|
||||
let not_italic = family.styles.iter().filter(|style| !style.italic);
|
||||
let italic = family.styles.iter().filter(|style| style.italic);
|
||||
not_italic.chain(italic).position(|style| style.to_named_style() == font.font_style)
|
||||
})
|
||||
.map(|i| i as u32),
|
||||
)
|
||||
.widget_instance(),
|
||||
]);
|
||||
second_widgets = Some(second_row);
|
||||
}
|
||||
|
|
@ -2033,6 +2140,7 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
|
|||
}
|
||||
|
||||
pub struct ParameterWidgetsInfo<'a> {
|
||||
persistent_data: &'a PersistentData,
|
||||
document_node: Option<&'a DocumentNode>,
|
||||
node_id: NodeId,
|
||||
index: usize,
|
||||
|
|
@ -2048,11 +2156,12 @@ impl<'a> ParameterWidgetsInfo<'a> {
|
|||
let (name, description) = context.network_interface.displayed_input_name_and_description(&node_id, index, context.selection_network_path);
|
||||
let input_type = context
|
||||
.network_interface
|
||||
.input_type(&InputConnector::node(node_id, index), context.selection_network_path)
|
||||
.input_type_not_invalid(&InputConnector::node(node_id, index), context.selection_network_path)
|
||||
.displayed_type();
|
||||
let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
|
||||
|
||||
ParameterWidgetsInfo {
|
||||
persistent_data: context.persistent_data,
|
||||
document_node,
|
||||
node_id,
|
||||
index,
|
||||
|
|
@ -2227,7 +2336,7 @@ pub mod choice {
|
|||
|
||||
let mut row = LayoutGroup::Row { widgets };
|
||||
if let Some(desc) = self.widget_factory.description() {
|
||||
row = row.with_tooltip_label(desc);
|
||||
row = row.with_tooltip_description(desc);
|
||||
}
|
||||
row
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ pub static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
|
|||
// Initialize with the hardcoded font used by overlay text
|
||||
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
|
||||
font_cache.insert(font, String::new(), FONT_DATA.to_vec());
|
||||
font_cache.insert(font, FONT_DATA.to_vec());
|
||||
font_cache
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -804,34 +804,32 @@ impl OverlayContextInternal {
|
|||
|
||||
let transform = self.get_transform();
|
||||
|
||||
// Draw the background circle with a white fill and colored outline
|
||||
let circle = kurbo::Circle::new((x, y), DOWEL_PIN_RADIUS);
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle);
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(color), None, &circle);
|
||||
|
||||
// Draw the two filled sectors using paths
|
||||
let mut path = BezPath::new();
|
||||
|
||||
// Top-left sector
|
||||
let start1 = FRAC_PI_2 + angle;
|
||||
let start1_x = x + DOWEL_PIN_RADIUS * start1.cos();
|
||||
let start1_y = y + DOWEL_PIN_RADIUS * start1.sin();
|
||||
path.move_to(kurbo::Point::new(x, y));
|
||||
let end_x = x + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle.cos());
|
||||
let end_y = y + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle.sin());
|
||||
path.line_to(kurbo::Point::new(end_x, end_y));
|
||||
// Draw arc manually
|
||||
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
|
||||
arc.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.line_to(kurbo::Point::new(start1_x, start1_y));
|
||||
|
||||
let arc1 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start1, FRAC_PI_2, 0.0);
|
||||
arc1.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.curve_to(p1, p2, p);
|
||||
});
|
||||
path.close_path();
|
||||
|
||||
// Bottom-right sector
|
||||
let start2 = PI + FRAC_PI_2 + angle;
|
||||
let start2_x = x + DOWEL_PIN_RADIUS * start2.cos();
|
||||
let start2_y = y + DOWEL_PIN_RADIUS * start2.sin();
|
||||
path.move_to(kurbo::Point::new(x, y));
|
||||
let end_x = x + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle.cos());
|
||||
let end_y = y + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle.sin());
|
||||
path.line_to(kurbo::Point::new(end_x, end_y));
|
||||
// Draw arc manually
|
||||
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), PI + FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
|
||||
arc.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.line_to(kurbo::Point::new(start2_x, start2_y));
|
||||
|
||||
let arc2 = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), start2, FRAC_PI_2, 0.0);
|
||||
arc2.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.curve_to(p1, p2, p);
|
||||
});
|
||||
path.close_path();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
|
|||
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::proto::{GraphErrorType, GraphErrors};
|
||||
use graph_craft::{Type, concrete};
|
||||
use graphene_std::uuid::NodeId;
|
||||
use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta};
|
||||
|
|
@ -129,7 +129,14 @@ impl NodeNetworkInterface {
|
|||
InputConnector::Export(_) => false,
|
||||
})
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(_) => self.resolved_types.node_graph_errors.iter().any(|error| error.node_path == node_path),
|
||||
DocumentNodeImplementation::ProtoNode(_) => self.resolved_types.node_graph_errors.iter().any(|error| {
|
||||
error.node_path == node_path
|
||||
&& match &error.error {
|
||||
GraphErrorType::InvalidImplementations { error_inputs, .. } => error_inputs.iter().any(|solution| solution.iter().any(|(index, _)| index == input_index)),
|
||||
_ => true,
|
||||
}
|
||||
}),
|
||||
|
||||
DocumentNodeImplementation::Extract => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -137,7 +144,7 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
}
|
||||
|
||||
fn input_type_not_invalid(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
pub fn input_type_not_invalid(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
let Some(input) = self.input_from_connector(input_connector, network_path) else {
|
||||
return TypeSource::Error("Could not get input from connector");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
|||
use super::utility_types::PanelType;
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::utility_types::FontCatalog;
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
|
|
@ -46,19 +47,21 @@ pub enum PortfolioMessage {
|
|||
},
|
||||
DestroyAllDocuments,
|
||||
EditorPreferences,
|
||||
FontCatalogLoaded {
|
||||
catalog: FontCatalog,
|
||||
},
|
||||
LoadFontData {
|
||||
font: Font,
|
||||
},
|
||||
FontLoaded {
|
||||
font_family: String,
|
||||
font_style: String,
|
||||
preview_url: String,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
Import,
|
||||
LoadDocumentResources {
|
||||
document_id: DocumentId,
|
||||
},
|
||||
LoadFont {
|
||||
font: Font,
|
||||
},
|
||||
NewDocumentWithName {
|
||||
name: String,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
responses.add(FrontendMessage::SendShortcutAltClick {
|
||||
shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft),
|
||||
});
|
||||
responses.add(FrontendMessage::SendShortcutShiftClick {
|
||||
shortcut: action_shortcut_manual!(Key::Shift, Key::MouseLeft),
|
||||
});
|
||||
|
||||
// Before loading any documents, initially prepare the welcome screen buttons layout
|
||||
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
|
||||
|
|
@ -316,7 +319,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
if let Some(active_document) = self.active_document()
|
||||
&& active_document.graph_view_overlay_open()
|
||||
{
|
||||
responses.add(NodeGraphMessage::Copy);
|
||||
responses.add(NodeGraphMessage::Cut);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -349,16 +352,31 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
self.active_document_id = None;
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
PortfolioMessage::FontLoaded {
|
||||
font_family,
|
||||
font_style,
|
||||
preview_url,
|
||||
data,
|
||||
} => {
|
||||
let font = Font::new(font_family, font_style);
|
||||
PortfolioMessage::FontCatalogLoaded { catalog } => {
|
||||
self.persistent_data.font_catalog = catalog;
|
||||
|
||||
self.persistent_data.font_cache.insert(font, preview_url, data);
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
|
||||
}
|
||||
|
||||
// Load the default font
|
||||
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into());
|
||||
responses.add(PortfolioMessage::LoadFontData { font });
|
||||
}
|
||||
PortfolioMessage::LoadFontData { font } => {
|
||||
if let Some(style) = self.persistent_data.font_catalog.find_font_style_in_catalog(&font) {
|
||||
let font = Font::new(font.font_family, style.to_named_style());
|
||||
|
||||
if !self.persistent_data.font_cache.loaded_font(&font) {
|
||||
responses.add(FrontendMessage::TriggerFontDataLoad { font, url: style.url });
|
||||
}
|
||||
}
|
||||
}
|
||||
PortfolioMessage::FontLoaded { font_family, font_style, data } => {
|
||||
let font = Font::new(font_family, font_style);
|
||||
self.persistent_data.font_cache.insert(font, data);
|
||||
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
|
||||
|
||||
for document_id in self.document_ids.iter() {
|
||||
let node_to_inspect = self.node_to_inspect();
|
||||
|
||||
|
|
@ -382,6 +400,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
if self.active_document_mut().is_some() {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
if current_tool == &ToolType::Text {
|
||||
responses.add(TextToolMessage::RefreshEditingFontData);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
|
||||
PortfolioMessage::Import => {
|
||||
|
|
@ -389,13 +411,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
responses.add(FrontendMessage::TriggerImport);
|
||||
}
|
||||
PortfolioMessage::LoadDocumentResources { document_id } => {
|
||||
if let Some(document) = self.document_mut(document_id) {
|
||||
document.load_layer_resources(responses);
|
||||
let catalog = &self.persistent_data.font_catalog;
|
||||
|
||||
if catalog.0.is_empty() {
|
||||
log::error!("Tried to load document resources before font catalog was loaded");
|
||||
}
|
||||
}
|
||||
PortfolioMessage::LoadFont { font } => {
|
||||
if !self.persistent_data.font_cache.loaded_font(&font) {
|
||||
responses.add_front(FrontendMessage::TriggerFontLoad { font });
|
||||
|
||||
if let Some(document) = self.documents.get_mut(&document_id) {
|
||||
document.load_layer_resources(responses, catalog);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::NewDocumentWithName { name } => {
|
||||
|
|
@ -592,7 +615,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
added_nodes = true;
|
||||
}
|
||||
|
||||
document.load_layer_resources(responses);
|
||||
document.load_layer_resources(responses, &self.persistent_data.font_catalog);
|
||||
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
||||
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
|
||||
all_new_ids.extend(new_ids.values().cloned());
|
||||
|
|
@ -973,7 +996,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
};
|
||||
if !document.is_loaded {
|
||||
document.is_loaded = true;
|
||||
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
|
||||
if self.persistent_data.font_catalog.0.is_empty() {
|
||||
responses.add_front(FrontendMessage::TriggerFontCatalogLoad);
|
||||
}
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
}
|
||||
|
|
@ -1229,10 +1254,6 @@ impl PortfolioMessageHandler {
|
|||
if self.active_document().is_some() {
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(ToolMessage::DeactivateTools);
|
||||
} else {
|
||||
// Load the default font upon creating the first document
|
||||
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into());
|
||||
responses.add(FrontendMessage::TriggerFontLoad { font });
|
||||
}
|
||||
|
||||
// TODO: Remove this and find a way to fix the issue where creating a new document when the node graph is open causes the transform in the new document to be incorrect
|
||||
|
|
|
|||
|
|
@ -1,11 +1,87 @@
|
|||
use graphene_std::text::FontCache;
|
||||
use graphene_std::text::{Font, FontCache};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PersistentData {
|
||||
pub font_cache: FontCache,
|
||||
pub font_catalog: FontCatalog,
|
||||
pub use_vello: bool,
|
||||
}
|
||||
|
||||
// TODO: Should this be a BTreeMap instead?
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FontCatalog(pub Vec<FontCatalogFamily>);
|
||||
|
||||
impl FontCatalog {
|
||||
pub fn find_font_style_in_catalog(&self, font: &Font) -> Option<FontCatalogStyle> {
|
||||
let family = self.0.iter().find(|family| family.name == font.font_family);
|
||||
|
||||
let found_style = family.map(|family| {
|
||||
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
|
||||
family.closest_style(weight, italic).clone()
|
||||
});
|
||||
|
||||
if found_style.is_none() {
|
||||
log::warn!("Font not found in catalog: {:?}", font);
|
||||
}
|
||||
|
||||
found_style
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FontCatalogFamily {
|
||||
/// The font family name.
|
||||
pub name: String,
|
||||
/// The font styles (variants) available for the font family.
|
||||
pub styles: Vec<FontCatalogStyle>,
|
||||
}
|
||||
|
||||
impl FontCatalogFamily {
|
||||
/// Finds the closest style to the given weight and italic setting.
|
||||
/// Aims to find the nearest weight while maintaining the italic setting if possible, but italic may change if no other option is available.
|
||||
pub fn closest_style(&self, weight: u32, italic: bool) -> &FontCatalogStyle {
|
||||
self.styles
|
||||
.iter()
|
||||
.map(|style| ((style.weight as i32 - weight as i32).unsigned_abs() + 10000 * (style.italic != italic) as u32, style))
|
||||
.min_by_key(|(distance, _)| *distance)
|
||||
.map(|(_, style)| style)
|
||||
.unwrap_or(&self.styles[0])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FontCatalogStyle {
|
||||
pub weight: u32,
|
||||
pub italic: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl FontCatalogStyle {
|
||||
pub fn to_named_style(&self) -> String {
|
||||
let weight = self.weight;
|
||||
let italic = self.italic;
|
||||
|
||||
let named_weight = Font::named_weight(weight);
|
||||
let maybe_italic = if italic { " Italic" } else { "" };
|
||||
|
||||
format!("{named_weight}{maybe_italic} ({weight})")
|
||||
}
|
||||
|
||||
pub fn from_named_style(named_style: &str, url: impl Into<String>) -> FontCatalogStyle {
|
||||
let weight = named_style.split_terminator(['(', ')']).next_back().and_then(|x| x.parse::<u32>().ok()).unwrap_or(400);
|
||||
let italic = named_style.contains("Italic (");
|
||||
FontCatalogStyle { weight, italic, url: url.into() }
|
||||
}
|
||||
|
||||
/// Get the URL for the stylesheet for loading a font preview for this style of the given family name, subsetted to only the letters in the family name.
|
||||
pub fn preview_url(&self, family: impl Into<String>) -> String {
|
||||
let name = family.into().replace(' ', "+");
|
||||
let italic = if self.italic { "ital," } else { "" };
|
||||
let weight = self.weight;
|
||||
format!("https://fonts.googleapis.com/css2?display=swap&family={name}:{italic}wght@{weight}&text={name}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Platform {
|
||||
#[default]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
mod preferences_message;
|
||||
mod preferences_message_handler;
|
||||
pub mod preferences_message_handler;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ pub enum PreferencesMessage {
|
|||
ModifyLayout { zoom_with_scroll: bool },
|
||||
GraphWireStyle { style: GraphWireStyle },
|
||||
ViewportZoomWheelRate { rate: f64 },
|
||||
UIScale { scale: f64 },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
use crate::consts::VIEWPORT_ZOOM_WHEEL_RATE;
|
||||
use crate::consts::{UI_SCALE_DEFAULT, VIEWPORT_ZOOM_WHEEL_RATE};
|
||||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct PreferencesMessageContext<'a> {
|
||||
pub tool_message_handler: &'a ToolMessageHandler,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)]
|
||||
#[serde(default)]
|
||||
pub struct PreferencesMessageHandler {
|
||||
pub selection_mode: SelectionMode,
|
||||
pub zoom_with_scroll: bool,
|
||||
|
|
@ -14,6 +21,7 @@ pub struct PreferencesMessageHandler {
|
|||
pub brush_tool: bool,
|
||||
pub graph_wire_style: GraphWireStyle,
|
||||
pub viewport_zoom_wheel_rate: f64,
|
||||
pub ui_scale: f64,
|
||||
}
|
||||
|
||||
impl PreferencesMessageHandler {
|
||||
|
|
@ -42,13 +50,16 @@ impl Default for PreferencesMessageHandler {
|
|||
brush_tool: false,
|
||||
graph_wire_style: GraphWireStyle::default(),
|
||||
viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE,
|
||||
ui_scale: UI_SCALE_DEFAULT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
||||
fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||
impl MessageHandler<PreferencesMessage, PreferencesMessageContext<'_>> for PreferencesMessageHandler {
|
||||
fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, context: PreferencesMessageContext) {
|
||||
let PreferencesMessageContext { tool_message_handler } = context;
|
||||
|
||||
match message {
|
||||
// Management messages
|
||||
PreferencesMessage::Load { preferences } => {
|
||||
|
|
@ -61,6 +72,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
|||
responses.add(PreferencesMessage::ModifyLayout {
|
||||
zoom_with_scroll: self.zoom_with_scroll,
|
||||
});
|
||||
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
|
||||
}
|
||||
PreferencesMessage::ResetToDefaults => {
|
||||
refresh_dialog(responses);
|
||||
|
|
@ -80,6 +92,11 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
|||
}
|
||||
PreferencesMessage::BrushTool { enabled } => {
|
||||
self.brush_tool = enabled;
|
||||
|
||||
if !enabled && tool_message_handler.tool_state.tool_data.active_tool_type == ToolType::Brush {
|
||||
responses.add(ToolMessage::ActivateToolSelect);
|
||||
}
|
||||
|
||||
responses.add(ToolMessage::RefreshToolShelf);
|
||||
}
|
||||
PreferencesMessage::ModifyLayout { zoom_with_scroll } => {
|
||||
|
|
@ -99,6 +116,10 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
|||
PreferencesMessage::ViewportZoomWheelRate { rate } => {
|
||||
self.viewport_zoom_wheel_rate = rate;
|
||||
}
|
||||
PreferencesMessage::UIScale { scale } => {
|
||||
self.ui_scale = scale;
|
||||
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ pub struct PivotGizmoState {
|
|||
|
||||
impl PivotGizmoState {
|
||||
pub fn is_pivot_type(&self) -> bool {
|
||||
// A disabled pivot is considered a pivot-type gizmo that is always centered
|
||||
self.gizmo_type == PivotGizmoType::Pivot || self.disabled
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
preferences,
|
||||
viewport,
|
||||
} = context;
|
||||
let font_cache = &persistent_data.font_cache;
|
||||
|
||||
match message {
|
||||
// Messages
|
||||
|
|
@ -122,11 +121,11 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
document_id,
|
||||
global_tool_data: &self.tool_state.document_tool_data,
|
||||
input,
|
||||
font_cache,
|
||||
shape_editor: &mut self.shape_editor,
|
||||
node_graph,
|
||||
preferences,
|
||||
viewport,
|
||||
persistent_data,
|
||||
};
|
||||
|
||||
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
|
||||
|
|
@ -217,7 +216,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
tool_data.tools.get(active_tool).unwrap().activate(responses);
|
||||
|
||||
// Register initial properties
|
||||
tool_data.tools.get(active_tool).unwrap().send_layout(responses, LayoutTarget::ToolOptions);
|
||||
tool_data.tools.get(active_tool).unwrap().refresh_options(responses, persistent_data);
|
||||
|
||||
// Notify the frontend about the initial active tool
|
||||
tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool);
|
||||
|
|
@ -230,11 +229,11 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
document_id,
|
||||
global_tool_data: &self.tool_state.document_tool_data,
|
||||
input,
|
||||
font_cache,
|
||||
shape_editor: &mut self.shape_editor,
|
||||
node_graph,
|
||||
preferences,
|
||||
viewport,
|
||||
persistent_data,
|
||||
};
|
||||
|
||||
// Set initial hints and cursor
|
||||
|
|
@ -257,7 +256,8 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
}
|
||||
ToolMessage::RefreshToolOptions => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().send_layout(responses, LayoutTarget::ToolOptions);
|
||||
|
||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, persistent_data);
|
||||
}
|
||||
ToolMessage::RefreshToolShelf => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
|
|
@ -341,11 +341,11 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
document_id,
|
||||
global_tool_data: &self.tool_state.document_tool_data,
|
||||
input,
|
||||
font_cache,
|
||||
shape_editor: &mut self.shape_editor,
|
||||
node_graph,
|
||||
preferences,
|
||||
viewport,
|
||||
persistent_data,
|
||||
};
|
||||
if matches!(tool_message, ToolMessage::UpdateHints) {
|
||||
if graph_view_overlay_open {
|
||||
|
|
@ -384,8 +384,6 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
ActivateToolShape,
|
||||
ActivateToolText,
|
||||
|
||||
ActivateToolBrush,
|
||||
|
||||
ToggleSelectVsPath,
|
||||
|
||||
SelectRandomWorkingColor,
|
||||
|
|
@ -400,3 +398,17 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
list
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolMessageHandler {
|
||||
pub fn actions_with_preferences(&self, preferences: &PreferencesMessageHandler) -> ActionList {
|
||||
let mut list = self.actions();
|
||||
|
||||
if preferences.brush_tool {
|
||||
list.extend(actions!(ToolMessageDiscriminant;
|
||||
ActivateToolBrush,
|
||||
));
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1575,6 +1575,10 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
shape_editor.set_selected_layers(target_layers);
|
||||
|
||||
let new_state = make_path_editable_is_allowed(&mut document.network_interface).is_some();
|
||||
if tool_data.make_path_editable_is_allowed != new_state {
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
|
|
@ -3125,8 +3129,14 @@ impl Fsm for PathToolFsmState {
|
|||
colinear,
|
||||
};
|
||||
|
||||
let old = tool_data.make_path_editable_is_allowed;
|
||||
tool_data.make_path_editable_is_allowed = make_path_editable_is_allowed(&mut document.network_interface).is_some();
|
||||
tool_data.update_selection_status(shape_editor, document);
|
||||
|
||||
if old != tool_data.make_path_editable_is_allowed {
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::ManipulatorMakeHandlesColinear) => {
|
||||
|
|
|
|||
|
|
@ -400,6 +400,7 @@ struct SelectToolData {
|
|||
selected_layers_changed: bool,
|
||||
snap_candidates: Vec<SnapCandidatePoint>,
|
||||
auto_panning: AutoPanning,
|
||||
drag_start_center: ViewportPosition,
|
||||
}
|
||||
|
||||
impl SelectToolData {
|
||||
|
|
@ -599,7 +600,7 @@ impl Fsm for SelectToolFsmState {
|
|||
document,
|
||||
input,
|
||||
viewport,
|
||||
font_cache,
|
||||
persistent_data,
|
||||
..
|
||||
} = tool_action_data;
|
||||
|
||||
|
|
@ -624,7 +625,7 @@ impl Fsm for SelectToolFsmState {
|
|||
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None);
|
||||
|
||||
if is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text") {
|
||||
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, font_cache);
|
||||
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &persistent_data.font_cache);
|
||||
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
|
||||
}
|
||||
}
|
||||
|
|
@ -911,11 +912,10 @@ impl Fsm for SelectToolFsmState {
|
|||
let angle = -mouse_position.angle_to(DVec2::X);
|
||||
let snapped_angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
|
||||
let extension = tool_data.drag_current - tool_data.drag_start;
|
||||
let origin = compass_center - extension;
|
||||
let origin = tool_data.drag_start_center;
|
||||
let viewport_diagonal = viewport.size().into_dvec2().length();
|
||||
|
||||
let edge = DVec2::from_angle(snapped_angle).normalize_or(DVec2::X) * viewport_diagonal;
|
||||
let edge = DVec2::from_angle(snapped_angle).normalize_or(DVec2::X);
|
||||
let perp = edge.perp();
|
||||
|
||||
let (edge_color, perp_color) = if edge.x.abs() > edge.y.abs() {
|
||||
|
|
@ -1032,6 +1032,8 @@ impl Fsm for SelectToolFsmState {
|
|||
let position = tool_data.pivot_gizmo().position(document);
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, Some(position));
|
||||
|
||||
tool_data.drag_start_center = position;
|
||||
|
||||
// If the user is dragging the bounding box bounds, go into ResizingBounds mode.
|
||||
// If the user is dragging the rotate trigger, go into RotatingBounds mode.
|
||||
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
|
|||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::portfolio::utility_types::{FontCatalog, FontCatalogStyle, PersistentData};
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
|
||||
|
|
@ -13,6 +14,7 @@ use crate::messages::tool::common_functionality::resize::Resize;
|
|||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
use crate::messages::tool::common_functionality::utility_functions::text_bounding_box;
|
||||
use crate::messages::tool::utility_types::ToolRefreshOptions;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
|
|
@ -31,8 +33,7 @@ pub struct TextOptions {
|
|||
font_size: f64,
|
||||
line_height_ratio: f64,
|
||||
character_spacing: f64,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
font: Font,
|
||||
fill: ToolColorOptions,
|
||||
tilt: f64,
|
||||
align: TextAlign,
|
||||
|
|
@ -44,8 +45,7 @@ impl Default for TextOptions {
|
|||
font_size: 24.,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 0.,
|
||||
font_name: graphene_std::consts::DEFAULT_FONT_FAMILY.into(),
|
||||
font_style: graphene_std::consts::DEFAULT_FONT_STYLE.into(),
|
||||
font: Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into()),
|
||||
fill: ToolColorOptions::new_primary(),
|
||||
tilt: 0.,
|
||||
align: TextAlign::default(),
|
||||
|
|
@ -71,13 +71,14 @@ pub enum TextToolMessage {
|
|||
TextChange { new_text: String, is_left_or_right_click: bool },
|
||||
UpdateBounds { new_text: String },
|
||||
UpdateOptions { options: TextOptionsUpdate },
|
||||
RefreshEditingFontData,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum TextOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
Font { family: String, style: String },
|
||||
Font { font: Font },
|
||||
FontSize(f64),
|
||||
LineHeightRatio(f64),
|
||||
Align(TextAlign),
|
||||
|
|
@ -96,31 +97,84 @@ impl ToolMetadata for TextTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_text_widgets(tool: &TextTool) -> Vec<WidgetInstance> {
|
||||
let font = FontInput::new(&tool.options.font_name, &tool.options.font_style)
|
||||
.is_style_picker(false)
|
||||
.on_update(|font_input: &FontInput| {
|
||||
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<WidgetInstance> {
|
||||
fn update_options(font: Font, commit_style: Option<String>) -> impl Fn(&()) -> Message + Clone {
|
||||
let mut font = font;
|
||||
if let Some(style) = commit_style {
|
||||
font.font_style = style;
|
||||
}
|
||||
|
||||
move |_| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
},
|
||||
options: TextOptionsUpdate::Font { font: font.clone() },
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance();
|
||||
let style = FontInput::new(&tool.options.font_name, &tool.options.font_style)
|
||||
.is_style_picker(true)
|
||||
.on_update(|font_input: &FontInput| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
},
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance();
|
||||
}
|
||||
}
|
||||
|
||||
let font = DropdownInput::new(vec![
|
||||
font_catalog
|
||||
.0
|
||||
.iter()
|
||||
.map(|family| {
|
||||
let font = Font::new(family.name.clone(), tool.options.font.font_style.clone());
|
||||
let commit_style = font_catalog.find_font_style_in_catalog(&tool.options.font).map(|style| style.to_named_style());
|
||||
let update = update_options(font.clone(), None);
|
||||
let commit = update_options(font, commit_style);
|
||||
|
||||
MenuListEntry::new(family.name.clone())
|
||||
.label(family.name.clone())
|
||||
.font(family.closest_style(400, false).preview_url(&family.name))
|
||||
.on_update(update)
|
||||
.on_commit(commit)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
])
|
||||
.selected_index(font_catalog.0.iter().position(|family| family.name == tool.options.font.font_family).map(|i| i as u32))
|
||||
.virtual_scrolling(true)
|
||||
.widget_instance();
|
||||
|
||||
let style = DropdownInput::new({
|
||||
font_catalog
|
||||
.0
|
||||
.iter()
|
||||
.find(|family| family.name == tool.options.font.font_family)
|
||||
.map(|family| {
|
||||
let build_entry = |style: &FontCatalogStyle| {
|
||||
let font_style = style.to_named_style();
|
||||
|
||||
let font = Font::new(tool.options.font.font_family.clone(), font_style.clone());
|
||||
let commit_style = font_catalog.find_font_style_in_catalog(&tool.options.font).map(|style| style.to_named_style());
|
||||
let update = update_options(font.clone(), None);
|
||||
let commit = update_options(font, commit_style);
|
||||
|
||||
MenuListEntry::new(font_style.clone()).on_update(update).on_commit(commit).label(font_style)
|
||||
};
|
||||
|
||||
vec![
|
||||
family.styles.iter().filter(|style| !style.italic).map(build_entry).collect::<Vec<_>>(),
|
||||
family.styles.iter().filter(|style| style.italic).map(build_entry).collect::<Vec<_>>(),
|
||||
]
|
||||
})
|
||||
.filter(|styles| !styles.is_empty())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.selected_index(
|
||||
font_catalog
|
||||
.0
|
||||
.iter()
|
||||
.find(|family| family.name == tool.options.font.font_family)
|
||||
.and_then(|family| {
|
||||
let not_italic = family.styles.iter().filter(|style| !style.italic);
|
||||
let italic = family.styles.iter().filter(|style| style.italic);
|
||||
not_italic
|
||||
.chain(italic)
|
||||
.position(|style| Some(style) == font_catalog.find_font_style_in_catalog(&tool.options.font).as_ref())
|
||||
})
|
||||
.map(|i| i as u32),
|
||||
)
|
||||
.widget_instance();
|
||||
|
||||
let size = NumberInput::new(Some(tool.options.font_size))
|
||||
.unit(" px")
|
||||
.label("Size")
|
||||
|
|
@ -172,9 +226,22 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetInstance> {
|
|||
]
|
||||
}
|
||||
|
||||
impl LayoutHolder for TextTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = create_text_widgets(self);
|
||||
impl ToolRefreshOptions for TextTool {
|
||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions, &persistent_data.font_catalog);
|
||||
}
|
||||
}
|
||||
|
||||
impl TextTool {
|
||||
fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, font_catalog: &FontCatalog) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.layout(font_catalog),
|
||||
layout_target,
|
||||
});
|
||||
}
|
||||
|
||||
fn layout(&self, font_catalog: &FontCatalog) -> Layout {
|
||||
let mut widgets = create_text_widgets(self, font_catalog);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
|
||||
|
||||
|
|
@ -215,11 +282,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||
return;
|
||||
};
|
||||
match options {
|
||||
TextOptionsUpdate::Font { family, style } => {
|
||||
self.options.font_name = family;
|
||||
self.options.font_style = style;
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
TextOptionsUpdate::Font { font } => {
|
||||
self.options.font = font;
|
||||
}
|
||||
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
|
||||
TextOptionsUpdate::LineHeightRatio(line_height_ratio) => self.options.line_height_ratio = line_height_ratio,
|
||||
|
|
@ -235,7 +299,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||
}
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions, &context.persistent_data.font_catalog);
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
|
|
@ -339,6 +403,7 @@ impl TextToolData {
|
|||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
|
||||
/// Set the editing state of the currently modifying layer
|
||||
fn set_editing(&self, editable: bool, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
||||
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
|
||||
|
|
@ -347,7 +412,7 @@ impl TextToolData {
|
|||
line_height_ratio: editing_text.typesetting.line_height_ratio,
|
||||
font_size: editing_text.typesetting.font_size,
|
||||
color: editing_text.color.unwrap_or(Color::BLACK),
|
||||
url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
|
||||
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default(),
|
||||
transform: editing_text.transform.to_cols_array(),
|
||||
max_width: editing_text.typesetting.max_width,
|
||||
max_height: editing_text.typesetting.max_height,
|
||||
|
|
@ -411,6 +476,7 @@ impl TextToolData {
|
|||
|
||||
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
|
||||
|
||||
responses.add(PortfolioMessage::LoadFontData { font: editing_text.font.clone() });
|
||||
responses.add(GraphOperationMessage::NewTextLayer {
|
||||
id: self.layer.to_node(),
|
||||
text: String::new(),
|
||||
|
|
@ -498,10 +564,11 @@ impl Fsm for TextToolFsmState {
|
|||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
font_cache,
|
||||
persistent_data,
|
||||
viewport,
|
||||
..
|
||||
} = transition_data;
|
||||
let font_cache = &persistent_data.font_cache;
|
||||
let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
|
|
@ -827,7 +894,7 @@ impl Fsm for TextToolFsmState {
|
|||
tilt: tool_options.tilt,
|
||||
align: tool_options.align,
|
||||
},
|
||||
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
|
||||
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
|
||||
color: tool_options.fill.active_color(),
|
||||
};
|
||||
tool_data.new_text(document, editing_text, font_cache, responses);
|
||||
|
|
@ -852,6 +919,14 @@ impl Fsm for TextToolFsmState {
|
|||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::RefreshEditingFontData) => {
|
||||
let font = Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone());
|
||||
responses.add(FrontendMessage::DisplayEditableTextboxUpdateFontData {
|
||||
font_data: font_cache.get(&font).map(|(data, _)| data.clone()).unwrap_or_default(),
|
||||
});
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text, is_left_or_right_click }) => {
|
||||
tool_data.new_text = new_text;
|
||||
|
||||
|
|
@ -871,6 +946,7 @@ impl Fsm for TextToolFsmState {
|
|||
}
|
||||
|
||||
responses.add(FrontendMessage::TriggerTextCommit);
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut;
|
|||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::preferences::PreferencesMessageHandler;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::text::FontCache;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
|
|
@ -24,18 +24,28 @@ pub struct ToolActionMessageContext<'a> {
|
|||
pub document_id: DocumentId,
|
||||
pub global_tool_data: &'a DocumentToolData,
|
||||
pub input: &'a InputPreprocessorMessageHandler,
|
||||
pub font_cache: &'a FontCache,
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub shape_editor: &'a mut ShapeState,
|
||||
pub node_graph: &'a NodeGraphExecutor,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
pub viewport: &'a ViewportMessageHandler,
|
||||
}
|
||||
|
||||
pub trait ToolCommon: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + LayoutHolder + ToolTransition + ToolMetadata {}
|
||||
impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + LayoutHolder + ToolTransition + ToolMetadata {}
|
||||
pub trait ToolCommon: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + ToolRefreshOptions + ToolTransition + ToolMetadata {}
|
||||
impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + ToolRefreshOptions + ToolTransition + ToolMetadata {}
|
||||
|
||||
type Tool = dyn ToolCommon + Send + Sync;
|
||||
|
||||
pub trait ToolRefreshOptions {
|
||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, _persistent_data: &PersistentData);
|
||||
}
|
||||
|
||||
impl<T: LayoutHolder> ToolRefreshOptions for T {
|
||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, _persistent_data: &PersistentData) {
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/// The FSM (finite state machine) is a flowchart between different operating states that a specific tool might be in.
|
||||
/// It is the central "core" logic area of each tool which is in charge of maintaining the state of the tool and responding to events coming from outside (like user input).
|
||||
/// For example, a tool might be `Ready` or `Drawing` depending on if the user is idle or actively drawing with the mouse held down.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import { createAppWindowState } from "@graphite/state-providers/app-window";
|
||||
import { createDialogState } from "@graphite/state-providers/dialog";
|
||||
import { createDocumentState } from "@graphite/state-providers/document";
|
||||
import { createFontsState } from "@graphite/state-providers/fonts";
|
||||
import { createFontsManager } from "/src/io-managers/fonts";
|
||||
import { createFullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import { createPortfolioState } from "@graphite/state-providers/portfolio";
|
||||
|
|
@ -27,12 +27,10 @@
|
|||
// State provider systems
|
||||
let dialog = createDialogState(editor);
|
||||
setContext("dialog", dialog);
|
||||
let tooltip = createTooltipState();
|
||||
let tooltip = createTooltipState(editor);
|
||||
setContext("tooltip", tooltip);
|
||||
let document = createDocumentState(editor);
|
||||
setContext("document", document);
|
||||
let fonts = createFontsState(editor);
|
||||
setContext("fonts", fonts);
|
||||
let fullscreen = createFullscreenState(editor);
|
||||
setContext("fullscreen", fullscreen);
|
||||
let nodeGraph = createNodeGraphState(editor);
|
||||
|
|
@ -48,6 +46,7 @@
|
|||
createLocalizationManager(editor);
|
||||
createPanicManager(editor, dialog);
|
||||
createPersistenceManager(editor, portfolio);
|
||||
createFontsManager(editor);
|
||||
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, createEventDispatcher } from "svelte";
|
||||
import { getContext, onDestroy, createEventDispatcher } from "svelte";
|
||||
|
||||
import type { HSV, RGB, FillChoice } from "@graphite/messages";
|
||||
import type { MenuDirection } from "@graphite/messages";
|
||||
import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
|
@ -40,6 +40,7 @@
|
|||
];
|
||||
|
||||
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>();
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
|
||||
export let colorOrGradient: FillChoice;
|
||||
export let allowNone = false;
|
||||
|
|
@ -424,12 +425,16 @@
|
|||
"--opaque-color-contrasting": (newColor.opaque() || new Color(0, 0, 0, 1)).contrastingColor(),
|
||||
}}
|
||||
>
|
||||
{@const hueDescription = "The shade along the spectrum of the rainbow."}
|
||||
{@const saturationDescription = "The vividness from grayscale to full color."}
|
||||
{@const valueDescription = "The brightness from black to full color."}
|
||||
<LayoutCol class="pickers-and-gradient">
|
||||
<LayoutRow class="pickers">
|
||||
<LayoutCol
|
||||
class="saturation-value-picker"
|
||||
data-tooltip-label="Saturation and Value"
|
||||
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
|
||||
data-tooltip-description={`To move only along the saturation (X) or value (Y) axis, perform the shortcut shown.${disabled ? "\n\nDisabled (read-only)." : ""}`}
|
||||
data-tooltip-shortcut={$tooltip.shiftClickShortcut?.shortcut ? JSON.stringify($tooltip.shiftClickShortcut.shortcut) : undefined}
|
||||
on:pointerdown={onPointerDown}
|
||||
data-saturation-value-picker
|
||||
>
|
||||
|
|
@ -449,7 +454,7 @@
|
|||
<LayoutCol
|
||||
class="hue-picker"
|
||||
data-tooltip-label="Hue"
|
||||
data-tooltip-description={`The shade along the spectrum of the rainbow.${disabled ? "\n\nDisabled (read-only)." : ""}`}
|
||||
data-tooltip-description={`${hueDescription}${disabled ? "\n\nDisabled (read-only)." : ""}`}
|
||||
on:pointerdown={onPointerDown}
|
||||
data-hue-picker
|
||||
>
|
||||
|
|
@ -522,10 +527,8 @@
|
|||
</LayoutRow>
|
||||
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltipDescription="Color model, color space, and HDR (coming soon)." /> -->
|
||||
<LayoutRow>
|
||||
<TextLabel
|
||||
tooltipLabel="Hex Color Code"
|
||||
tooltipDescription="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors.">Hex</TextLabel
|
||||
>
|
||||
{@const hexDescription = "Color code in hexadecimal format. 6 digits if opaque, 8 with alpha. Accepts input of CSS color values including named colors."}
|
||||
<TextLabel tooltipLabel="Hex Color Code" tooltipDescription={hexDescription}>Hex</TextLabel>
|
||||
<Separator type="Related" />
|
||||
<LayoutRow>
|
||||
<TextInput
|
||||
|
|
@ -537,7 +540,7 @@
|
|||
}}
|
||||
centered={true}
|
||||
tooltipLabel="Hex Color Code"
|
||||
tooltipDescription="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."
|
||||
tooltipDescription={hexDescription}
|
||||
bind:this={hexCodeInputWidget}
|
||||
/>
|
||||
</LayoutRow>
|
||||
|
|
@ -601,16 +604,17 @@
|
|||
v: "Value Component",
|
||||
}[channel]}
|
||||
tooltipDescription={{
|
||||
h: "The shade along the spectrum of the rainbow.",
|
||||
s: "The vividness from grayscale to full color.",
|
||||
v: "The brightness from black to full color.",
|
||||
h: hueDescription,
|
||||
s: saturationDescription,
|
||||
v: valueDescription,
|
||||
}[channel]}
|
||||
/>
|
||||
{/each}
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextLabel tooltipLabel="Alpha" tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%).">Alpha</TextLabel>
|
||||
{@const alphaDescription = "The level of translucency, from transparent (0%) to opaque (100%)."}
|
||||
<TextLabel tooltipLabel="Alpha" tooltipDescription={alphaDescription}>Alpha</TextLabel>
|
||||
<Separator type="Related" />
|
||||
<NumberInput
|
||||
value={!isNone ? alpha * 100 : undefined}
|
||||
|
|
@ -630,7 +634,7 @@
|
|||
mode="Range"
|
||||
displayDecimalPlaces={1}
|
||||
tooltipLabel="Alpha"
|
||||
tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%)."
|
||||
tooltipDescription={alphaDescription}
|
||||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="leftover-space" />
|
||||
|
|
@ -670,7 +674,7 @@
|
|||
data-pure-tile={name.toLowerCase()}
|
||||
style:--pure-color={color}
|
||||
style:--pure-color-gray={gray}
|
||||
data-tooltip-label="Set to Red"
|
||||
data-tooltip-label={`Set to ${name}`}
|
||||
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
|
||||
/>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
export let parentsValuePath: string[] = [];
|
||||
export let entries: MenuListEntry[][];
|
||||
export let entriesHash: bigint;
|
||||
export let activeEntry: MenuListEntry | undefined = undefined;
|
||||
export let open: boolean;
|
||||
export let direction: MenuDirection = "Bottom";
|
||||
|
|
@ -37,26 +38,29 @@
|
|||
export let drawIcon = false;
|
||||
export let interactive = false;
|
||||
export let scrollableY = false;
|
||||
export let virtualScrollingEntryHeight = 0;
|
||||
export let virtualScrolling = false;
|
||||
|
||||
// Keep the child references outside of the entries array so as to avoid infinite recursion.
|
||||
let childReferences: MenuList[][] = [];
|
||||
let search = "";
|
||||
|
||||
let reactiveEntries = entries;
|
||||
let highlighted = activeEntry as MenuListEntry | undefined;
|
||||
let virtualScrollingEntriesStart = 0;
|
||||
|
||||
// Called only when `open` is changed from outside this component
|
||||
// `watchOpen` is called only when `open` is changed from outside this component
|
||||
$: watchOpen(open);
|
||||
$: watchEntries(entries);
|
||||
$: watchEntriesHash(entriesHash);
|
||||
$: watchRemeasureWidth(filteredEntries, drawIcon);
|
||||
$: watchHighlightedWithSearch(filteredEntries, open);
|
||||
|
||||
$: filteredEntries = entries.map((section) => section.filter((entry) => inSearch(search, entry)));
|
||||
$: virtualScrollingTotalHeight = filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight;
|
||||
$: virtualScrollingStartIndex = Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
|
||||
$: virtualScrollingEndIndex = filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
|
||||
$: virtualScrollingEntryHeight = virtualScrolling ? 20 : 0;
|
||||
$: filteredEntries = reactiveEntries.map((section) => section.filter((entry) => inSearch(search, entry)));
|
||||
$: startIndex = virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0;
|
||||
// Virtual scrolling calculations
|
||||
$: virtualScrollingTotalHeight = filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight;
|
||||
$: virtualScrollingStartIndex = filteredEntries.length === 0 ? 0 : Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
|
||||
$: virtualScrollingEndIndex = filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
|
||||
|
||||
// TODO: Move keyboard input handling entirely to the unified system in `input.ts`.
|
||||
// TODO: The current approach is hacky and blocks the allowances for shortcuts like the key to open the browser's dev tools.
|
||||
|
|
@ -139,6 +143,10 @@
|
|||
});
|
||||
}
|
||||
|
||||
function watchEntriesHash(_entriesHash: bigint) {
|
||||
reactiveEntries = entries;
|
||||
}
|
||||
|
||||
function watchRemeasureWidth(_: MenuListEntry[][], __: boolean) {
|
||||
self?.measureAndEmitNaturalWidth();
|
||||
}
|
||||
|
|
@ -149,23 +157,31 @@
|
|||
}
|
||||
|
||||
function getChildReference(menuListEntry: MenuListEntry): MenuList | undefined {
|
||||
const index = filteredEntries.flat().indexOf(menuListEntry);
|
||||
return childReferences.flat().filter((x) => x)[index];
|
||||
const index = filteredEntries.flat().findIndex((entry) => entry.value === menuListEntry.value);
|
||||
|
||||
if (index !== -1) {
|
||||
return childReferences.flat().filter((x) => x)[index];
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("MenuListEntry not found in filteredEntries:", menuListEntry);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onEntryClick(menuListEntry: MenuListEntry) {
|
||||
// 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);
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
reactiveEntries = reactiveEntries;
|
||||
}
|
||||
dispatch("open", false);
|
||||
open = false;
|
||||
reactiveEntries = reactiveEntries;
|
||||
|
||||
// Notify the parent about the clicked entry as the new active entry
|
||||
dispatch("activeEntry", menuListEntry);
|
||||
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
|
||||
}
|
||||
|
||||
function onEntryPointerEnter(menuListEntry: MenuListEntry) {
|
||||
|
|
@ -177,8 +193,10 @@
|
|||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = true;
|
||||
entries = entries;
|
||||
} else dispatch("open", true);
|
||||
reactiveEntries = reactiveEntries;
|
||||
} else {
|
||||
dispatch("open", true);
|
||||
}
|
||||
}
|
||||
|
||||
function onEntryPointerLeave(menuListEntry: MenuListEntry) {
|
||||
|
|
@ -190,8 +208,10 @@
|
|||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
} else dispatch("open", false);
|
||||
reactiveEntries = reactiveEntries;
|
||||
} else {
|
||||
dispatch("open", false);
|
||||
}
|
||||
}
|
||||
|
||||
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
|
||||
|
|
@ -365,7 +385,7 @@
|
|||
let container = scroller?.div?.();
|
||||
if (!container || !highlighted) return;
|
||||
let containerBoundingRect = container.getBoundingClientRect();
|
||||
let highlightedIndex = filteredEntries.flat().findIndex((entry) => entry === highlighted);
|
||||
let highlightedIndex = filteredEntries.flat().findIndex((entry) => entry.value === highlighted?.value);
|
||||
|
||||
let selectedBoundingRect = new DOMRect();
|
||||
if (virtualScrollingEntryHeight) {
|
||||
|
|
@ -386,10 +406,6 @@
|
|||
container.scrollBy(0, selectedBoundingRect.y - (containerBoundingRect.y + containerBoundingRect.height) + selectedBoundingRect.height);
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollViewTo(distanceDown: number) {
|
||||
scroller?.div?.()?.scrollTo(0, distanceDown);
|
||||
}
|
||||
</script>
|
||||
|
||||
<FloatingMenu
|
||||
|
|
@ -419,8 +435,8 @@
|
|||
{#if virtualScrollingEntryHeight}
|
||||
<LayoutRow class="scroll-spacer" styles={{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }} />
|
||||
{/if}
|
||||
{#each entries as section, sectionIndex (sectionIndex)}
|
||||
{#if includeSeparator(entries, section, sectionIndex, search)}
|
||||
{#each reactiveEntries as section, sectionIndex (sectionIndex)}
|
||||
{#if includeSeparator(reactiveEntries, section, sectionIndex, search)}
|
||||
<Separator type="Section" direction="Vertical" />
|
||||
{/if}
|
||||
{#each currentEntries(section, virtualScrollingEntryHeight, virtualScrollingStartIndex, virtualScrollingEndIndex, search) as entry, entryIndex (entryIndex + startIndex)}
|
||||
|
|
@ -442,10 +458,10 @@
|
|||
{/if}
|
||||
|
||||
{#if entry.font}
|
||||
<link rel="stylesheet" href={entry.font?.toString()} />
|
||||
<link rel="stylesheet" href={entry.font} />
|
||||
{/if}
|
||||
|
||||
<TextLabel class="entry-label" styles={{ "font-family": `${!entry.font ? "inherit" : entry.value}` }}>{entry.label}</TextLabel>
|
||||
<TextLabel class="entry-label" styles={entry.font ? { "font-family": entry.value } : {}}>{entry.label}</TextLabel>
|
||||
|
||||
{#if entry.tooltipShortcut?.shortcut.length}
|
||||
<ShortcutLabel shortcut={entry.tooltipShortcut} />
|
||||
|
|
@ -470,6 +486,7 @@
|
|||
open={getChildReference(entry)?.open || false}
|
||||
direction="TopRight"
|
||||
entries={entry.children}
|
||||
entriesHash={entry.childrenHash || 0n}
|
||||
{minWidth}
|
||||
{drawIcon}
|
||||
{scrollableY}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
|
||||
let self: FloatingMenu | undefined;
|
||||
|
||||
$: label = filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim());
|
||||
$: description = filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim());
|
||||
$: label = parseMarkdown(filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim()));
|
||||
$: description = parseMarkdown(filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim()));
|
||||
$: shortcutJSON = $tooltip.element?.getAttribute("data-tooltip-shortcut")?.trim();
|
||||
$: shortcut = ((shortcutJSON) => {
|
||||
if (!shortcutJSON) return undefined;
|
||||
|
|
@ -32,6 +32,26 @@
|
|||
if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return "";
|
||||
return text;
|
||||
}
|
||||
|
||||
function parseMarkdown(markdown: string | undefined): string | undefined {
|
||||
if (!markdown) return undefined;
|
||||
|
||||
return (
|
||||
markdown
|
||||
// .split("\n")
|
||||
// .map((line) => line.trim())
|
||||
// .join("\n")
|
||||
// .split("\n\n")
|
||||
// .map((paragraph) => paragraph.replaceAll("\n", " "))
|
||||
// .join("\n\n")
|
||||
// Bold
|
||||
.replace(/\*\*((?:(?!\*\*).)+)\*\*/g, "<strong>$1</strong>")
|
||||
// Italic
|
||||
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||
// Backticks
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if label || description}
|
||||
|
|
@ -40,7 +60,7 @@
|
|||
{#if label || shortcut}
|
||||
<LayoutRow class="tooltip-header">
|
||||
{#if label}
|
||||
<TextLabel class="tooltip-label">{label}</TextLabel>
|
||||
<TextLabel class="tooltip-label">{@html label}</TextLabel>
|
||||
{/if}
|
||||
{#if shortcut}
|
||||
<ShortcutLabel shortcut={{ shortcut }} />
|
||||
|
|
@ -48,7 +68,7 @@
|
|||
</LayoutRow>
|
||||
{/if}
|
||||
{#if description}
|
||||
<TextLabel class="tooltip-description">{description}</TextLabel>
|
||||
<TextLabel class="tooltip-description">{@html description}</TextLabel>
|
||||
{/if}
|
||||
</FloatingMenu>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -372,6 +372,9 @@
|
|||
// Start with the parent of the spawner for this floating menu and keep widening the search for any other valid spawners that are hover-transferrable
|
||||
let currentAncestor = (targetSpawner && ownSpawner?.parentElement) || undefined;
|
||||
while (currentAncestor) {
|
||||
// If the current ancestor blocks hover transfer, stop searching
|
||||
if (currentAncestor.hasAttribute("data-block-hover-transfer")) break;
|
||||
|
||||
const ownSpawnerDepthFromCurrentAncestor = ownSpawner && getDepthFromAncestor(ownSpawner, currentAncestor);
|
||||
const currentAncestor2 = currentAncestor; // This duplicate variable avoids an ESLint warning
|
||||
|
||||
|
|
@ -382,8 +385,8 @@
|
|||
const notOurself = !ownDescendantMenuSpawners.includes(item);
|
||||
// And filter away unequal depths from the current ancestor
|
||||
const notUnequalDepths = notOurself && getDepthFromAncestor(item, currentAncestor2) === ownSpawnerDepthFromCurrentAncestor;
|
||||
// And filter away elements that explicitly disable hover transfer
|
||||
return notUnequalDepths && !(item as HTMLElement).getAttribute?.("data-floating-menu-spawner")?.includes("no-hover-transfer");
|
||||
// And filter away descendants that explicitly disable hover transfer
|
||||
return notUnequalDepths && !(item instanceof HTMLElement && item.hasAttribute("data-block-hover-transfer"));
|
||||
});
|
||||
|
||||
// If none were found, widen the search by a level and keep trying (or stop looping if the root was reached)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
type MouseCursorIcon,
|
||||
type XY,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxUpdateFontData,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerTextCommit,
|
||||
|
|
@ -360,10 +361,14 @@
|
|||
if (!textInput) return;
|
||||
editor.handle.updateBounds(textInputCleanup(textInput.innerText));
|
||||
};
|
||||
|
||||
textInputMatrix = displayEditableTextbox.transform;
|
||||
const newFont = new FontFace("text-font", `url(${displayEditableTextbox.url})`);
|
||||
window.document.fonts.add(newFont);
|
||||
textInput.style.fontFamily = "text-font";
|
||||
|
||||
const data = new Uint8Array(displayEditableTextbox.fontData);
|
||||
if (data.length > 0) {
|
||||
window.document.fonts.add(new FontFace("text-font", data));
|
||||
textInput.style.fontFamily = "text-font";
|
||||
}
|
||||
|
||||
// Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060
|
||||
|
||||
|
|
@ -471,6 +476,15 @@
|
|||
|
||||
displayEditableTextbox(data);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxUpdateFontData, async (data) => {
|
||||
await tick();
|
||||
|
||||
const fontData = new Uint8Array(data.fontData);
|
||||
if (fontData.length > 0 && textInput) {
|
||||
window.document.fonts.add(new FontFace("text-font", fontData));
|
||||
textInput.style.fontFamily = "text-font";
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxTransform, async (data) => {
|
||||
textInputMatrix = data.transform;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
UpdateLayersPanelControlBarLeftLayout,
|
||||
UpdateLayersPanelControlBarRightLayout,
|
||||
UpdateLayersPanelBottomBarLayout,
|
||||
SendShortcutAltClick,
|
||||
} from "@graphite/messages";
|
||||
import type { ActionShortcut, DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages";
|
||||
import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||
|
||||
|
|
@ -49,6 +49,7 @@
|
|||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
|
||||
let list: LayoutCol | undefined;
|
||||
|
||||
|
|
@ -73,13 +74,7 @@
|
|||
let layersPanelControlBarRightLayout: Layout = [];
|
||||
let layersPanelBottomBarLayout: Layout = [];
|
||||
|
||||
let altClickShortcut: ActionShortcut | undefined;
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => {
|
||||
altClickShortcut = data.shortcut;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => {
|
||||
patchLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout);
|
||||
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
|
||||
|
|
@ -627,8 +622,8 @@
|
|||
data-tooltip-description={(listing.entry.expanded
|
||||
? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)"
|
||||
: "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") +
|
||||
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nNote: a selected layer is currently contained within.\n" : "")}
|
||||
data-tooltip-shortcut={altClickShortcut?.shortcut ? JSON.stringify(altClickShortcut.shortcut) : undefined}
|
||||
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")}
|
||||
data-tooltip-shortcut={$tooltip.altClickShortcut?.shortcut ? JSON.stringify($tooltip.altClickShortcut.shortcut) : undefined}
|
||||
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
|
||||
tabindex="0"
|
||||
></button>
|
||||
|
|
@ -639,8 +634,9 @@
|
|||
<IconLabel
|
||||
icon="Clipped"
|
||||
class="clipped-arrow"
|
||||
tooltipDescription="Clipping mask is active. To release it, perform the shortcut on the layer border."
|
||||
tooltipShortcut={altClickShortcut}
|
||||
tooltipLabel="Layer Clipped"
|
||||
tooltipDescription="Clipping mask is active. To release it, target the bottom border of the layer and perform the shortcut shown."
|
||||
tooltipShortcut={$tooltip.altClickShortcut}
|
||||
/>
|
||||
{/if}
|
||||
<div class="thumbnail">
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
|
||||
style:--layer-area-width={layerAreaWidth}
|
||||
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
|
||||
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
|
||||
data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
|
||||
data-tooltip-description={`
|
||||
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
|
||||
`.trim()}
|
||||
|
|
@ -651,7 +651,7 @@
|
|||
style:--clip-path-id={`url(#${clipPathId})`}
|
||||
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
|
||||
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
|
||||
data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
|
||||
data-tooltip-description={`
|
||||
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
|
||||
`.trim()}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
/>
|
||||
</button>
|
||||
{#if expanded}
|
||||
<LayoutCol class="body">
|
||||
<LayoutCol class="body" data-block-hover-transfer>
|
||||
{#each widgetData.layout as layoutGroup}
|
||||
{#if isWidgetSpanRow(layoutGroup)}
|
||||
<WidgetSpan widgetData={layoutGroup} {layoutTarget} />
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
import ColorInput from "@graphite/components/widgets/inputs/ColorInput.svelte";
|
||||
import CurveInput from "@graphite/components/widgets/inputs/CurveInput.svelte";
|
||||
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
|
||||
import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte";
|
||||
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
|
||||
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
|
||||
import ReferencePointInput from "@graphite/components/widgets/inputs/ReferencePointInput.svelte";
|
||||
|
|
@ -61,16 +60,16 @@
|
|||
return widgets;
|
||||
}
|
||||
|
||||
function widgetValueCommit(index: number, value: unknown) {
|
||||
editor.handle.widgetValueCommit(layoutTarget, widgets[index].widgetId, value);
|
||||
function widgetValueCommit(widgetIndex: number, value: unknown) {
|
||||
editor.handle.widgetValueCommit(layoutTarget, widgets[widgetIndex].widgetId, value);
|
||||
}
|
||||
|
||||
function widgetValueUpdate(index: number, value: unknown) {
|
||||
editor.handle.widgetValueUpdate(layoutTarget, widgets[index].widgetId, value);
|
||||
function widgetValueUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
|
||||
editor.handle.widgetValueUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
|
||||
}
|
||||
|
||||
function widgetValueCommitAndUpdate(index: number, value: unknown) {
|
||||
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[index].widgetId, value);
|
||||
function widgetValueCommitAndUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
|
||||
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
|
||||
}
|
||||
|
||||
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
|
||||
|
|
@ -85,43 +84,47 @@
|
|||
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
|
||||
|
||||
<div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}>
|
||||
{#each widgets as component, index}
|
||||
{#each widgets as component, widgetIndex}
|
||||
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
|
||||
{#if checkboxInput}
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
|
||||
{#if colorInput}
|
||||
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
<ColorInput
|
||||
{...exclude(colorInput)}
|
||||
on:value={({ detail }) => widgetValueUpdate(widgetIndex, detail, false)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, colorInput.value)}
|
||||
/>
|
||||
{/if}
|
||||
<!-- TODO: Curves Input is currently unused -->
|
||||
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
|
||||
{#if curvesInput}
|
||||
<CurveInput {...exclude(curvesInput)} on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(index, value), { debounceTime: 120 }).debounceUpdateValue(detail)} />
|
||||
<CurveInput
|
||||
{...exclude(curvesInput)}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(widgetIndex, value, false), { debounceTime: 120 }).debounceUpdateValue(detail)}
|
||||
/>
|
||||
{/if}
|
||||
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
|
||||
{#if dropdownInput}
|
||||
<DropdownInput
|
||||
{...exclude(dropdownInput)}
|
||||
on:hoverInEntry={({ detail }) => {
|
||||
return widgetValueUpdate(index, detail);
|
||||
return widgetValueUpdate(widgetIndex, detail, false);
|
||||
}}
|
||||
on:hoverOutEntry={({ detail }) => {
|
||||
return widgetValueUpdate(index, detail);
|
||||
return widgetValueUpdate(widgetIndex, detail, false);
|
||||
}}
|
||||
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)}
|
||||
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)}
|
||||
/>
|
||||
{/if}
|
||||
{@const fontInput = narrowWidgetProps(component.props, "FontInput")}
|
||||
{#if fontInput}
|
||||
<FontInput {...exclude(fontInput)} on:changeFont={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")}
|
||||
{#if parameterExposeButton}
|
||||
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const iconButton = narrowWidgetProps(component.props, "IconButton")}
|
||||
{#if iconButton}
|
||||
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const iconLabel = narrowWidgetProps(component.props, "IconLabel")}
|
||||
{#if iconLabel}
|
||||
|
|
@ -138,25 +141,25 @@
|
|||
{/if}
|
||||
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
|
||||
{#if imageButton}
|
||||
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
|
||||
{#if nodeCatalog}
|
||||
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(index, e.detail)} />
|
||||
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(widgetIndex, e.detail, false)} />
|
||||
{/if}
|
||||
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
|
||||
{#if numberInput}
|
||||
<NumberInput
|
||||
{...exclude(numberInput)}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(index, value)).debounceUpdateValue(detail)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(index, numberInput.value)}
|
||||
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(index, "Increment")}
|
||||
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(index, "Decrement")}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(widgetIndex, value, true)).debounceUpdateValue(detail)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, numberInput.value)}
|
||||
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(widgetIndex, "Increment", false)}
|
||||
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(widgetIndex, "Decrement", false)}
|
||||
/>
|
||||
{/if}
|
||||
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
|
||||
{#if referencePointInput}
|
||||
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
|
||||
{#if popoverButton}
|
||||
|
|
@ -166,7 +169,7 @@
|
|||
{/if}
|
||||
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
|
||||
{#if radioInput}
|
||||
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const separator = narrowWidgetProps(component.props, "Separator")}
|
||||
{#if separator}
|
||||
|
|
@ -178,19 +181,23 @@
|
|||
{/if}
|
||||
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
|
||||
{#if textAreaInput}
|
||||
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)} />
|
||||
{/if}
|
||||
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
|
||||
{#if textButton}
|
||||
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, [])} on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextButton
|
||||
{...exclude(textButton)}
|
||||
action={() => widgetValueCommitAndUpdate(widgetIndex, [], true)}
|
||||
on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)}
|
||||
/>
|
||||
{/if}
|
||||
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
|
||||
{#if breadcrumbTrailButtons}
|
||||
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(index, breadcrumbIndex)} />
|
||||
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(widgetIndex, breadcrumbIndex, true)} />
|
||||
{/if}
|
||||
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
|
||||
{#if textInput}
|
||||
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const textLabel = narrowWidgetProps(component.props, "TextLabel")}
|
||||
{#if textLabel}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
export let menuListChildren: MenuListEntry[][] | undefined = undefined;
|
||||
export let menuListChildrenHash: bigint | undefined = undefined;
|
||||
|
||||
// Callbacks
|
||||
// TODO: Replace this with an event binding (and on other components that do this)
|
||||
|
|
@ -71,7 +72,8 @@
|
|||
data-disabled={disabled || undefined}
|
||||
data-text-button
|
||||
tabindex={disabled ? -1 : 0}
|
||||
data-floating-menu-spawner={menuListChildrenExists ? "" : "no-hover-transfer"}
|
||||
data-floating-menu-spawner
|
||||
data-block-hover-transfer={menuListChildrenExists ? undefined : ""}
|
||||
on:click={onClick}
|
||||
>
|
||||
{#if icon}
|
||||
|
|
@ -90,6 +92,7 @@
|
|||
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
|
||||
open={self?.open || false}
|
||||
entries={menuListChildren || []}
|
||||
entriesHash={menuListChildrenHash || 0n}
|
||||
direction="Bottom"
|
||||
minWidth={240}
|
||||
drawIcon={true}
|
||||
|
|
|
|||
|
|
@ -12,15 +12,16 @@
|
|||
|
||||
const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>();
|
||||
|
||||
let menuList: MenuList | undefined;
|
||||
let self: LayoutRow | undefined;
|
||||
|
||||
export let entries: MenuListEntry[][];
|
||||
export let entriesHash: bigint | undefined = undefined;
|
||||
export let selectedIndex: number | undefined = undefined; // When not provided, a dash is displayed
|
||||
export let drawIcon = false;
|
||||
export let interactive = true;
|
||||
export let disabled = false;
|
||||
export let narrow = false;
|
||||
export let virtualScrolling = false;
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
|
|
@ -53,19 +54,32 @@
|
|||
activeEntry = makeActiveEntry();
|
||||
}
|
||||
|
||||
// Called when the `activeEntry` two-way binding on this component's MenuList component is changed, or by the `selectedIndex()` watcher above (but we want to skip that case)
|
||||
// Called when the `activeEntry` two-way binding on this component's MenuList component is changed, or by the `watchSelectedIndex()` watcher above (but we want to skip that case)
|
||||
function watchActiveEntry(activeEntry: MenuListEntry) {
|
||||
if (activeEntrySkipWatcher) {
|
||||
activeEntrySkipWatcher = false;
|
||||
} else if (activeEntry !== DASH_ENTRY) {
|
||||
// We need to set to the initial value first to track a right history step, as if we hover in initial selection.
|
||||
if (initialSelectedIndex !== undefined) dispatch("hoverInEntry", initialSelectedIndex);
|
||||
dispatch("selectedIndex", entries.flat().indexOf(activeEntry));
|
||||
const index = entries.flat().findIndex((entry) => entry.value === activeEntry.value);
|
||||
if (index !== -1) {
|
||||
dispatch("selectedIndex", index);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Selected index not found in entries:", activeEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchHoverInEntry(hoveredEntry: MenuListEntry) {
|
||||
dispatch("hoverInEntry", entries.flat().indexOf(hoveredEntry));
|
||||
const index = entries.flat().findIndex((entry) => entry.value === hoveredEntry.value);
|
||||
|
||||
if (index !== -1) {
|
||||
dispatch("hoverInEntry", index);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Hovered entry not found in entries:", hoveredEntry);
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchHoverOutEntry() {
|
||||
|
|
@ -123,11 +137,12 @@
|
|||
{open}
|
||||
{activeEntry}
|
||||
{entries}
|
||||
entriesHash={entriesHash || 0n}
|
||||
{drawIcon}
|
||||
{interactive}
|
||||
{virtualScrolling}
|
||||
direction="Bottom"
|
||||
scrollableY={true}
|
||||
bind:this={menuList}
|
||||
/>
|
||||
</LayoutRow>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,186 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount, tick } from "svelte";
|
||||
|
||||
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
|
||||
import type { FontsState } from "@graphite/state-providers/fonts";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const fonts = getContext<FontsState>("fonts");
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
fontFamily: string;
|
||||
fontStyle: string;
|
||||
changeFont: { fontFamily: string; fontStyle: string; fontFileUrl: string | undefined };
|
||||
}>();
|
||||
|
||||
let menuList: MenuList | undefined;
|
||||
|
||||
export let fontFamily: string;
|
||||
export let fontStyle: string;
|
||||
export let isStyle = false;
|
||||
export let disabled = false;
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
|
||||
let open = false;
|
||||
let entries: MenuListEntry[] = [];
|
||||
let activeEntry: MenuListEntry | undefined = undefined;
|
||||
let minWidth = isStyle ? 0 : 300;
|
||||
|
||||
$: watchFont(fontFamily, fontStyle);
|
||||
|
||||
async function watchFont(..._: string[]) {
|
||||
// We set this function's result to a local variable to avoid reading from `entries` which causes Svelte to trigger an update that results in an infinite loop
|
||||
const newEntries = await getEntries();
|
||||
entries = newEntries;
|
||||
activeEntry = getActiveEntry(newEntries);
|
||||
}
|
||||
|
||||
async function setOpen() {
|
||||
open = true;
|
||||
|
||||
// Scroll to the active entry (the scroller div does not yet exist so we must wait for the component to render)
|
||||
await tick();
|
||||
|
||||
if (activeEntry) {
|
||||
const index = entries.indexOf(activeEntry);
|
||||
menuList?.scrollViewTo(Math.max(0, index * 20 - 190));
|
||||
}
|
||||
}
|
||||
|
||||
function toggleOpen() {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
|
||||
if (open) setOpen();
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFont(newName: string) {
|
||||
let family;
|
||||
let style;
|
||||
|
||||
if (isStyle) {
|
||||
dispatch("fontStyle", newName);
|
||||
|
||||
family = fontFamily;
|
||||
style = newName;
|
||||
} else {
|
||||
dispatch("fontFamily", newName);
|
||||
|
||||
family = newName;
|
||||
style = "Regular (400)";
|
||||
}
|
||||
|
||||
const fontFileUrl = await fonts.getFontFileUrl(family, style);
|
||||
dispatch("changeFont", { fontFamily: family, fontStyle: style, fontFileUrl });
|
||||
}
|
||||
|
||||
async function getEntries(): Promise<MenuListEntry[]> {
|
||||
const x = isStyle ? fonts.getFontStyles(fontFamily) : fonts.fontNames();
|
||||
return (await x).map((entry: { name: string; url: URL | undefined }) => ({
|
||||
value: entry.name,
|
||||
label: entry.name,
|
||||
font: entry.url,
|
||||
action: () => selectFont(entry.name),
|
||||
}));
|
||||
}
|
||||
|
||||
function getActiveEntry(entries: MenuListEntry[]): MenuListEntry {
|
||||
const selectedChoice = isStyle ? fontStyle : fontFamily;
|
||||
|
||||
return entries.find((entry) => entry.value === selectedChoice) as MenuListEntry;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
entries = await getEntries();
|
||||
|
||||
activeEntry = getActiveEntry(entries);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
||||
<LayoutRow class="font-input">
|
||||
<LayoutRow
|
||||
class="dropdown-box"
|
||||
classes={{ disabled }}
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
{tooltipLabel}
|
||||
{tooltipDescription}
|
||||
{tooltipShortcut}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
on:click={toggleOpen}
|
||||
data-floating-menu-spawner
|
||||
>
|
||||
<TextLabel class="dropdown-label">{activeEntry?.value || ""}</TextLabel>
|
||||
<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
on:naturalWidth={({ detail }) => isStyle && (minWidth = detail)}
|
||||
{activeEntry}
|
||||
on:activeEntry={({ detail }) => (activeEntry = detail)}
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
entries={[entries]}
|
||||
minWidth={isStyle ? 0 : minWidth}
|
||||
virtualScrollingEntryHeight={isStyle ? 0 : 20}
|
||||
scrollableY={true}
|
||||
bind:this={menuList}
|
||||
/>
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
.font-input {
|
||||
position: relative;
|
||||
|
||||
.dropdown-box {
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
background: var(--color-1-nearblack);
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
|
||||
.dropdown-label {
|
||||
margin: 0;
|
||||
margin-left: 8px;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 6px 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
.text-label {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
.text-label {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list .floating-menu-container .floating-menu-content {
|
||||
max-height: 400px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<LayoutCol class="working-colors-button">
|
||||
<LayoutRow class="primary swatch">
|
||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={primaryOpen}
|
||||
on:open={({ detail }) => (primaryOpen = detail)}
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="secondary swatch">
|
||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={secondaryOpen}
|
||||
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
&.monospace {
|
||||
&.monospace,
|
||||
code {
|
||||
font-family: "Source Code Pro", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -94,5 +95,10 @@
|
|||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--color-3-darkgray);
|
||||
padding: 0 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
let menuBarLayout: Layout = [];
|
||||
|
||||
// On mac menu bar needs to be scaled with inverse of UI scale to match native menu buttons.
|
||||
$: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28;
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
|
||||
patchLayout(menuBarLayout, updateMenuBarLayout);
|
||||
|
|
@ -25,7 +28,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<LayoutRow class="title-bar">
|
||||
<LayoutRow class="title-bar" styles={{ height: height + "px" }}>
|
||||
<!-- Menu bar -->
|
||||
<LayoutRow>
|
||||
{#if $appWindow.platform !== "Mac"}
|
||||
|
|
@ -48,7 +51,6 @@
|
|||
|
||||
<style lang="scss" global>
|
||||
.title-bar {
|
||||
height: 28px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
> .layout-row {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import { SendShortcutF11 } from "@graphite/messages";
|
||||
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
const fullscreen = getContext<FullscreenState>("fullscreen");
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let f11Shortcut: ActionShortcut | undefined = undefined;
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => {
|
||||
f11Shortcut = data.shortcut;
|
||||
});
|
||||
});
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
|
||||
async function handleClick() {
|
||||
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
|
||||
|
|
@ -31,7 +22,7 @@
|
|||
on:click={handleClick}
|
||||
tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||
tooltipDescription={$fullscreen.keyboardLockApiSupported ? "While fullscreen, keyboard shortcuts normally reserved by the browser become available." : ""}
|
||||
tooltipShortcut={f11Shortcut}
|
||||
tooltipShortcut={$tooltip.f11Shortcut}
|
||||
>
|
||||
<IconLabel icon={$fullscreen.windowFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
|
||||
</LayoutRow>
|
||||
|
|
|
|||
44
frontend/src/io-managers/fonts.ts
Normal file
44
frontend/src/io-managers/fonts.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerFontCatalogLoad, TriggerFontDataLoad } from "@graphite/messages";
|
||||
|
||||
type ApiResponse = { family: string; variants: string[]; files: Record<string, string> }[];
|
||||
|
||||
const FONT_LIST_API = "https://api.graphite.art/font-list";
|
||||
|
||||
export function createFontsManager(editor: Editor) {
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontCatalogLoad, async () => {
|
||||
const response = await fetch(FONT_LIST_API);
|
||||
const fontListResponse = (await response.json()) as { items: ApiResponse };
|
||||
const fontListData = fontListResponse.items;
|
||||
|
||||
const catalog = fontListData.map((font) => {
|
||||
const styles = font.variants.map((variant) => {
|
||||
const weight = variant === "regular" || variant === "italic" ? 400 : parseInt(variant, 10);
|
||||
const italic = variant.endsWith("italic");
|
||||
const url = font.files[variant].replace("http://", "https://");
|
||||
|
||||
return { weight, italic, url };
|
||||
});
|
||||
return { name: font.family, styles };
|
||||
});
|
||||
|
||||
editor.handle.onFontCatalogLoad(catalog);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontDataLoad, async (triggerFontDataLoad) => {
|
||||
const { fontFamily, fontStyle } = triggerFontDataLoad.font;
|
||||
|
||||
try {
|
||||
if (!triggerFontDataLoad.url) throw new Error("No URL provided for font data load");
|
||||
const response = await fetch(triggerFontDataLoad.url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const data = new Uint8Array(buffer);
|
||||
|
||||
editor.handle.onFontLoad(fontFamily, fontStyle, data);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load font:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -481,11 +481,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
The browser's clipboard permission has been denied.
|
||||
|
||||
Open the browser's website settings (usually accessible
|
||||
just left of the URL) to allow this permission.
|
||||
just left of the URL bar) to allow this permission.
|
||||
`;
|
||||
const nothing = stripIndents`
|
||||
No valid clipboard data was found. You may have better
|
||||
luck pasting with the standard keyboard shortcut instead.
|
||||
success pasting with the standard keyboard shortcut instead.
|
||||
`;
|
||||
|
||||
const matchMessage = {
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ export class SendShortcutAltClick extends JsMessage {
|
|||
readonly shortcut!: ActionShortcut | undefined;
|
||||
}
|
||||
|
||||
export class SendShortcutShiftClick extends JsMessage {
|
||||
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
|
||||
readonly shortcut!: ActionShortcut | undefined;
|
||||
}
|
||||
|
||||
export class UpdateNodeThumbnail extends JsMessage {
|
||||
readonly id!: bigint;
|
||||
|
||||
|
|
@ -319,6 +324,10 @@ export class UpdateViewportPhysicalBounds extends JsMessage {
|
|||
readonly height!: number;
|
||||
}
|
||||
|
||||
export class UpdateUIScale extends JsMessage {
|
||||
readonly scale!: number;
|
||||
}
|
||||
|
||||
// Rust enum `Key`
|
||||
export type KeyRaw = string;
|
||||
// Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key
|
||||
|
|
@ -789,7 +798,7 @@ export class DisplayEditableTextbox extends JsMessage {
|
|||
@Type(() => Color)
|
||||
readonly color!: Color;
|
||||
|
||||
readonly url!: string;
|
||||
readonly fontData!: ArrayBuffer;
|
||||
|
||||
readonly transform!: number[];
|
||||
|
||||
|
|
@ -800,6 +809,10 @@ export class DisplayEditableTextbox extends JsMessage {
|
|||
readonly align!: TextAlign;
|
||||
}
|
||||
|
||||
export class DisplayEditableTextboxUpdateFontData extends JsMessage {
|
||||
readonly fontData!: ArrayBuffer;
|
||||
}
|
||||
|
||||
export class DisplayEditableTextboxTransform extends JsMessage {
|
||||
readonly transform!: number[];
|
||||
}
|
||||
|
|
@ -861,9 +874,13 @@ export class Font {
|
|||
fontStyle!: string;
|
||||
}
|
||||
|
||||
export class TriggerFontLoad extends JsMessage {
|
||||
export class TriggerFontCatalogLoad extends JsMessage {}
|
||||
|
||||
export class TriggerFontDataLoad extends JsMessage {
|
||||
@Type(() => Font)
|
||||
font!: Font;
|
||||
|
||||
url!: string;
|
||||
}
|
||||
|
||||
export class TriggerVisitLink extends JsMessage {
|
||||
|
|
@ -994,13 +1011,14 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri
|
|||
export type MenuListEntry = {
|
||||
value: string;
|
||||
label: string;
|
||||
font?: URL;
|
||||
font?: string;
|
||||
icon?: IconName;
|
||||
disabled?: boolean;
|
||||
tooltipLabel?: string;
|
||||
tooltipDescription?: string;
|
||||
tooltipShortcut?: ActionShortcut;
|
||||
children?: MenuListEntry[][];
|
||||
childrenHash?: bigint;
|
||||
};
|
||||
|
||||
export class CurveManipulatorGroup {
|
||||
|
|
@ -1032,6 +1050,8 @@ export class CurveInput extends WidgetProps {
|
|||
export class DropdownInput extends WidgetProps {
|
||||
entries!: MenuListEntry[][];
|
||||
|
||||
entriesHash!: bigint;
|
||||
|
||||
selectedIndex!: number | undefined;
|
||||
|
||||
drawIcon!: boolean;
|
||||
|
|
@ -1042,6 +1062,8 @@ export class DropdownInput extends WidgetProps {
|
|||
|
||||
narrow!: boolean;
|
||||
|
||||
virtualScrolling!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltipLabel!: string | undefined;
|
||||
|
||||
|
|
@ -1058,25 +1080,6 @@ export class DropdownInput extends WidgetProps {
|
|||
maxWidth!: number;
|
||||
}
|
||||
|
||||
export class FontInput extends WidgetProps {
|
||||
fontFamily!: string;
|
||||
|
||||
fontStyle!: string;
|
||||
|
||||
isStyle!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltipLabel!: string | undefined;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltipDescription!: string | undefined;
|
||||
|
||||
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
|
||||
tooltipShortcut!: ActionShortcut | undefined;
|
||||
}
|
||||
|
||||
export class IconButton extends WidgetProps {
|
||||
icon!: IconName;
|
||||
|
||||
|
|
@ -1345,6 +1348,8 @@ export class TextButton extends WidgetProps {
|
|||
tooltipShortcut!: ActionShortcut | undefined;
|
||||
|
||||
menuListChildren!: MenuListEntry[][];
|
||||
|
||||
menuListChildrenHash!: bigint;
|
||||
}
|
||||
|
||||
export class BreadcrumbTrailButtons extends WidgetProps {
|
||||
|
|
@ -1445,7 +1450,6 @@ const widgetSubTypes = [
|
|||
{ value: ColorInput, name: "ColorInput" },
|
||||
{ value: CurveInput, name: "CurveInput" },
|
||||
{ value: DropdownInput, name: "DropdownInput" },
|
||||
{ value: FontInput, name: "FontInput" },
|
||||
{ value: IconButton, name: "IconButton" },
|
||||
{ value: ImageButton, name: "ImageButton" },
|
||||
{ value: ImageLabel, name: "ImageLabel" },
|
||||
|
|
@ -1691,16 +1695,19 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
DisplayDialogDismiss,
|
||||
DisplayDialogPanic,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxUpdateFontData,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
SendUIMetadata,
|
||||
SendShortcutF11,
|
||||
SendShortcutAltClick,
|
||||
SendShortcutShiftClick,
|
||||
TriggerAboutGraphiteLocalizedCommitDate,
|
||||
TriggerDisplayThirdPartyLicensesDialog,
|
||||
TriggerExportImage,
|
||||
TriggerFetchAndOpenDocument,
|
||||
TriggerFontLoad,
|
||||
TriggerFontCatalogLoad,
|
||||
TriggerFontDataLoad,
|
||||
TriggerImport,
|
||||
TriggerLoadFirstAutoSaveDocument,
|
||||
TriggerLoadPreferences,
|
||||
|
|
@ -1766,6 +1773,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateToolShelfLayout,
|
||||
UpdateViewportHolePunch,
|
||||
UpdateViewportPhysicalBounds,
|
||||
UpdateUIScale,
|
||||
UpdateVisibleNodes,
|
||||
UpdateWelcomeScreenButtonsLayout,
|
||||
UpdateWirePathInProgress,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { type AppWindowPlatform, UpdatePlatform, UpdateViewportHolePunch, UpdateMaximized, UpdateFullscreen } from "@graphite/messages";
|
||||
import { type AppWindowPlatform, UpdatePlatform, UpdateViewportHolePunch, UpdateMaximized, UpdateFullscreen, UpdateUIScale } from "@graphite/messages";
|
||||
|
||||
export function createAppWindowState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
|
|
@ -9,6 +9,7 @@ export function createAppWindowState(editor: Editor) {
|
|||
maximized: false,
|
||||
fullscreen: false,
|
||||
viewportHolePunch: false,
|
||||
uiScale: 1,
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
|
|
@ -36,6 +37,12 @@ export function createAppWindowState(editor: Editor) {
|
|||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateUIScale, (uiScale) => {
|
||||
update((state) => {
|
||||
state.uiScale = uiScale.scale;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerFontLoad } from "@graphite/messages";
|
||||
|
||||
export function createFontsState(editor: Editor) {
|
||||
// TODO: Do some code cleanup to remove the need for this empty store
|
||||
const { subscribe } = writable({});
|
||||
|
||||
function createURL(font: string, weight: string): URL {
|
||||
const url = new URL("https://fonts.googleapis.com/css2");
|
||||
url.searchParams.set("display", "swap");
|
||||
url.searchParams.set("family", `${font}:wght@${weight}`);
|
||||
url.searchParams.set("text", font);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async function fontNames(): Promise<{ name: string; url: URL | undefined }[]> {
|
||||
const pickPreviewWeight = (variants: string[]) => {
|
||||
const weights = variants.map((variant) => Number(variant.match(/.* \((\d+)\)/)?.[1] || "NaN"));
|
||||
const weightGoal = 400;
|
||||
const sorted = weights.map((weight) => [weight, Math.abs(weightGoal - weight - 1)]);
|
||||
sorted.sort(([_, a], [__, b]) => a - b);
|
||||
return sorted[0][0].toString();
|
||||
};
|
||||
return (await loadFontList()).map((font) => ({ name: font.family, url: createURL(font.family, pickPreviewWeight(font.variants)) }));
|
||||
}
|
||||
|
||||
async function getFontStyles(fontFamily: string): Promise<{ name: string; url: URL | undefined }[]> {
|
||||
const font = (await loadFontList()).find((value) => value.family === fontFamily);
|
||||
return font?.variants.map((variant) => ({ name: variant, url: undefined })) || [];
|
||||
}
|
||||
|
||||
async function getFontFileUrl(fontFamily: string, fontStyle: string): Promise<string | undefined> {
|
||||
const font = (await loadFontList()).find((value) => value.family === fontFamily);
|
||||
const fontFileUrl = font?.files.get(fontStyle);
|
||||
return fontFileUrl?.replace("http://", "https://");
|
||||
}
|
||||
|
||||
function formatFontStyleName(fontStyle: string): string {
|
||||
const isItalic = fontStyle.endsWith("italic");
|
||||
const weight = fontStyle === "regular" || fontStyle === "italic" ? 400 : parseInt(fontStyle, 10);
|
||||
let weightName = "";
|
||||
|
||||
let bestWeight = Infinity;
|
||||
weightNameMapping.forEach((nameChecking, weightChecking) => {
|
||||
if (Math.abs(weightChecking - weight) < bestWeight) {
|
||||
bestWeight = Math.abs(weightChecking - weight);
|
||||
weightName = nameChecking;
|
||||
}
|
||||
});
|
||||
|
||||
return `${weightName}${isItalic ? " Italic" : ""} (${weight})`;
|
||||
}
|
||||
|
||||
let fontList: Promise<{ family: string; variants: string[]; files: Map<string, string> }[]> | undefined;
|
||||
|
||||
async function loadFontList(): Promise<{ family: string; variants: string[]; files: Map<string, string> }[]> {
|
||||
if (fontList) return fontList;
|
||||
|
||||
fontList = new Promise<{ family: string; variants: string[]; files: Map<string, string> }[]>((resolve) => {
|
||||
fetch(fontListAPI)
|
||||
.then((response) => response.json())
|
||||
.then((fontListResponse) => {
|
||||
const fontListData = fontListResponse.items as { family: string; variants: string[]; files: Record<string, string> }[];
|
||||
const result = fontListData.map((font) => {
|
||||
const { family } = font;
|
||||
const variants = font.variants.map(formatFontStyleName);
|
||||
const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]]));
|
||||
return { family, variants, files };
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
return fontList;
|
||||
}
|
||||
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontLoad, async (triggerFontLoad) => {
|
||||
const url = await getFontFileUrl(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle);
|
||||
if (url) {
|
||||
const response = await (await fetch(url)).arrayBuffer();
|
||||
editor.handle.onFontLoad(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle, url, new Uint8Array(response));
|
||||
} else {
|
||||
editor.handle.errorDialog("Failed to load font", `The font ${triggerFontLoad.font.fontFamily} with style ${triggerFontLoad.font.fontStyle} does not exist`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
fontNames,
|
||||
getFontStyles,
|
||||
getFontFileUrl,
|
||||
};
|
||||
}
|
||||
export type FontsState = ReturnType<typeof createFontsState>;
|
||||
|
||||
const fontListAPI = "https://api.graphite.art/font-list";
|
||||
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
|
||||
const weightNameMapping = new Map([
|
||||
[100, "Thin"],
|
||||
[200, "Extra Light"],
|
||||
[300, "Light"],
|
||||
[400, "Regular"],
|
||||
[500, "Medium"],
|
||||
[600, "Semi Bold"],
|
||||
[700, "Bold"],
|
||||
[800, "Extra Bold"],
|
||||
[900, "Black"],
|
||||
[950, "Extra Black"],
|
||||
]);
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { SendShortcutAltClick, SendShortcutF11, SendShortcutShiftClick, type ActionShortcut } from "@graphite/messages";
|
||||
|
||||
const SHOW_TOOLTIP_DELAY_MS = 500;
|
||||
|
||||
export function createTooltipState() {
|
||||
export function createTooltipState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
visible: false,
|
||||
element: undefined as Element | undefined,
|
||||
position: { x: 0, y: 0 },
|
||||
shiftClickShortcut: undefined as ActionShortcut | undefined,
|
||||
altClickShortcut: undefined as ActionShortcut | undefined,
|
||||
f11Shortcut: undefined as ActionShortcut | undefined,
|
||||
});
|
||||
|
||||
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
|
|
@ -45,6 +51,25 @@ export function createTooltipState() {
|
|||
}, SHOW_TOOLTIP_DELAY_MS);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutShiftClick, async (data) => {
|
||||
update((state) => {
|
||||
state.shiftClickShortcut = data.shortcut;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => {
|
||||
update((state) => {
|
||||
state.altClickShortcut = data.shortcut;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => {
|
||||
update((state) => {
|
||||
state.f11Shortcut = data.shortcut;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("mousedown", closeTooltip);
|
||||
document.addEventListener("keydown", closeTooltip);
|
||||
|
||||
|
|
|
|||
|
|
@ -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::Platform;
|
||||
use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily, Platform};
|
||||
use editor::messages::prelude::*;
|
||||
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
||||
use graph_craft::document::NodeId;
|
||||
|
|
@ -116,7 +116,6 @@ impl EditorHandle {
|
|||
#[cfg(not(feature = "native"))]
|
||||
fn dispatch<T: Into<Message>>(&self, message: T) {
|
||||
// Process no further messages after a crash to avoid spamming the console
|
||||
|
||||
use crate::MESSAGE_BUFFER;
|
||||
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||
return;
|
||||
|
|
@ -330,21 +329,43 @@ impl EditorHandle {
|
|||
|
||||
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
|
||||
#[wasm_bindgen(js_name = widgetValueUpdate)]
|
||||
pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)
|
||||
}
|
||||
|
||||
/// Commit the value of a given UI widget to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommit)]
|
||||
pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
self.widget_value_commit_helper(layout_target, widget_id, value)
|
||||
}
|
||||
|
||||
/// Update the value of a given UI widget, and commit it to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommitAndUpdate)]
|
||||
pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
self.widget_value_commit_helper(layout_target.clone(), widget_id, value.clone())?;
|
||||
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn widget_value_update_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
let widget_id = WidgetId(widget_id);
|
||||
match (from_value(layout_target), from_value(value)) {
|
||||
(Ok(layout_target), Ok(value)) => {
|
||||
let message = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value };
|
||||
self.dispatch(message);
|
||||
|
||||
if resend_widget {
|
||||
let resend_message = LayoutMessage::ResendActiveWidget { layout_target, widget_id };
|
||||
self.dispatch(resend_message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(target, val) => Err(Error::new(&format!("Could not update UI\nDetails:\nTarget: {target:?}\nValue: {val:?}")).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit the value of a given UI widget to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommit)]
|
||||
pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
pub fn widget_value_commit_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
let widget_id = WidgetId(widget_id);
|
||||
match (from_value(layout_target), from_value(value)) {
|
||||
(Ok(layout_target), Ok(value)) => {
|
||||
|
|
@ -356,14 +377,6 @@ impl EditorHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update the value of a given UI widget, and commit it to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommitAndUpdate)]
|
||||
pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
self.widget_value_commit(layout_target.clone(), widget_id, value.clone())?;
|
||||
self.widget_value_update(layout_target, widget_id, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadPreferences)]
|
||||
pub fn load_preferences(&self, preferences: Option<String>) {
|
||||
let preferences = if let Some(preferences) = preferences {
|
||||
|
|
@ -562,15 +575,21 @@ impl EditorHandle {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The font catalog has been loaded
|
||||
#[wasm_bindgen(js_name = onFontCatalogLoad)]
|
||||
pub fn on_font_catalog_load(&self, catalog: JsValue) -> Result<(), JsValue> {
|
||||
// Deserializing from TS type: `{ name: string; styles: { weight: number, italic: boolean, url: string }[] }[]`
|
||||
let families = serde_wasm_bindgen::from_value::<Vec<FontCatalogFamily>>(catalog)?;
|
||||
let message = PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(families) };
|
||||
self.dispatch(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A font has been downloaded
|
||||
#[wasm_bindgen(js_name = onFontLoad)]
|
||||
pub fn on_font_load(&self, font_family: String, font_style: String, preview_url: String, data: Vec<u8>) -> Result<(), JsValue> {
|
||||
let message = PortfolioMessage::FontLoaded {
|
||||
font_family,
|
||||
font_style,
|
||||
preview_url,
|
||||
data,
|
||||
};
|
||||
pub fn on_font_load(&self, font_family: String, font_style: String, data: Vec<u8>) -> Result<(), JsValue> {
|
||||
let message = PortfolioMessage::FontLoaded { font_family, font_style, data };
|
||||
self.dispatch(message);
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
855
libraries/rawkit/Cargo.lock
generated
855
libraries/rawkit/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -3,8 +3,8 @@ members = ["rawkit-proc-macros"]
|
|||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
quote = "1.0.37"
|
||||
syn = "2.0.87"
|
||||
quote = "1.0"
|
||||
syn = "2.0"
|
||||
|
||||
[package]
|
||||
name = "rawkit"
|
||||
|
|
@ -28,12 +28,12 @@ rawkit-tests = ["dep:image", "dep:libraw-rs", "dep:reqwest", "dep:rayon"]
|
|||
rawkit-proc-macros = { version = "0.1.0", path = "rawkit-proc-macros" }
|
||||
|
||||
# Required dependencies
|
||||
bitstream-io = "2.5.3"
|
||||
num_enum = "0.7.3"
|
||||
thiserror = "1.0.66"
|
||||
bitstream-io = "4.9.0"
|
||||
num_enum = "0.7.5"
|
||||
thiserror = "2.0.17"
|
||||
|
||||
# Optional dependencies (should be dev dependencies, but Cargo currently doesn't allow optional dev dependencies)
|
||||
image = { version = "0.25.4", optional = true }
|
||||
reqwest = { version = "0.12.9", optional = true, features = ["blocking"] }
|
||||
image = { version = "0.25.9", optional = true }
|
||||
reqwest = { version = "0.12.26", optional = true, features = ["blocking"] }
|
||||
libraw-rs = { version = "0.0.4", optional = true }
|
||||
rayon = { version = "1.10.0", optional = true }
|
||||
rayon = { version = "1.11.0", optional = true }
|
||||
|
|
|
|||
|
|
@ -19,5 +19,5 @@ quote = { workspace = true }
|
|||
syn = { workspace = true }
|
||||
|
||||
# Required dependencies
|
||||
toml = "0.8.19"
|
||||
proc-macro2 = "1.0.89"
|
||||
toml = "0.9.10+spec-1.1.0"
|
||||
proc-macro2 = "1.0.103"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ fn read_and_huffman_decode_file<R: Read + Seek, E: Endianness>(huff: &[u16], fil
|
|||
let huffman_table = &huff[1..];
|
||||
|
||||
// `number_of_bits` will be no more than 32, so the result is put into a u32
|
||||
let bits: u32 = file.read(number_of_bits).unwrap();
|
||||
let bits: u32 = file.read_var(number_of_bits).unwrap();
|
||||
let bits = bits as usize;
|
||||
|
||||
let bits_to_seek_from = huffman_table[bits].to_le_bytes()[1] as i64 - number_of_bits as i64;
|
||||
|
|
@ -49,7 +49,7 @@ fn read_and_huffman_decode_file<R: Read + Seek, E: Endianness>(huff: &[u16], fil
|
|||
|
||||
fn read_n_bits_from_file<R: Read + Seek, E: Endianness>(number_of_bits: u32, file: &mut BitReader<R, E>) -> u32 {
|
||||
// `number_of_bits` will be no more than 32, so the result is put into a u32
|
||||
file.read(number_of_bits).unwrap()
|
||||
file.read_var(number_of_bits).unwrap()
|
||||
}
|
||||
|
||||
/// ljpeg is a lossless variant of JPEG which gets used for decoding the embedded (thumbnail) preview images in raw files
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ pub struct OriginalLocation {
|
|||
pub dependants: Vec<Vec<NodeId>>,
|
||||
/// A list of flags indicating whether the input is exposed in the UI
|
||||
pub inputs_exposed: Vec<bool>,
|
||||
/// For automatically inserted Convert and Into nodes, if there is an error, display it on the node it is connect to.
|
||||
pub auto_convert_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for DocumentNode {
|
||||
|
|
@ -664,12 +666,9 @@ impl NodeNetwork {
|
|||
if node.original_location.path.is_some() {
|
||||
log::warn!("Attempting to overwrite node path");
|
||||
} else {
|
||||
node.original_location = OriginalLocation {
|
||||
path: Some(new_path),
|
||||
inputs_exposed: node.inputs.iter().map(|input| input.is_exposed()).collect(),
|
||||
dependants: (0..node.implementation.output_count()).map(|_| Vec::new()).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
node.original_location.path = Some(new_path);
|
||||
node.original_location.inputs_exposed = node.inputs.iter().map(|input| input.is_exposed()).collect();
|
||||
node.original_location.dependants = (0..node.implementation.output_count()).map(|_| Vec::new()).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -539,11 +539,23 @@ impl ProtoNetwork {
|
|||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GraphErrorType {
|
||||
NodeNotFound(NodeId),
|
||||
UnexpectedGenerics { index: usize, inputs: Vec<Type> },
|
||||
UnexpectedGenerics {
|
||||
index: usize,
|
||||
inputs: Vec<Type>,
|
||||
},
|
||||
NoImplementations,
|
||||
NoConstructor,
|
||||
InvalidImplementations { inputs: String, error_inputs: Vec<Vec<(usize, (Type, Type))>> },
|
||||
MultipleImplementations { inputs: String, valid: Vec<NodeIOTypes> },
|
||||
/// The `inputs` represents a formatted list of input indices corresponding to their types.
|
||||
/// Each element in `error_inputs` represents a valid `NodeIOTypes` implementation.
|
||||
/// The inner Vec stores the inputs which need to be changed and what type each needs to be changed to.
|
||||
InvalidImplementations {
|
||||
inputs: String,
|
||||
error_inputs: Vec<Vec<(usize, (Type, Type))>>,
|
||||
},
|
||||
MultipleImplementations {
|
||||
inputs: String,
|
||||
valid: Vec<NodeIOTypes>,
|
||||
},
|
||||
}
|
||||
impl Debug for GraphErrorType {
|
||||
// TODO: format with the document graph context so the input index is the same as in the graph UI.
|
||||
|
|
@ -756,9 +768,11 @@ impl TypingContext {
|
|||
|
||||
match valid_impls.as_slice() {
|
||||
[] => {
|
||||
let convert_node_index_offset = node.original_location.auto_convert_index.unwrap_or(0);
|
||||
let mut best_errors = usize::MAX;
|
||||
let mut error_inputs = Vec::new();
|
||||
for node_io in impls.keys() {
|
||||
// For errors on Convert nodes, offset the input index so it correctly corresponds to the node it is connected to.
|
||||
let current_errors = [call_argument]
|
||||
.into_iter()
|
||||
.chain(&inputs)
|
||||
|
|
@ -766,10 +780,7 @@ impl TypingContext {
|
|||
.zip([&node_io.call_argument].into_iter().chain(&node_io.inputs).cloned())
|
||||
.enumerate()
|
||||
.filter(|(_, (p1, p2))| !valid_type(p1, p2))
|
||||
.map(|(index, ty)| {
|
||||
let i = node.original_location.inputs(index).min_by_key(|s| s.node.len()).map(|s| s.index).unwrap_or(index);
|
||||
(i, ty)
|
||||
})
|
||||
.map(|(index, expected)| (index - 1 + convert_node_index_offset, expected))
|
||||
.collect::<Vec<_>>();
|
||||
if current_errors.len() < best_errors {
|
||||
best_errors = current_errors.len();
|
||||
|
|
@ -783,7 +794,7 @@ impl TypingContext {
|
|||
.into_iter()
|
||||
.chain(&inputs)
|
||||
.enumerate()
|
||||
.filter_map(|(i, t)| if i == 0 { None } else { Some(format!("• Input {i}: {t}")) })
|
||||
.filter_map(|(i, t)| if i == 0 { None } else { Some(format!("• Input {}: {t}", i + convert_node_index_offset)) })
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })])
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
|
|||
pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5;
|
||||
|
||||
// Fonts
|
||||
pub const DEFAULT_FONT_FAMILY: &str = "Cabin";
|
||||
pub const DEFAULT_FONT_FAMILY: &str = "Lato";
|
||||
pub const DEFAULT_FONT_STYLE: &str = "Regular (400)";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use glam::DVec2;
|
|||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez};
|
||||
use std::ops::Sub;
|
||||
|
||||
/// Represents different ways of calculating the centroid.
|
||||
/// Represents different geometric interpretations of calculating the centroid (center of mass).
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum CentroidType {
|
||||
|
|
|
|||
|
|
@ -194,7 +194,6 @@ impl From<Fill> for FillChoice {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum describing the type of [Fill].
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ fn text<'i: 'n>(
|
|||
_: impl Ctx,
|
||||
editor: &'i WasmEditorApi,
|
||||
text: String,
|
||||
font_name: Font,
|
||||
font: Font,
|
||||
#[unit(" px")]
|
||||
#[default(24.)]
|
||||
font_size: f64,
|
||||
|
|
@ -39,5 +39,5 @@ fn text<'i: 'n>(
|
|||
align,
|
||||
};
|
||||
|
||||
to_path(&text, &font_name, &editor.font_cache, typesetting, per_glyph_instances)
|
||||
to_path(&text, &font, &editor.font_cache, typesetting, per_glyph_instances)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ fn math<T: num_traits::float::Float>(
|
|||
/// The value of "A" when calculating the expression.
|
||||
#[implementations(f64, f32)]
|
||||
operand_a: T,
|
||||
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2".
|
||||
/// A math expression that may incorporate "A" and/or "B", such as `sqrt(A + B) - B^2`.
|
||||
#[default(A + B)]
|
||||
expression: String,
|
||||
/// The value of "B" when calculating the expression.
|
||||
|
|
@ -76,98 +76,113 @@ fn math<T: num_traits::float::Float>(
|
|||
}
|
||||
}
|
||||
|
||||
/// The addition operation (+) calculates the sum of two numbers.
|
||||
/// The addition operation (`+`) calculates the sum of two scalar numbers or vectors.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn add<U: Add<T>, T>(
|
||||
fn add<A: Add<B>, B>(
|
||||
_: impl Ctx,
|
||||
/// The left-hand side of the addition operation.
|
||||
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
|
||||
augend: U,
|
||||
augend: A,
|
||||
/// The right-hand side of the addition operation.
|
||||
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
|
||||
addend: T,
|
||||
) -> <U as Add<T>>::Output {
|
||||
addend: B,
|
||||
) -> <A as Add<B>>::Output {
|
||||
augend + addend
|
||||
}
|
||||
|
||||
/// The subtraction operation (-) calculates the difference between two numbers.
|
||||
/// The subtraction operation (`-`) calculates the difference between two scalar numbers or vectors.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn subtract<U: Sub<T>, T>(
|
||||
fn subtract<A: Sub<B>, B>(
|
||||
_: impl Ctx,
|
||||
/// The left-hand side of the subtraction operation.
|
||||
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
|
||||
minuend: U,
|
||||
minuend: A,
|
||||
/// The right-hand side of the subtraction operation.
|
||||
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
|
||||
subtrahend: T,
|
||||
) -> <U as Sub<T>>::Output {
|
||||
subtrahend: B,
|
||||
) -> <A as Sub<B>>::Output {
|
||||
minuend - subtrahend
|
||||
}
|
||||
|
||||
/// The multiplication operation (×) calculates the product of two numbers.
|
||||
/// The multiplication operation (`×`) calculates the product of two scalar numbers, vectors, or transforms.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn multiply<U: Mul<T>, T>(
|
||||
fn multiply<A: Mul<B>, B>(
|
||||
_: impl Ctx,
|
||||
/// The left-hand side of the multiplication operation.
|
||||
#[implementations(f64, f32, u32, DVec2, f64, DVec2, DAffine2)]
|
||||
multiplier: U,
|
||||
multiplier: A,
|
||||
/// The right-hand side of the multiplication operation.
|
||||
#[default(1.)]
|
||||
#[implementations(f64, f32, u32, DVec2, DVec2, f64, DAffine2)]
|
||||
multiplicand: T,
|
||||
) -> <U as Mul<T>>::Output {
|
||||
multiplicand: B,
|
||||
) -> <A as Mul<B>>::Output {
|
||||
multiplier * multiplicand
|
||||
}
|
||||
|
||||
/// The division operation (÷) calculates the quotient of two numbers.
|
||||
/// The division operation (`÷`) calculates the quotient of two scalar numbers or vectors.
|
||||
///
|
||||
/// Produces 0 if the denominator is 0.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>(
|
||||
fn divide<A: Div<B> + Default + PartialEq, B: Default + PartialEq>(
|
||||
_: impl Ctx,
|
||||
/// The left-hand side of the division operation.
|
||||
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
|
||||
numerator: U,
|
||||
numerator: A,
|
||||
/// The right-hand side of the division operation.
|
||||
#[default(1.)]
|
||||
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
|
||||
denominator: T,
|
||||
) -> <U as Div<T>>::Output
|
||||
denominator: B,
|
||||
) -> <A as Div<B>>::Output
|
||||
where
|
||||
<U as Div<T>>::Output: Default,
|
||||
<A as Div<B>>::Output: Default,
|
||||
{
|
||||
if denominator == T::default() {
|
||||
return <U as Div<T>>::Output::default();
|
||||
if denominator == B::default() {
|
||||
return <A as Div<B>>::Output::default();
|
||||
}
|
||||
numerator / denominator
|
||||
}
|
||||
|
||||
/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled.
|
||||
/// The reciprocal operation (`1/x`) calculates the multiplicative inverse of a number.
|
||||
///
|
||||
/// Produces 0 if the input is 0.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
|
||||
fn reciprocal<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The number for which the reciprocal is calculated.
|
||||
#[implementations(f64, f32)]
|
||||
value: T,
|
||||
) -> T {
|
||||
if value == T::from(0.).unwrap() { T::from(0.).unwrap() } else { T::from(1.).unwrap() / value }
|
||||
}
|
||||
|
||||
/// The modulo operation (`%`) calculates the remainder from the division of two scalar numbers or vectors.
|
||||
///
|
||||
/// The sign of the result shares the sign of the numerator unless *Always Positive* is enabled.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn modulo<A: Rem<B, Output: Add<B, Output: Rem<B, Output = A::Output>>>, B: Copy>(
|
||||
_: impl Ctx,
|
||||
/// The left-hand side of the modulo operation.
|
||||
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
|
||||
numerator: U,
|
||||
numerator: A,
|
||||
/// The right-hand side of the modulo operation.
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
|
||||
modulus: T,
|
||||
/// Ensures the result will always be positive, even if the numerator is negative.
|
||||
modulus: B,
|
||||
/// Ensures the result is always positive, even if the numerator is negative.
|
||||
#[default(true)]
|
||||
always_positive: bool,
|
||||
) -> <U as Rem<T>>::Output {
|
||||
) -> <A as Rem<B>>::Output {
|
||||
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
|
||||
}
|
||||
|
||||
/// The exponent operation (^) calculates the result of raising a number to a power.
|
||||
/// The exponent operation (`^`) calculates the result of raising a number to a power.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn exponent<T: Pow<T>>(
|
||||
_: impl Ctx,
|
||||
/// The base number that will be raised to the power.
|
||||
/// The base number that is raised to the power.
|
||||
#[implementations(f64, f32, u32)]
|
||||
base: T,
|
||||
/// The power to which the base number will be raised.
|
||||
/// The power to which the base number is raised.
|
||||
#[implementations(f64, f32, u32)]
|
||||
#[default(2.)]
|
||||
power: T,
|
||||
|
|
@ -175,15 +190,18 @@ fn exponent<T: Pow<T>>(
|
|||
base.pow(power)
|
||||
}
|
||||
|
||||
/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n.
|
||||
/// The `n`th root operation (`√`) calculates the inverse of exponentiation. Square root inverts squaring, cube root inverts cubing, and so on.
|
||||
///
|
||||
/// This is equivalent to raising the number to the power of `1/n`.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn root<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The number for which the nth root will be calculated.
|
||||
/// The number inside the radical for which the `n`th root is calculated.
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f32)]
|
||||
radicand: T,
|
||||
/// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on.
|
||||
/// Degrees 0 or less are invalid and will produce an output of 0.
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f32)]
|
||||
degree: T,
|
||||
|
|
@ -192,16 +210,18 @@ fn root<T: num_traits::float::Float>(
|
|||
radicand.sqrt()
|
||||
} else if degree == T::from(3.).unwrap() {
|
||||
radicand.cbrt()
|
||||
} else if degree <= T::from(0.).unwrap() {
|
||||
T::from(0.).unwrap()
|
||||
} else {
|
||||
radicand.powf(T::from(1.).unwrap() / degree)
|
||||
}
|
||||
}
|
||||
|
||||
/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e".
|
||||
/// The logarithmic function (`log`) calculates the logarithm of a number with a specified base. If the natural logarithm function (`ln`) is desired, set the base to "e".
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn logarithm<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The number for which the logarithm will be calculated.
|
||||
/// The number for which the logarithm is calculated.
|
||||
#[implementations(f64, f32)]
|
||||
value: T,
|
||||
/// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm).
|
||||
|
|
@ -220,7 +240,7 @@ fn logarithm<T: num_traits::float::Float>(
|
|||
}
|
||||
}
|
||||
|
||||
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
|
||||
/// The sine trigonometric function (`sin`) calculates the ratio of the angle's opposite side length to its hypotenuse length.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn sine<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
|
|
@ -233,7 +253,7 @@ fn sine<T: num_traits::float::Float>(
|
|||
if radians { theta.sin() } else { theta.to_radians().sin() }
|
||||
}
|
||||
|
||||
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
|
||||
/// The cosine trigonometric function (`cos`) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn cosine<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
|
|
@ -246,7 +266,7 @@ fn cosine<T: num_traits::float::Float>(
|
|||
if radians { theta.cos() } else { theta.to_radians().cos() }
|
||||
}
|
||||
|
||||
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
|
||||
/// The tangent trigonometric function (`tan`) calculates the ratio of the angle's opposite side length to its adjacent side length.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn tangent<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
|
|
@ -259,41 +279,43 @@ fn tangent<T: num_traits::float::Float>(
|
|||
if radians { theta.tan() } else { theta.to_radians().tan() }
|
||||
}
|
||||
|
||||
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
|
||||
/// The inverse sine trigonometric function (`asin`) calculates the angle whose sine is the input value.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn sine_inverse<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
|
||||
/// The given value for which the angle is calculated. Must be in the domain `[-1, 1]` (it will be clamped to -1 or 1 otherwise).
|
||||
#[implementations(f64, f32)]
|
||||
value: T,
|
||||
/// Whether the resulting angle should be given in as radians instead of degrees.
|
||||
radians: bool,
|
||||
) -> T {
|
||||
if radians { value.asin() } else { value.asin().to_degrees() }
|
||||
let angle = value.clamp(T::from(-1.).unwrap(), T::from(1.).unwrap()).asin();
|
||||
if radians { angle } else { angle.to_degrees() }
|
||||
}
|
||||
|
||||
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
|
||||
/// The inverse cosine trigonometric function (`acos`) calculates the angle whose cosine is the input value.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn cosine_inverse<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
|
||||
/// The given value for which the angle is calculated. Must be in the domain `[-1, 1]` (it will be clamped to -1 or 1 otherwise).
|
||||
#[implementations(f64, f32)]
|
||||
value: T,
|
||||
/// Whether the resulting angle should be given in as radians instead of degrees.
|
||||
radians: bool,
|
||||
) -> T {
|
||||
if radians { value.acos() } else { value.acos().to_degrees() }
|
||||
let angle = value.clamp(T::from(-1.).unwrap(), T::from(1.).unwrap()).acos();
|
||||
if radians { angle } else { angle.to_degrees() }
|
||||
}
|
||||
|
||||
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
|
||||
/// atan: the angle whose tangent is the specified scalar number.
|
||||
/// atan2: the angle of a ray from the origin to the specified vec2.
|
||||
/// The inverse tangent trigonometric function (`atan` or `atan2`, depending on input type) calculates:
|
||||
/// `atan`: the angle whose tangent is the input scalar number.
|
||||
/// `atan2`: the angle of a ray from the origin to the input vec2.
|
||||
///
|
||||
/// The resulting angle is always in the range [-90°, 90°] or, in radians, [-π/2, π/2].
|
||||
/// The resulting angle is always in the range `[-90°, 90°]` or, in radians, `[-π/2, π/2]`.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn tangent_inverse<T: TangentInverse>(
|
||||
_: impl Ctx,
|
||||
/// The given value for which the angle will be calculated.
|
||||
/// The given value for which the angle is calculated.
|
||||
#[implementations(f64, f32, DVec2)]
|
||||
value: T,
|
||||
/// Whether the resulting angle should be given in as radians instead of degrees.
|
||||
|
|
@ -325,18 +347,30 @@ impl TangentInverse for DVec2 {
|
|||
}
|
||||
}
|
||||
|
||||
/// Linearly maps an input value from one range to another. The ranges may be reversed.
|
||||
///
|
||||
/// For example, 0.5 in the input range `[0, 1]` would map to 0 in the output range `[-180, 180]`.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn remap<U: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, f32)] value: U,
|
||||
#[implementations(f64, f32)] input_min: U,
|
||||
/// The value to be mapped between ranges.
|
||||
#[implementations(f64, f32)]
|
||||
value: U,
|
||||
/// The lower bound of the input range.
|
||||
#[implementations(f64, f32)]
|
||||
input_min: U,
|
||||
/// The upper bound of the input range.
|
||||
#[implementations(f64, f32)]
|
||||
#[default(1.)]
|
||||
input_max: U,
|
||||
#[implementations(f64, f32)] output_min: U,
|
||||
/// The lower bound of the output range.
|
||||
#[implementations(f64, f32)]
|
||||
output_min: U,
|
||||
/// The upper bound of the output range.
|
||||
#[implementations(f64, f32)]
|
||||
#[default(1.)]
|
||||
output_max: U,
|
||||
/// Whether to constrain the result within the output range instead of extrapolating beyond its bounds.
|
||||
clamped: bool,
|
||||
) -> U {
|
||||
let input_range = input_max - input_min;
|
||||
|
|
@ -363,17 +397,17 @@ fn remap<U: num_traits::float::Float>(
|
|||
}
|
||||
}
|
||||
|
||||
/// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
|
||||
/// The random function (`rand`) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn random(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
/// Seed to determine the unique variation of which number will be generated.
|
||||
/// Seed to determine the unique variation of which number is generated.
|
||||
seed: u64,
|
||||
/// The smaller end of the range within which the random number will be generated.
|
||||
/// The smaller end of the range within which the random number is generated.
|
||||
#[default(0.)]
|
||||
min: f64,
|
||||
/// The larger end of the range within which the random number will be generated.
|
||||
/// The larger end of the range within which the random number is generated.
|
||||
#[default(1.)]
|
||||
max: f64,
|
||||
) -> f64 {
|
||||
|
|
@ -404,89 +438,89 @@ fn to_f64(_: impl Ctx, value: f64) -> f64 {
|
|||
value
|
||||
}
|
||||
|
||||
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
|
||||
/// The rounding function (`round`) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn round<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The number which will be rounded.
|
||||
/// The number to be rounded to the nearest whole number.
|
||||
#[implementations(f64, f32)]
|
||||
value: T,
|
||||
) -> T {
|
||||
value.round()
|
||||
}
|
||||
|
||||
/// The floor function (floor) rounds down an input value to the nearest whole number, unless the input number is already whole.
|
||||
/// The floor function (`floor`) rounds down an input value to the nearest whole number, unless the input number is already whole.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn floor<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The number which will be rounded down.
|
||||
/// The number to be rounded down.
|
||||
#[implementations(f64, f32)]
|
||||
value: T,
|
||||
) -> T {
|
||||
value.floor()
|
||||
}
|
||||
|
||||
/// The ceiling function (ceil) rounds up an input value to the nearest whole number, unless the input number is already whole.
|
||||
/// The ceiling function (`ceil`) rounds up an input value to the nearest whole number, unless the input number is already whole.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn ceiling<T: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The number which will be rounded up.
|
||||
/// The number to be rounded up.
|
||||
#[implementations(f64, f32)]
|
||||
value: T,
|
||||
) -> T {
|
||||
value.ceil()
|
||||
}
|
||||
|
||||
/// The absolute value function (abs) removes the negative sign from an input value, if present.
|
||||
/// The absolute value function (`abs`) removes the negative sign from an input value, if present.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn absolute_value<T: num_traits::sign::Signed>(
|
||||
_: impl Ctx,
|
||||
/// The number which will be made positive.
|
||||
/// The number to be made positive.
|
||||
#[implementations(f64, f32, i32, i64)]
|
||||
value: T,
|
||||
) -> T {
|
||||
value.abs()
|
||||
}
|
||||
|
||||
/// The minimum function (min) picks the smaller of two numbers.
|
||||
/// The minimum function (`min`) picks the smaller of two numbers.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn min<T: std::cmp::PartialOrd>(
|
||||
_: impl Ctx,
|
||||
/// One of the two numbers, of which the lesser will be returned.
|
||||
/// One of the two numbers, of which the lesser is returned.
|
||||
#[implementations(f64, f32, u32, &str)]
|
||||
value: T,
|
||||
/// The other of the two numbers, of which the lesser will be returned.
|
||||
/// The other of the two numbers, of which the lesser is returned.
|
||||
#[implementations(f64, f32, u32, &str)]
|
||||
other_value: T,
|
||||
) -> T {
|
||||
if value < other_value { value } else { other_value }
|
||||
}
|
||||
|
||||
/// The maximum function (max) picks the larger of two numbers.
|
||||
/// The maximum function (`max`) picks the larger of two numbers.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn max<T: std::cmp::PartialOrd>(
|
||||
_: impl Ctx,
|
||||
/// One of the two numbers, of which the greater will be returned.
|
||||
/// One of the two numbers, of which the greater is returned.
|
||||
#[implementations(f64, f32, u32, &str)]
|
||||
value: T,
|
||||
/// The other of the two numbers, of which the greater will be returned.
|
||||
/// The other of the two numbers, of which the greater is returned.
|
||||
#[implementations(f64, f32, u32, &str)]
|
||||
other_value: T,
|
||||
) -> T {
|
||||
if value > other_value { value } else { other_value }
|
||||
}
|
||||
|
||||
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
|
||||
/// The clamp function (`clamp`) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn clamp<T: std::cmp::PartialOrd>(
|
||||
_: impl Ctx,
|
||||
/// The number to be clamped, which will be restricted to the range between the minimum and maximum values.
|
||||
/// The number to be clamped, which is restricted to the range between the minimum and maximum values.
|
||||
#[implementations(f64, f32, u32, &str)]
|
||||
value: T,
|
||||
/// The left (smaller) side of the range. The output will never be less than this number.
|
||||
/// The left (smaller) side of the range. The output is never less than this number.
|
||||
#[implementations(f64, f32, u32, &str)]
|
||||
min: T,
|
||||
/// The right (greater) side of the range. The output will never be greater than this number.
|
||||
/// The right (greater) side of the range. The output is never greater than this number.
|
||||
#[implementations(f64, f32, u32, &str)]
|
||||
max: T,
|
||||
) -> T {
|
||||
|
|
@ -504,10 +538,10 @@ fn clamp<T: std::cmp::PartialOrd>(
|
|||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn greatest_common_divisor<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops::SubAssign>(
|
||||
_: impl Ctx,
|
||||
/// One of the two numbers for which the GCD will be calculated.
|
||||
/// One of the two numbers for which the GCD is calculated.
|
||||
#[implementations(u32, u64, i32)]
|
||||
value: T,
|
||||
/// The other of the two numbers for which the GCD will be calculated.
|
||||
/// The other of the two numbers for which the GCD is calculated.
|
||||
#[implementations(u32, u64, i32)]
|
||||
other_value: T,
|
||||
) -> T {
|
||||
|
|
@ -524,10 +558,10 @@ fn greatest_common_divisor<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32
|
|||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn least_common_multiple<T: num_traits::ToPrimitive + num_traits::FromPrimitive + num_traits::identities::Zero>(
|
||||
_: impl Ctx,
|
||||
/// One of the two numbers for which the LCM will be calculated.
|
||||
/// One of the two numbers for which the LCM is calculated.
|
||||
#[implementations(u32, u64, i32)]
|
||||
value: T,
|
||||
/// The other of the two numbers for which the LCM will be calculated.
|
||||
/// The other of the two numbers for which the LCM is calculated.
|
||||
#[implementations(u32, u64, i32)]
|
||||
other_value: T,
|
||||
) -> T {
|
||||
|
|
@ -574,36 +608,8 @@ fn binary_gcd<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops:
|
|||
a << shift
|
||||
}
|
||||
|
||||
/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn equals<T: std::cmp::PartialEq<T>>(
|
||||
_: impl Ctx,
|
||||
/// One of the two numbers to compare for equality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
|
||||
value: T,
|
||||
/// The other of the two numbers to compare for equality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
|
||||
other_value: T,
|
||||
) -> bool {
|
||||
other_value == value
|
||||
}
|
||||
|
||||
/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn not_equals<T: std::cmp::PartialEq<T>>(
|
||||
_: impl Ctx,
|
||||
/// One of the two numbers to compare for inequality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str)]
|
||||
value: T,
|
||||
/// The other of the two numbers to compare for inequality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str)]
|
||||
other_value: T,
|
||||
) -> bool {
|
||||
other_value != value
|
||||
}
|
||||
|
||||
/// The less-than operation (<) compares two values and returns true if the first value is less than the second, or false if it is not.
|
||||
/// If enabled with "Or Equal", the less-than-or-equal operation (<=) will be used instead.
|
||||
/// The less-than operation (`<`) compares two values and returns true if the first value is less than the second, or false if it is not.
|
||||
/// If enabled with *Or Equal*, the less-than-or-equal operation (`<=`) is used instead.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn less_than<T: std::cmp::PartialOrd<T>>(
|
||||
_: impl Ctx,
|
||||
|
|
@ -613,14 +619,14 @@ fn less_than<T: std::cmp::PartialOrd<T>>(
|
|||
/// The number on the right-hand side of the comparison.
|
||||
#[implementations(f64, f32, u32)]
|
||||
other_value: T,
|
||||
/// Uses the less-than-or-equal operation (<=) instead of the less-than operation (<).
|
||||
/// Uses the less-than-or-equal operation (`<=`) instead of the less-than operation (`<`).
|
||||
or_equal: bool,
|
||||
) -> bool {
|
||||
if or_equal { value <= other_value } else { value < other_value }
|
||||
}
|
||||
|
||||
/// The greater-than operation (>) compares two values and returns true if the first value is greater than the second, or false if it is not.
|
||||
/// If enabled with "Or Equal", the greater-than-or-equal operation (>=) will be used instead.
|
||||
/// The greater-than operation (`>`) compares two values and returns true if the first value is greater than the second, or false if it is not.
|
||||
/// If enabled with *Or Equal*, the greater-than-or-equal operation (`>=`) is used instead.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn greater_than<T: std::cmp::PartialOrd<T>>(
|
||||
_: impl Ctx,
|
||||
|
|
@ -630,13 +636,41 @@ fn greater_than<T: std::cmp::PartialOrd<T>>(
|
|||
/// The number on the right-hand side of the comparison.
|
||||
#[implementations(f64, f32, u32)]
|
||||
other_value: T,
|
||||
/// Uses the greater-than-or-equal operation (>=) instead of the greater-than operation (>).
|
||||
/// Uses the greater-than-or-equal operation (`>=`) instead of the greater-than operation (`>`).
|
||||
or_equal: bool,
|
||||
) -> bool {
|
||||
if or_equal { value >= other_value } else { value > other_value }
|
||||
}
|
||||
|
||||
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
|
||||
/// The equality operation (`==`, `XNOR`) compares two values and returns true if they are equal, or false if they are not.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn equals<T: std::cmp::PartialEq<T>>(
|
||||
_: impl Ctx,
|
||||
/// One of the two values to compare for equality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
|
||||
value: T,
|
||||
/// The other of the two values to compare for equality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
|
||||
other_value: T,
|
||||
) -> bool {
|
||||
other_value == value
|
||||
}
|
||||
|
||||
/// The inequality operation (`!=`, `XOR`) compares two values and returns true if they are not equal, or false if they are.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn not_equals<T: std::cmp::PartialEq<T>>(
|
||||
_: impl Ctx,
|
||||
/// One of the two values to compare for inequality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str)]
|
||||
value: T,
|
||||
/// The other of the two values to compare for inequality.
|
||||
#[implementations(f64, f32, u32, DVec2, bool, &str)]
|
||||
other_value: T,
|
||||
) -> bool {
|
||||
other_value != value
|
||||
}
|
||||
|
||||
/// The logical OR operation (`||`) returns true if either of the two inputs are true, or false if both are false.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn logical_or(
|
||||
_: impl Ctx,
|
||||
|
|
@ -648,7 +682,7 @@ fn logical_or(
|
|||
value || other_value
|
||||
}
|
||||
|
||||
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
|
||||
/// The logical AND operation (`&&`) returns true if both of the two inputs are true, or false if any are false.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn logical_and(
|
||||
_: impl Ctx,
|
||||
|
|
@ -660,7 +694,7 @@ fn logical_and(
|
|||
value && other_value
|
||||
}
|
||||
|
||||
/// The logical not operation (!) reverses true and false value of the input.
|
||||
/// The logical NOT operation (`!`) reverses true and false value of the input.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn logical_not(
|
||||
_: impl Ctx,
|
||||
|
|
@ -736,20 +770,84 @@ fn footprint_value(_: impl Ctx, _primary: (), transform: DAffine2, #[default(100
|
|||
}
|
||||
}
|
||||
|
||||
/// The dot product operation (`·`) calculates the degree of similarity of a vec2 pair based on their angles and lengths.
|
||||
///
|
||||
/// Calculated as `‖a‖‖b‖cos(θ)`, it represents the product of their lengths (`‖a‖‖b‖`) scaled by the alignment of their directions (`cos(θ)`).
|
||||
/// The output ranges from the positive to negative product of their lengths based on when they are pointing in the same or opposite directions.
|
||||
/// If any vector has zero length, the output is 0.
|
||||
#[node_macro::node(category("Math: Vector"))]
|
||||
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
|
||||
vector_a.dot(vector_b)
|
||||
fn dot_product(
|
||||
_: impl Ctx,
|
||||
/// An operand of the dot product operation.
|
||||
vector_a: DVec2,
|
||||
/// The other operand of the dot product operation.
|
||||
#[default((1., 0.))]
|
||||
vector_b: DVec2,
|
||||
/// Whether to normalize both input vectors so the calculation ranges in `[-1, 1]` by considering only their degree of directional alignment.
|
||||
normalize: bool,
|
||||
) -> f64 {
|
||||
if normalize {
|
||||
vector_a.normalize_or_zero().dot(vector_b.normalize_or_zero())
|
||||
} else {
|
||||
vector_a.dot(vector_b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the length or magnitude of a vector.
|
||||
/// Calculates the angle swept between two vectors.
|
||||
///
|
||||
/// The value is always positive and ranges from 0° (both vectors point the same direction) to 180° (both vectors point opposite directions).
|
||||
#[node_macro::node(category("Math: Vector"))]
|
||||
fn angle_between(_: impl Ctx, vector_a: DVec2, vector_b: DVec2, radians: bool) -> f64 {
|
||||
let dot_product = vector_a.normalize_or_zero().dot(vector_b.normalize_or_zero());
|
||||
let angle = dot_product.acos();
|
||||
if radians { angle } else { angle.to_degrees() }
|
||||
}
|
||||
|
||||
pub trait ToPosition {
|
||||
fn to_position(self) -> DVec2;
|
||||
}
|
||||
impl ToPosition for DVec2 {
|
||||
fn to_position(self) -> DVec2 {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl ToPosition for DAffine2 {
|
||||
fn to_position(self) -> DVec2 {
|
||||
self.translation
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the angle needed for a rightward-facing object placed at the observer position to turn so it points toward the target position.
|
||||
#[node_macro::node(category("Math: Vector"))]
|
||||
fn angle_to<T: ToPosition, U: ToPosition>(
|
||||
_: impl Ctx,
|
||||
/// The position from which the angle is measured.
|
||||
#[implementations(DVec2, DAffine2, DVec2, DAffine2)]
|
||||
observer: T,
|
||||
/// The position toward which the angle is measured.
|
||||
#[expose]
|
||||
#[implementations(DVec2, DVec2, DAffine2, DAffine2)]
|
||||
target: U,
|
||||
/// Whether the resulting angle should be given in as radians instead of degrees.
|
||||
radians: bool,
|
||||
) -> f64 {
|
||||
let from = observer.to_position();
|
||||
let to = target.to_position();
|
||||
let delta = to - from;
|
||||
let angle = delta.y.atan2(delta.x);
|
||||
if radians { angle } else { angle.to_degrees() }
|
||||
}
|
||||
|
||||
// TODO: Rename to "Magnitude"
|
||||
/// The magnitude operator (`‖x‖`) calculates the length of a vec2, which is the distance from the base to the tip of the arrow represented by the vector.
|
||||
#[node_macro::node(category("Math: Vector"))]
|
||||
fn length(_: impl Ctx, vector: DVec2) -> f64 {
|
||||
vector.length()
|
||||
}
|
||||
|
||||
/// Scales the input vector to unit length while preserving it's direction. This is equivalent to dividing the input vector by it's own magnitude.
|
||||
/// Scales the input vector to unit length while preserving its direction. This is equivalent to dividing the input vector by its own magnitude.
|
||||
///
|
||||
/// Returns zero when the input vector is zero.
|
||||
/// Returns 0 when the input vector has zero length.
|
||||
#[node_macro::node(category("Math: Vector"))]
|
||||
fn normalize(_: impl Ctx, vector: DVec2) -> DVec2 {
|
||||
vector.normalize_or_zero()
|
||||
|
|
@ -765,7 +863,7 @@ mod test {
|
|||
pub fn dot_product_function() {
|
||||
let vector_a = DVec2::new(1., 2.);
|
||||
let vector_b = DVec2::new(3., 4.);
|
||||
assert_eq!(dot_product((), vector_a, vector_b), 11.);
|
||||
assert_eq!(dot_product((), vector_a, vector_b, false), 11.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -574,7 +574,6 @@ fn vibrance<T: Adjust<Color>>(
|
|||
image
|
||||
}
|
||||
|
||||
/// Color Channel
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
|
|
@ -586,7 +585,6 @@ pub enum RedGreenBlue {
|
|||
Blue,
|
||||
}
|
||||
|
||||
/// Color Channel
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
#[widget(Radio)]
|
||||
|
|
@ -599,7 +597,7 @@ pub enum RedGreenBlueAlpha {
|
|||
Alpha,
|
||||
}
|
||||
|
||||
/// Style of noise pattern
|
||||
/// Style of noise pattern.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
#[widget(Dropdown)]
|
||||
|
|
@ -616,9 +614,9 @@ pub enum NoiseType {
|
|||
WhiteNoise,
|
||||
}
|
||||
|
||||
/// Style of layered levels of the noise pattern.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
/// Style of layered levels of the noise pattern
|
||||
pub enum FractalType {
|
||||
#[default]
|
||||
None,
|
||||
|
|
@ -632,7 +630,7 @@ pub enum FractalType {
|
|||
DomainWarpIndependent,
|
||||
}
|
||||
|
||||
/// Distance function used by the cellular noise
|
||||
/// Distance function used by the cellular noise.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
pub enum CellularDistanceFunction {
|
||||
|
|
@ -663,7 +661,6 @@ pub enum CellularReturnType {
|
|||
Division,
|
||||
}
|
||||
|
||||
/// Type of domain warp
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
#[widget(Dropdown)]
|
||||
|
|
|
|||
|
|
@ -7,16 +7,55 @@ use std::sync::Arc;
|
|||
use core_types::specta;
|
||||
|
||||
/// A font type (storing font family and font style and an optional preview URL)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, core_types::specta::Type)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, DynAny, core_types::specta::Type)]
|
||||
pub struct Font {
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
#[serde(rename = "fontStyle", deserialize_with = "migrate_font_style")]
|
||||
pub font_style: String,
|
||||
#[serde(skip)]
|
||||
pub font_style_to_restore: Option<String>,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Font {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.font_family.hash(state);
|
||||
self.font_style.hash(state);
|
||||
// Don't consider `font_style_to_restore` in the HashMaps
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Font {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// Don't consider `font_style_to_restore` in the HashMaps
|
||||
self.font_family == other.font_family && self.font_style == other.font_style
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(font_family: String, font_style: String) -> Self {
|
||||
Self { font_family, font_style }
|
||||
Self {
|
||||
font_family,
|
||||
font_style,
|
||||
font_style_to_restore: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn named_weight(weight: u32) -> &'static str {
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
|
||||
match weight {
|
||||
100 => "Thin",
|
||||
200 => "Extra Light",
|
||||
300 => "Light",
|
||||
400 => "Regular",
|
||||
500 => "Medium",
|
||||
600 => "Semi Bold",
|
||||
700 => "Bold",
|
||||
800 => "Extra Bold",
|
||||
900 => "Black",
|
||||
950 => "Extra Black",
|
||||
_ => "Regular",
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for Font {
|
||||
|
|
@ -24,21 +63,33 @@ impl Default for Font {
|
|||
Self::new(core_types::consts::DEFAULT_FONT_FAMILY.into(), core_types::consts::DEFAULT_FONT_STYLE.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Default, DynAny)]
|
||||
pub struct FontCache {
|
||||
/// Actual font file data used for rendering a font
|
||||
font_file_data: HashMap<Font, Vec<u8>>,
|
||||
/// Web font preview URLs used for showing fonts when live editing
|
||||
preview_urls: HashMap<Font, String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FontCache {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FontCache")
|
||||
.field("font_file_data", &self.font_file_data.keys().collect::<Vec<_>>())
|
||||
.field("preview_urls", &self.preview_urls)
|
||||
.finish()
|
||||
f.debug_struct("FontCache").field("font_file_data", &self.font_file_data.keys().collect::<Vec<_>>()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for FontCache {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.font_file_data.len().hash(state);
|
||||
self.font_file_data.keys().for_each(|font| font.hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for FontCache {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.font_file_data.len() != other.font_file_data.len() {
|
||||
return false;
|
||||
}
|
||||
self.font_file_data.keys().all(|font| other.font_file_data.contains_key(font))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,26 +121,8 @@ impl FontCache {
|
|||
}
|
||||
|
||||
/// Insert a new font into the cache
|
||||
pub fn insert(&mut self, font: Font, perview_url: String, data: Vec<u8>) {
|
||||
pub fn insert(&mut self, font: Font, data: Vec<u8>) {
|
||||
self.font_file_data.insert(font.clone(), data);
|
||||
self.preview_urls.insert(font, perview_url);
|
||||
}
|
||||
|
||||
/// Gets the preview URL for showing in text field when live editing
|
||||
pub fn get_preview_url(&self, font: &Font) -> Option<&String> {
|
||||
self.preview_urls.get(font)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for FontCache {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.preview_urls.len().hash(state);
|
||||
self.preview_urls.iter().for_each(|(font, url)| {
|
||||
font.hash(state);
|
||||
url.hash(state)
|
||||
});
|
||||
self.font_file_data.len().hash(state);
|
||||
self.font_file_data.keys().for_each(|font| font.hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,11 +86,13 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
|
|||
} else {
|
||||
identity_node.clone()
|
||||
};
|
||||
|
||||
let mut original_location = OriginalLocation::default();
|
||||
original_location.auto_convert_index = Some(i);
|
||||
DocumentNode {
|
||||
inputs,
|
||||
implementation: DocumentNodeImplementation::ProtoNode(proto_node),
|
||||
visible: true,
|
||||
original_location,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
website/LICENSE.txt
Normal file
0
website/LICENSE.txt
Normal file
|
|
@ -12,11 +12,11 @@ css = ["/layout/reading-material.css"]
|
|||
|
||||
<article>
|
||||
|
||||
Graphite is open source software built by its community and distributed by *Graphite Labs, LLC*, the official [organization](/about#organization) that operates the project. Builds of the application are free to use by anyone for any purpose, even commercially. The artwork you produce is solely yours.
|
||||
Graphite is open source software made by its community and distributed by *Graphite Labs, LLC*, the official [organization](/about#organization) that operates the project. Builds of the application are free to use by anyone for any purpose, even commercially. The artwork you produce is solely yours.
|
||||
|
||||
The source code [available on GitHub](https://github.com/GraphiteEditor/Graphite) (including the Graphite editor application, libraries, and other software materials) is shared under the Apache 2.0 license posted below, unless otherwise noted within the repository.
|
||||
## Source code
|
||||
|
||||
Visual assets, including but not limited to brand graphics, logos, fonts, icon sets, SVG files, and sample artworks are excluded from this code license and held under copyright by their respective owners. Derivative works must substitute these assets before distributing modified releases of the software. This protects the integrity of the [brand](/logo) and encourages the community to [contribute](/volunteer) back to the official project instead of fragmenting the ecosystem.
|
||||
The source code [available on GitHub](https://github.com/GraphiteEditor/Graphite) (including the Graphite editor application, libraries, and other software materials) is shared under the Apache License 2.0 posted below, unless otherwise noted within the repository.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -197,6 +197,28 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
## Branding
|
||||
|
||||
Assets that contribute to the visual identity of the Graphite brand and software (including [logos](/logo) and icon sets) are not considered software code, and are licensed separately from the [source code license](#source-code) described above. These are kept in a [separate repository](https://github.com/Keavon/graphite-branded-assets/) because they are designed independently and are not within scope of the open source project.
|
||||
|
||||
Official builds of Graphite may be compiled and redistributed freely under the terms of the Graphite Branding License, but derivative works (i.e. forks) must substitute these assets before distributing modified releases of the software. This protects the integrity of the brand and encourages developers to [contribute](/volunteer) back in support of the official community-based project instead of fragmenting the ecosystem with lookalikes.
|
||||
|
||||
---
|
||||
|
||||
Graphite Branding License
|
||||
|
||||
Copyright (c) 2021-2025 Graphite Labs, LLC. All rights reserved.
|
||||
|
||||
This repository includes proprietary assets (the "Assets"), including but not limited to logos, icons, and branding materials. The Assets are not software source code and are consequently not licensed under the same terms as the other works of Graphite software materials. The intention of this license is to maintain the full permissiveness of the software code while reserving protections for the brand and visual identity of the official Graphite product.
|
||||
|
||||
Permission is granted to use, reproduce, and distribute the Assets solely as part of unmodified build artifacts produced from the official Graphite repository at https://github.com/GraphiteEditor/Graphite, on master branch commits, using the official build process as documented therein. Any other use of the Assets—including copying, extraction, incorporation into modified or forked builds, or use in other projects or contexts—is prohibited without prior written permission from the copyright holder.
|
||||
|
||||
This license does not grant any rights under trademark law or affect the licensing of the software source code.
|
||||
|
||||
THE ASSETS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE ASSETS.
|
||||
|
||||
This license is automatically terminated if the terms herein are violated.
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ The hexagon represents one unit in the lattice grid chemical structure of graphe
|
|||
|
||||
## Usage policy
|
||||
|
||||
The Graphite logo is made available for community use, with some limitations. While the software is free and open source, the brand identity is more restrictive. Please be respectful of the Graphite brand by reviewing the usage policy.
|
||||
The Graphite logo is made available for community use, with some limitations. While the software is free and open source, the brand identity is more restrictive. Please be respectful of the Graphite brand by reviewing this usage policy.
|
||||
|
||||
Please be advised that the logo is not part of the software's Apache 2.0 [license](/license). Users of the logo must adhere to the usage policy:
|
||||
Be aware that the logo is not covered under the [Apache License 2.0](/license#source-code) of Graphite's source code. Its usage within Graphite software distributions is covered by the [Graphite Branding License](/license#branding).
|
||||
|
||||
Outside the context of the Graphite application (or derivatives), community usage of the logo must adhere to the usage policy:
|
||||
|
||||
1. Do not use the Graphite logo as your own. It should not be used as your primary—or most visually prominent—branding, and may never be incorporated into your own logo. Your usage should not imply that it's a part of the official Graphite project, nor that it's endorsed or affiliated.
|
||||
2. Your (product, service, website, company, channel, etc.) brand name should not begin with "Graphite", or use it as the most prominent word in the name. It may only be used as a secondary or descriptive word that comes after your own unique name. For example: "XYZ for Graphite" is acceptable, but "Graphite XYZ" is not since it implies affiliation.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue