Restructure project directories (#333)

`/client/web` -> `/frontend`
`/client/cli` -> *delete for now*
`/client/native` -> *delete for now*
`/core/editor` -> `/editor`
`/core/document` -> `/graphene`
`/core/renderer` -> `/charcoal`
`/core/proc-macro` -> `/proc-macros` *(now plural)*
This commit is contained in:
Keavon Chambers
2021-08-07 05:17:18 -07:00
parent 434695d578
commit 53ad105f57
239 changed files with 197 additions and 224 deletions
+79
View File
@@ -0,0 +1,79 @@
export interface RGB {
r: number;
g: number;
b: number;
a: number;
}
export interface HSV {
h: number;
s: number;
v: number;
a: number;
}
export function hsvToRgb(hsv: HSV): RGB {
let { h } = hsv;
const { s, v } = hsv;
h *= 6;
const i = Math.floor(h);
const f = h - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const mod = i % 6;
const r = Math.round([v, q, p, p, t, v][mod]);
const g = Math.round([t, v, v, q, p, p][mod]);
const b = Math.round([p, p, t, v, v, q][mod]);
return { r, g, b, a: hsv.a };
}
export function rgbToHsv(rgb: RGB) {
const { r, g, b } = rgb;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const s = max === 0 ? 0 : d / max;
const v = max;
let h = 0;
if (max === min) {
h = 0;
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
default:
}
h /= 6;
}
return { h, s, v, a: rgb.a };
}
export function rgbToDecimalRgb(rgb: RGB) {
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
return { r, g, b, a: rgb.a };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isRGB(data: any): data is RGB {
if (typeof data !== "object" || data === null) return false;
return (
typeof data.r === "number" &&
!Number.isNaN(data.r) &&
typeof data.g === "number" &&
!Number.isNaN(data.g) &&
typeof data.b === "number" &&
!Number.isNaN(data.b) &&
typeof data.a === "number" &&
!Number.isNaN(data.a)
);
}
+22
View File
@@ -0,0 +1,22 @@
import { createDialog, dismissDialog } from "@/utilities/dialog";
import { TextButtonWidget } from "@/components/widgets/widgets";
export default function comingSoon(issueNumber?: number) {
const bugMessage = `— but you can help add it!\nSee issue #${issueNumber} on GitHub.`;
const details = `This feature is not implemented yet${issueNumber ? bugMessage : ""}`;
const okButton: TextButtonWidget = {
kind: "TextButton",
callback: async () => dismissDialog(),
props: { label: "OK", emphasized: true, minWidth: 96 },
};
const issueButton: TextButtonWidget = {
kind: "TextButton",
callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"),
props: { label: `Issue #${issueNumber}`, minWidth: 96 },
};
const buttons = [okButton];
if (issueNumber) buttons.push(issueButton);
createDialog("Warning", "Coming soon", details, buttons);
}
+37
View File
@@ -0,0 +1,37 @@
import { reactive, readonly } from "vue";
import { TextButtonWidget } from "@/components/widgets/widgets";
const state = reactive({
visible: false,
icon: "",
heading: "",
details: "",
buttons: [] as TextButtonWidget[],
});
export function createDialog(icon: string, heading: string, details: string, buttons: TextButtonWidget[]) {
state.visible = true;
state.icon = icon;
state.heading = heading;
state.details = details;
state.buttons = buttons;
}
export function dismissDialog() {
state.visible = false;
}
export function submitDialog() {
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
if (firstEmphasizedButton) {
// If statement satisfies TypeScript
if (firstEmphasizedButton.callback) firstEmphasizedButton.callback();
}
}
export function dialogIsVisible(): boolean {
return state.visible;
}
export default readonly(state);
+97
View File
@@ -0,0 +1,97 @@
import { reactive, readonly } from "vue";
import { createDialog, dismissDialog } from "@/utilities/dialog";
import { ResponseType, registerResponseHandler, Response, SetActiveDocument, UpdateOpenDocumentsList, DisplayConfirmationToCloseDocument } from "@/utilities/response-handler";
const wasm = import("@/../wasm/pkg");
const state = reactive({
title: "",
unsaved: false,
documents: [] as Array<string>,
activeDocumentIndex: 0,
});
export async function selectDocument(tabIndex: number) {
const { select_document } = await wasm;
select_document(tabIndex);
}
export async function closeDocumentWithConfirmation(tabIndex: number) {
selectDocument(tabIndex);
const tabLabel = state.documents[tabIndex];
// TODO: Rename to "Save changes before closing?" when we can actually save documents somewhere, not just export SVGs
createDialog("File", "Close without exporting SVG?", tabLabel, [
{
kind: "TextButton",
callback: async () => {
(await wasm).export_document();
dismissDialog();
},
props: { label: "Export", emphasized: true, minWidth: 96 },
},
{
kind: "TextButton",
callback: async () => {
(await wasm).close_document(tabIndex);
dismissDialog();
},
props: { label: "Discard", minWidth: 96 },
},
{
kind: "TextButton",
callback: async () => {
dismissDialog();
},
props: { label: "Cancel", minWidth: 96 },
},
]);
}
export async function closeAllDocumentsWithConfirmation() {
createDialog("Copy", "Close all documents?", "Unsaved work will be lost!", [
{
kind: "TextButton",
callback: async () => {
(await wasm).close_all_documents();
dismissDialog();
},
props: { label: "Discard All", minWidth: 96 },
},
{
kind: "TextButton",
callback: async () => {
dismissDialog();
},
props: { label: "Cancel", minWidth: 96 },
},
]);
}
export default readonly(state);
registerResponseHandler(ResponseType.UpdateOpenDocumentsList, (responseData: Response) => {
const documentListData = responseData as UpdateOpenDocumentsList;
if (documentListData) {
state.documents = documentListData.open_documents;
state.title = state.documents[state.activeDocumentIndex];
}
});
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
const documentData = responseData as SetActiveDocument;
if (documentData) {
state.activeDocumentIndex = documentData.document_index;
state.title = state.documents[state.activeDocumentIndex];
}
});
registerResponseHandler(ResponseType.DisplayConfirmationToCloseDocument, (responseData: Response) => {
const data = responseData as DisplayConfirmationToCloseDocument;
closeDocumentWithConfirmation(data.document_index);
});
registerResponseHandler(ResponseType.DisplayConfirmationToCloseAllDocuments, (_responseData: Response) => {
closeAllDocumentsWithConfirmation();
});
(async () => (await wasm).get_open_documents_list())();
+37
View File
@@ -0,0 +1,37 @@
import { reactive, readonly } from "vue";
const state = reactive({
windowFullscreen: false,
keyboardLocked: false,
});
export function fullscreenModeChanged() {
state.windowFullscreen = Boolean(document.fullscreenElement);
if (!state.windowFullscreen) state.keyboardLocked = false;
}
export function keyboardLockApiSupported(): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return "keyboard" in navigator && "lock" in (navigator as any).keyboard;
}
export async function enterFullscreen() {
await document.documentElement.requestFullscreen();
if (keyboardLockApiSupported()) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (navigator as any).keyboard.lock(["ControlLeft", "ControlRight"]);
state.keyboardLocked = true;
}
}
export async function exitFullscreen() {
await document.exitFullscreen();
}
export async function toggleFullscreen() {
if (state.windowFullscreen) await exitFullscreen();
else await enterFullscreen();
}
export default readonly(state);
+76
View File
@@ -0,0 +1,76 @@
import { toggleFullscreen } from "@/utilities/fullscreen";
import { dialogIsVisible, dismissDialog, submitDialog } from "@/utilities/dialog";
const wasm = import("@/../wasm/pkg");
export function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean {
// Don't redirect user input from text entry into HTML elements
const target = e.target as HTMLElement;
if (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable) return false;
// Don't redirect when a modal is covering the workspace
if (dialogIsVisible()) return false;
// Don't redirect a fullscreen request
if (e.key.toLowerCase() === "f11" && e.type === "keydown" && !e.repeat) {
e.preventDefault();
toggleFullscreen();
return false;
}
// Don't redirect a reload request
if (e.key.toLowerCase() === "f5") return false;
// Don't redirect debugging tools
if (e.key.toLowerCase() === "f12") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "c") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "i") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "j") return false;
// Redirect to the backend
return true;
}
export async function handleKeyDown(e: KeyboardEvent) {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const { on_key_down } = await wasm;
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
on_key_down(e.key, modifiers);
return;
}
if (dialogIsVisible()) {
if (e.key === "Escape") dismissDialog();
if (e.key === "Enter") submitDialog();
// Prevent the Enter key from acting like a click on the last clicked button, which might reopen the dialog
e.preventDefault();
}
}
export async function handleKeyUp(e: KeyboardEvent) {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const { on_key_up } = await wasm;
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
on_key_up(e.key, modifiers);
}
}
export async function handleMouseDown(e: MouseEvent) {
const target = e.target && (e.target as HTMLElement);
const clickedInsideDialog = target && target.closest(".dialog-modal .floating-menu-content");
if (dialogIsVisible() && !clickedInsideDialog) {
dismissDialog();
e.preventDefault();
e.stopPropagation();
}
}
export function makeModifiersBitfield(control: boolean, shift: boolean, alt: boolean): number {
// eslint-disable-next-line no-bitwise
return Number(control) | (Number(shift) << 1) | (Number(alt) << 2);
}
+3
View File
@@ -0,0 +1,3 @@
export function clamp(value: number, min = 0, max = 1) {
return Math.max(min, Math.min(value, max));
}
@@ -0,0 +1,4 @@
// This file is instantiated by wasm-bindgen in `/frontend/wasm/src/lib.rs` and re-exports the `handleResponse` function to
// provide access to the global copy of `response-handler.ts` with its shared state, not an isolated duplicate with empty state
export { handleResponse } from "@/utilities/response-handler";
+333
View File
@@ -0,0 +1,333 @@
import { reactive } from "vue";
/* eslint-disable @typescript-eslint/no-explicit-any */
type ResponseCallback = (responseData: Response) => void;
type ResponseMap = {
[response: string]: ResponseCallback | undefined;
};
const state = reactive({
responseMap: {} as ResponseMap,
});
export enum ResponseType {
UpdateCanvas = "UpdateCanvas",
ExportDocument = "ExportDocument",
ExpandFolder = "ExpandFolder",
CollapseFolder = "CollapseFolder",
SetActiveTool = "SetActiveTool",
SetActiveDocument = "SetActiveDocument",
UpdateOpenDocumentsList = "UpdateOpenDocumentsList",
UpdateWorkingColors = "UpdateWorkingColors",
UpdateLayer = "UpdateLayer",
SetCanvasZoom = "SetCanvasZoom",
SetCanvasRotation = "SetCanvasRotation",
DisplayConfirmationToCloseDocument = "DisplayConfirmationToCloseDocument",
DisplayConfirmationToCloseAllDocuments = "DisplayConfirmationToCloseAllDocuments",
}
export function registerResponseHandler(responseType: ResponseType, callback: ResponseCallback) {
state.responseMap[responseType] = callback;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function handleResponse(responseType: string, responseData: any) {
const callback = state.responseMap[responseType];
const data = parseResponse(responseType, responseData);
if (callback && data) {
callback(data);
} else if (data) {
console.error(`Received a Response of type "${responseType}" but no handler was registered for it from the client.`);
} else {
console.error(`Received a Response of type "${responseType}" but but was not able to parse the data.`);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parseResponse(responseType: string, data: any): Response {
switch (responseType) {
case "DocumentChanged":
return newDocumentChanged(data.DocumentChanged);
case "CollapseFolder":
return newCollapseFolder(data.CollapseFolder);
case "ExpandFolder":
return newExpandFolder(data.ExpandFolder);
case "SetActiveTool":
return newSetActiveTool(data.SetActiveTool);
case "SetActiveDocument":
return newSetActiveDocument(data.SetActiveDocument);
case "UpdateOpenDocumentsList":
return newUpdateOpenDocumentsList(data.UpdateOpenDocumentsList);
case "UpdateCanvas":
return newUpdateCanvas(data.UpdateCanvas);
case "UpdateLayer":
return newUpdateLayer(data.UpdateLayer);
case "SetCanvasZoom":
return newSetCanvasZoom(data.SetCanvasZoom);
case "SetCanvasRotation":
return newSetCanvasRotation(data.SetCanvasRotation);
case "ExportDocument":
return newExportDocument(data.ExportDocument);
case "UpdateWorkingColors":
return newUpdateWorkingColors(data.UpdateWorkingColors);
case "DisplayConfirmationToCloseDocument":
return newDisplayConfirmationToCloseDocument(data.DisplayConfirmationToCloseDocument);
case "DisplayConfirmationToCloseAllDocuments":
return newDisplayConfirmationToCloseAllDocuments(data.DisplayConfirmationToCloseAllDocuments);
default:
throw new Error(`Unrecognized origin/responseType pair: ${origin}, '${responseType}'`);
}
}
export type Response = SetActiveTool | UpdateCanvas | DocumentChanged | CollapseFolder | ExpandFolder | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation;
export interface UpdateOpenDocumentsList {
open_documents: Array<string>;
}
function newUpdateOpenDocumentsList(input: any): UpdateOpenDocumentsList {
return { open_documents: input.open_documents };
}
export interface Color {
red: number;
green: number;
blue: number;
alpha: number;
}
function newColor(input: any): Color {
// TODO: Possibly change this in the Rust side to avoid any pitfalls
return { red: input.red * 255, green: input.green * 255, blue: input.blue * 255, alpha: input.alpha };
}
export interface UpdateWorkingColors {
primary: Color;
secondary: Color;
}
function newUpdateWorkingColors(input: any): UpdateWorkingColors {
return {
primary: newColor(input.primary),
secondary: newColor(input.secondary),
};
}
export interface SetActiveTool {
tool_name: string;
}
function newSetActiveTool(input: any): SetActiveTool {
return {
tool_name: input.tool_name,
};
}
export interface SetActiveDocument {
document_index: number;
}
function newSetActiveDocument(input: any): SetActiveDocument {
return {
document_index: input.document_index,
};
}
export interface DisplayConfirmationToCloseDocument {
document_index: number;
}
function newDisplayConfirmationToCloseDocument(input: any): DisplayConfirmationToCloseDocument {
return {
document_index: input.document_index,
};
}
function newDisplayConfirmationToCloseAllDocuments(_input: any): {} {
return {};
}
export interface UpdateCanvas {
document: string;
}
function newUpdateCanvas(input: any): UpdateCanvas {
return {
document: input.document,
};
}
export interface ExportDocument {
document: string;
}
function newExportDocument(input: any): UpdateCanvas {
return {
document: input.document,
};
}
export type DocumentChanged = {};
function newDocumentChanged(_: any): DocumentChanged {
return {};
}
export interface CollapseFolder {
path: BigUint64Array;
}
function newCollapseFolder(input: any): CollapseFolder {
return {
path: newPath(input.path),
};
}
export interface UpdateLayer {
path: BigUint64Array;
data: LayerPanelEntry;
}
function newUpdateLayer(input: any): UpdateLayer {
return {
path: newPath(input.data.path),
data: newLayerPanelEntry(input.data),
};
}
export interface ExpandFolder {
path: BigUint64Array;
children: Array<LayerPanelEntry>;
}
function newExpandFolder(input: any): ExpandFolder {
return {
path: newPath(input.path),
children: input.children.map((child: any) => newLayerPanelEntry(child)),
};
}
export interface SetCanvasZoom {
new_zoom: number;
}
function newSetCanvasZoom(input: any): SetCanvasZoom {
return {
new_zoom: input.new_zoom,
};
}
export interface SetCanvasRotation {
new_radians: number;
}
function newSetCanvasRotation(input: any): SetCanvasRotation {
return {
new_radians: input.new_radians,
};
}
function newPath(input: any): BigUint64Array {
// eslint-disable-next-line
const u32CombinedPairs = input.map((n: Array<bigint>) => BigInt((BigInt(n[0]) << BigInt(32)) | BigInt(n[1])));
return new BigUint64Array(u32CombinedPairs);
}
export enum BlendMode {
Normal = "normal",
Multiply = "multiply",
Darken = "darken",
ColorBurn = "color-burn",
Screen = "screen",
Lighten = "lighten",
ColorDodge = "color-dodge",
Overlay = "overlay",
SoftLight = "soft-light",
HardLight = "hard-light",
Difference = "difference",
Exclusion = "exclusion",
Hue = "hue",
Saturation = "saturation",
Color = "color",
Luminosity = "luminosity",
}
function newBlendMode(input: string): BlendMode {
const blendMode = {
Normal: BlendMode.Normal,
Multiply: BlendMode.Multiply,
Darken: BlendMode.Darken,
ColorBurn: BlendMode.ColorBurn,
Screen: BlendMode.Screen,
Lighten: BlendMode.Lighten,
ColorDodge: BlendMode.ColorDodge,
Overlay: BlendMode.Overlay,
SoftLight: BlendMode.SoftLight,
HardLight: BlendMode.HardLight,
Difference: BlendMode.Difference,
Exclusion: BlendMode.Exclusion,
Hue: BlendMode.Hue,
Saturation: BlendMode.Saturation,
Color: BlendMode.Color,
Luminosity: BlendMode.Luminosity,
}[input];
if (!blendMode) throw new Error(`Invalid blend mode "${blendMode}"`);
return blendMode;
}
function newOpacity(input: number): number {
return input * 100;
}
export interface LayerPanelEntry {
name: string;
visible: boolean;
blend_mode: BlendMode;
opacity: number;
layer_type: LayerType;
path: BigUint64Array;
layer_data: LayerData;
thumbnail: string;
}
function newLayerPanelEntry(input: any): LayerPanelEntry {
return {
name: input.name,
visible: input.visible,
blend_mode: newBlendMode(input.blend_mode),
opacity: newOpacity(input.opacity),
layer_type: newLayerType(input.layer_type),
layer_data: newLayerData(input.layer_data),
path: newPath(input.path),
thumbnail: input.thumbnail,
};
}
export interface LayerData {
expanded: boolean;
selected: boolean;
}
function newLayerData(input: any): LayerData {
return {
expanded: input.expanded,
selected: input.selected,
};
}
export enum LayerType {
Folder = "Folder",
Shape = "Shape",
Circle = "Circle",
Rect = "Rect",
Line = "Line",
PolyLine = "PolyLine",
Ellipse = "Ellipse",
}
function newLayerType(input: any): LayerType {
switch (input) {
case "Folder":
return LayerType.Folder;
case "Shape":
return LayerType.Shape;
case "Circle":
return LayerType.Circle;
case "Rect":
return LayerType.Rect;
case "Line":
return LayerType.Line;
case "PolyLine":
return LayerType.PolyLine;
case "Ellipse":
return LayerType.Ellipse;
default:
throw Error(`Received invalid input as an enum variant for LayerType: ${input}`);
}
}