├── .cargo
└── config.toml
├── .eslintrc.json
├── .gitignore
├── .husky
└── pre-commit
├── .nvmrc
├── .prettierignore
├── .taurignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── apps
├── browser
│ └── README.md
└── desktop
│ ├── app
│ ├── App.css
│ ├── App.tsx
│ └── routes.ts
│ ├── assets
│ ├── fonts
│ │ └── Inter-VariableFont.ttf
│ └── toolbar
│ │ ├── grid.svg
│ │ ├── hand.svg
│ │ ├── info.svg
│ │ ├── pen.svg
│ │ ├── select.svg
│ │ └── shape.svg
│ ├── components.json
│ ├── components
│ ├── EditorView.tsx
│ ├── GlyphGrid.tsx
│ ├── InteractiveScene.tsx
│ ├── Layout.tsx
│ ├── Metrics.tsx
│ ├── NavigationPane.tsx
│ ├── SidePane.tsx
│ ├── StaticScene.tsx
│ ├── Titlebar.tsx
│ ├── Toolbar.tsx
│ ├── ToolsPane.tsx
│ └── ui
│ │ └── tooltip.tsx
│ ├── context
│ └── CanvasContext.tsx
│ ├── data
│ └── charsets
│ │ ├── adobe-latin-1.ts
│ │ └── index.ts
│ ├── index.css
│ ├── index.html
│ ├── lib
│ ├── core
│ │ ├── Contour.test.ts
│ │ ├── Contour.ts
│ │ ├── EditActions.test.ts
│ │ ├── EditActions.ts
│ │ ├── EditEngine.test.ts
│ │ ├── EditEngine.ts
│ │ ├── EntityId.ts
│ │ ├── EventEmitter.test.ts
│ │ ├── EventEmitter.ts
│ │ ├── PatternMatcher.test.ts
│ │ ├── PatternMatcher.ts
│ │ ├── PatternParser.test.ts
│ │ ├── PatternParser.ts
│ │ ├── RuleTable.ts
│ │ ├── UndoManager.ts
│ │ └── common.ts
│ ├── editor
│ │ ├── ContourManager.ts
│ │ ├── Editor.ts
│ │ ├── FrameHandler.ts
│ │ ├── Painter.ts
│ │ ├── Scene.ts
│ │ └── Viewport.ts
│ ├── graphics
│ │ ├── Path.ts
│ │ └── backends
│ │ │ ├── CanvasKitRenderer.ts
│ │ │ └── errors.ts
│ ├── math
│ │ ├── circle.ts
│ │ ├── line.ts
│ │ ├── point.ts
│ │ ├── rect.test.ts
│ │ ├── rect.ts
│ │ ├── shape.ts
│ │ └── vector.ts
│ ├── styles
│ │ └── style.ts
│ ├── tools
│ │ ├── Hand.ts
│ │ ├── Pen.ts
│ │ ├── Select.ts
│ │ ├── Shape.ts
│ │ └── tools.ts
│ ├── utils.ts
│ └── utils
│ │ └── utils.ts
│ ├── main.tsx
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ ├── canvaskit.wasm
│ └── cursors
│ │ ├── pen@1.svg
│ │ └── pen@2.svg
│ ├── store
│ └── store.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── types
│ ├── common.ts
│ ├── edit.ts
│ ├── events.ts
│ ├── graphics.ts
│ ├── handle.ts
│ ├── math.ts
│ ├── nudge.ts
│ ├── segments.ts
│ ├── svg.d.ts
│ └── tool.ts
│ ├── views
│ ├── Editor.tsx
│ ├── FontInfo.tsx
│ └── Home.tsx
│ ├── vite-env.d.ts
│ └── vite.config.ts
├── crates
├── shift-editor
│ ├── Cargo.toml
│ └── src
│ │ ├── editor.rs
│ │ └── lib.rs
├── shift-events
│ ├── Cargo.toml
│ └── src
│ │ ├── events.rs
│ │ └── lib.rs
├── shift-font
│ ├── Cargo.toml
│ └── src
│ │ ├── contour.rs
│ │ ├── entity.rs
│ │ ├── font.rs
│ │ ├── font_service.rs
│ │ ├── glyph.rs
│ │ ├── lib.rs
│ │ ├── otf_ttf.rs
│ │ ├── path.rs
│ │ └── ufo.rs
├── shift-tauri
│ ├── .gitignore
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── build.rs
│ ├── capabilities
│ │ └── default.json
│ ├── icons
│ │ ├── 128x128.png
│ │ ├── 128x128@2x.png
│ │ ├── 32x32.png
│ │ ├── Square107x107Logo.png
│ │ ├── Square142x142Logo.png
│ │ ├── Square150x150Logo.png
│ │ ├── Square284x284Logo.png
│ │ ├── Square30x30Logo.png
│ │ ├── Square310x310Logo.png
│ │ ├── Square44x44Logo.png
│ │ ├── Square71x71Logo.png
│ │ ├── Square89x89Logo.png
│ │ ├── StoreLogo.png
│ │ ├── android
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ └── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_round.png
│ │ ├── icon.icns
│ │ ├── icon.ico
│ │ ├── icon.png
│ │ └── ios
│ │ │ ├── AppIcon-20x20@1x.png
│ │ │ ├── AppIcon-20x20@2x-1.png
│ │ │ ├── AppIcon-20x20@2x.png
│ │ │ ├── AppIcon-20x20@3x.png
│ │ │ ├── AppIcon-29x29@1x.png
│ │ │ ├── AppIcon-29x29@2x-1.png
│ │ │ ├── AppIcon-29x29@2x.png
│ │ │ ├── AppIcon-29x29@3x.png
│ │ │ ├── AppIcon-40x40@1x.png
│ │ │ ├── AppIcon-40x40@2x-1.png
│ │ │ ├── AppIcon-40x40@2x.png
│ │ │ ├── AppIcon-40x40@3x.png
│ │ │ ├── AppIcon-512@2x.png
│ │ │ ├── AppIcon-60x60@2x.png
│ │ │ ├── AppIcon-60x60@3x.png
│ │ │ ├── AppIcon-76x76@1x.png
│ │ │ ├── AppIcon-76x76@2x.png
│ │ │ └── AppIcon-83.5x83.5@2x.png
│ ├── src
│ │ ├── commands.rs
│ │ ├── core.rs
│ │ ├── lib.rs
│ │ ├── main.rs
│ │ ├── menu.rs
│ │ └── shortcuts.rs
│ └── tauri.conf.json
└── shift-unicode
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ ├── lib.rs
│ └── unicode.rs
├── gen
├── README.md
└── charsets
│ ├── .gitignore
│ ├── .python-version
│ ├── README.md
│ ├── charsets.py
│ ├── main.py
│ ├── pyproject.toml
│ └── uv.lock
├── package.json
├── packages
└── shared
│ ├── README.md
│ ├── package.json
│ ├── scripts
│ └── exports-ts-rs.js
│ ├── src
│ ├── index.ts
│ └── types
│ │ ├── EntityId.ts
│ │ ├── Font.ts
│ │ ├── FontCompiledEvent.ts
│ │ ├── FontLoadedEvent.ts
│ │ ├── FontMetadata.ts
│ │ ├── Glyph.ts
│ │ ├── IContour.ts
│ │ ├── IContourPoint.ts
│ │ ├── Metrics.ts
│ │ ├── MovedPoint.ts
│ │ ├── PathCommand.ts
│ │ ├── PointType.ts
│ │ ├── PointsAddedEvent.ts
│ │ └── PointsMovedEvent.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
└── rebuild_types.sh
└── turbo.json
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [env]
2 | TS_RS_EXPORT_DIR = { value = "./packages/shared/src/types", relative = true }
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": [
4 | "@typescript-eslint",
5 | "import",
6 | "react",
7 | "react-hooks"
8 | ],
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:import/recommended",
13 | "plugin:import/typescript",
14 | "plugin:react/recommended",
15 | "plugin:react-hooks/recommended"
16 | ],
17 | "settings": {
18 | "react": {
19 | "version": "detect"
20 | },
21 | "import/resolver": {
22 | "typescript": {
23 | "alwaysTryTypes": true,
24 | "project": "./apps/desktop/tsconfig.json"
25 | },
26 | "alias": {
27 | "map": [
28 | ["@", "./apps/desktop"],
29 | ["@/components", "./apps/desktop/components"],
30 | ["@/lib", "./apps/desktop/lib"],
31 | ["@/store", "./apps/desktop/store"],
32 | ["@/types", "./apps/desktop/types"],
33 | ["@/context", "./apps/desktop/context"],
34 | ["@/assets", "./apps/desktop/assets"],
35 | ["@/hooks", "./apps/desktop/hooks"]
36 | ],
37 | "extensions": [".ts", ".tsx", ".js", ".jsx", ".json"]
38 | }
39 | }
40 | },
41 | "rules": {
42 | // Import sorting
43 | "import/order": [
44 | "error",
45 | {
46 | "groups": [
47 | "builtin",
48 | "external",
49 | "internal",
50 | ["parent", "sibling"],
51 | "index",
52 | "object",
53 | "type"
54 | ],
55 | "pathGroups": [
56 | {
57 | "pattern": "react",
58 | "group": "builtin",
59 | "position": "before"
60 | },
61 | {
62 | "pattern": "@tauri-uis/**",
63 | "group": "external",
64 | "position": "after"
65 | }
66 | ],
67 | "pathGroupsExcludedImportTypes": ["react"],
68 | "newlines-between": "always",
69 | "alphabetize": {
70 | "order": "asc",
71 | "caseInsensitive": true
72 | }
73 | }
74 | ],
75 | // TypeScript specific rules
76 | "@typescript-eslint/no-explicit-any": "warn",
77 | "@typescript-eslint/explicit-function-return-type": "off",
78 | "@typescript-eslint/explicit-module-boundary-types": "off",
79 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
80 | "@typescript-eslint/naming-convention": [
81 | "error",
82 | {
83 | "selector": "interface",
84 | "format": ["PascalCase"]
85 | },
86 | {
87 | "selector": "typeAlias",
88 | "format": ["PascalCase"]
89 | },
90 | {
91 | "selector": "enum",
92 | "format": ["PascalCase"]
93 | }
94 | ],
95 | // React specific rules
96 | "react/react-in-jsx-scope": "off",
97 | "react/prop-types": "warn",
98 | "react-hooks/rules-of-hooks": "error",
99 | "react-hooks/exhaustive-deps": "off"
100 | }
101 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | !.vscode/extensions.json
17 | .idea
18 | .DS_Store
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 | .cursorrules
25 | .vscode
26 |
27 | target*
28 | .turbo
29 | .bin
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm format
2 | pnpm lint
3 | pnpm test
4 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/jod
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | build
4 | src-tauri
--------------------------------------------------------------------------------
/.taurignore:
--------------------------------------------------------------------------------
1 | !crates/*
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = ["crates/*"]
4 |
5 |
6 | [workspace.dependencies]
7 | shift-events = { path = "crates/shift-events" }
8 | shift-font = { path = "crates/shift-font" }
9 | shift-editor = { path = "crates/shift-editor" }
10 | shift-unicode = { path = "crates/shift-unicode" }
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Shift
6 |
A modern, cross-platform font editor built with TypeScript and Rust, focused on bringing contemporary technologies and design principles to type design.
7 |
8 | 
9 |
10 |
11 |
12 |
13 | ## Why Shift?
14 |
15 | Shift aims to redefine font editing by combining the power of Rust for performance-critical tasks with the flexibility of web-based UI technologies. Whether you're a type designer or a developer, Shift offers a fresh approach to creating and editing fonts with a focus on speed, precision, and extensibility.
16 |
17 | > [!IMPORTANT]
18 | > Shift is in a pre-alpha state and is currently only suitable for developers interested in contributing to the project
19 |
20 | ## Architecture
21 |
22 | Shift uses the Tauri framework:
23 |
24 | - **UI**: Uses React and Vite for components around the canvas
25 | - **Rendering**: Webview based UI utilising the HTML canvas and rendered with CanvasKit (Skia) for high-quality graphics
26 | - **State management**: Zustland for global React state mangement
27 | - **Backend**: Rust for high-intensive operations and font related processing
28 |
29 | ## Getting Started
30 |
31 | ### Prerequisites
32 |
33 | - **Rust** (1.70 or later): [Install Rust](https://www.rust-lang.org/tools/install)
34 | - **pnpm** (1.0 or later): [Install pnpm](https://pnpm.io/installation)
35 | - **typescript** (5.8 or later, also known as tsc): [Install typescript](https://www.typescriptlang.org/download)
36 | - **node.js** (23.10 or later): [Install node.js](https://nodejs.org/en/download)
37 | - **System Dependencies**:
38 | - **Windows**: Microsoft Visual C++ Build Tools, WebView2
39 | - **macOS**: Xcode Command Line Tools (`xcode-select --install`)
40 | - **Linux**: WebKit2GTK (`libwebkit2gtk-4.0-dev`) and build essentials
41 |
42 | ### Quick Start
43 |
44 | 1. **Clone the repository**:
45 |
46 | ```bash
47 | git clone https://github.com/shift-editor/shift.git
48 | cd shift
49 | ```
50 |
51 | 2. **Install dependencies**:
52 |
53 | ```bash
54 | pnpm install
55 | ```
56 |
57 | 3. **Run the development server**:
58 |
59 | ```bash
60 | pnpm dev:app
61 | ```
62 |
63 | ### Build for Production
64 |
65 | ```bash
66 | pnpm build:app
67 |
68 | ```
69 |
70 | ### Common Issues
71 |
72 | - If you encounter build errors, ensure you have all system dependencies installed
73 | - For Linux users, make sure WebKit2GTK development libraries are installed
74 | - For detailed troubleshooting, check the [Tauri docs](https://v1.tauri.app/v1/guides/getting-started/prerequisites/)
75 |
76 | ## Development Roadmap
77 |
78 | We aim to implement the typical features present in font editors such as FontForge, Glyphs, RobotFont etc.
79 |
80 | ## License
81 |
82 | [GNU General Public License (GPL) v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html)
83 |
84 | Copyright © 2025 Kostya Farber. All rights reserved.
85 |
--------------------------------------------------------------------------------
/apps/browser/README.md:
--------------------------------------------------------------------------------
1 | future potential to ship this in the browser
2 |
--------------------------------------------------------------------------------
/apps/desktop/app/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: var(--color-secondary);
3 | overflow: hidden;
4 | }
5 |
6 | @font-face {
7 | font-family: 'Inter';
8 | src: url('/assets/fonts/Inter-VariableFont.ttf') format('truetype');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 |
13 | .cursor-pen {
14 | cursor:
15 | url('/cursors/pen@1.svg') 15 8,
16 | pointer;
17 | cursor:
18 | -webkit-image-set(url('/cursors/pen@2.svg') 2x) 15 8,
19 | pointer;
20 | }
21 |
22 | .cursor-hand {
23 | cursor: move;
24 | cursor: grab;
25 | cursor: -moz-grab;
26 | cursor: -webkit-grab;
27 | }
28 |
29 | .cursor-hand:active {
30 | cursor: grabbing;
31 | cursor: -moz-grabbing;
32 | cursor: -webkit-grabbing;
33 | }
34 |
--------------------------------------------------------------------------------
/apps/desktop/app/App.tsx:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import { HashRouter, Route, Routes } from 'react-router-dom';
3 |
4 | import { routes } from './routes';
5 |
6 | export const App = () => {
7 | return (
8 |
9 |
10 | {routes.map((route) => (
11 | } />
12 | ))}
13 |
14 |
15 | );
16 | };
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/apps/desktop/app/routes.ts:
--------------------------------------------------------------------------------
1 | import GridSvg from '@/assets/toolbar/grid.svg';
2 | import InfoSvg from '@/assets/toolbar/info.svg';
3 | import { Svg } from '@/types/common';
4 | import { Editor } from '@/views/Editor';
5 | import { FontInfo } from '@/views/FontInfo';
6 | import { Home } from '@/views/Home';
7 |
8 | interface Route {
9 | id: string;
10 | path: string;
11 | component: React.ComponentType;
12 | description: string;
13 | icon?: Svg;
14 | }
15 |
16 | export const routes: Route[] = [
17 | {
18 | id: 'home',
19 | path: '/',
20 | icon: GridSvg,
21 | component: Home,
22 | description: 'Display all glyphs',
23 | },
24 | {
25 | id: 'info',
26 | path: '/info',
27 | icon: InfoSvg,
28 | component: FontInfo,
29 | description: 'Display and edit font information, such as family name, weight, style, etc.',
30 | },
31 | {
32 | id: 'editor',
33 | path: '/editor/:glyphId',
34 | component: Editor,
35 | description: 'Display and edit a glyph',
36 | },
37 | ];
38 |
--------------------------------------------------------------------------------
/apps/desktop/assets/fonts/Inter-VariableFont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shift-editor/shift/ed519408f54119ea83678ee2390cea7e145148be/apps/desktop/assets/fonts/Inter-VariableFont.ttf
--------------------------------------------------------------------------------
/apps/desktop/assets/toolbar/grid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/apps/desktop/assets/toolbar/hand.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/desktop/assets/toolbar/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/desktop/assets/toolbar/pen.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/desktop/assets/toolbar/select.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/desktop/assets/toolbar/shape.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/apps/desktop/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/apps/desktop/components/EditorView.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from 'react';
2 |
3 | import { Glyph, Metrics } from '@shift/shared';
4 | import { invoke } from '@tauri-apps/api/core';
5 |
6 | import { CanvasContextProvider } from '@/context/CanvasContext';
7 | import AppState, { getEditor } from '@/store/store';
8 |
9 | import { InteractiveScene } from './InteractiveScene';
10 | import { Metrics as MetricsComponent } from './Metrics';
11 | import { StaticScene } from './StaticScene';
12 |
13 | interface EditorViewProps {
14 | glyphId: string;
15 | }
16 |
17 | export const EditorView: FC = ({ glyphId }) => {
18 | const activeTool = AppState((state) => state.activeTool);
19 | const editor = getEditor();
20 |
21 | useEffect(() => {
22 | const fetchFontData = async () => {
23 | const [metrics, glyph] = await Promise.all([
24 | invoke('get_font_metrics'),
25 | invoke('get_glyph', {
26 | unicode: parseInt(glyphId, 16),
27 | }),
28 | ]);
29 |
30 | const guides = {
31 | ascender: { y: metrics.ascender },
32 | capHeight: { y: metrics.capHeight },
33 | xHeight: { y: metrics.xHeight },
34 | descender: { y: metrics.descender },
35 | baseline: { y: 0 },
36 | xAdvance: glyph.x_advance,
37 | };
38 |
39 | editor.constructGuidesPath(guides);
40 | editor.setViewportUpm(metrics.unitsPerEm);
41 |
42 | editor.loadContours(glyph.contours);
43 | editor.redrawGlyph();
44 | };
45 |
46 | fetchFontData();
47 |
48 | editor.activeTool().setReady();
49 |
50 | return () => {
51 | editor.activeTool().setIdle();
52 | editor.clearContours();
53 | };
54 | }, [glyphId]);
55 |
56 | const onWheel = (e: React.WheelEvent) => {
57 | e.preventDefault();
58 |
59 | const pan = editor.getPan();
60 |
61 | const dx = e.deltaX;
62 | const dy = e.deltaY;
63 |
64 | editor.pan(pan.x - dx, pan.y - dy);
65 | editor.requestRedraw();
66 | };
67 |
68 | return (
69 | {
73 | const position = editor.getMousePosition(e.clientX, e.clientY);
74 | const { x, y } = editor.projectScreenToUpm(position.x, position.y);
75 | editor.setUpmMousePosition(x, y);
76 | }}
77 | >
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/apps/desktop/components/GlyphGrid.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 |
3 | import { ADOBE_LATIN_1 } from '@/data/charsets';
4 |
5 | export const GlyphGrid = () => {
6 | const navigate = useNavigate();
7 |
8 | return (
9 |
10 |
11 |
Adobe Latin 1
12 |
13 |
14 | {Object.values(ADOBE_LATIN_1).map((glyph) => {
15 | return (
16 |
17 | navigate(`/editor/${glyph.unicode}`)}
20 | >
21 | {String.fromCharCode(parseInt(glyph.unicode, 16))}
22 |
23 |
24 | );
25 | })}
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/apps/desktop/components/InteractiveScene.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { CanvasContext } from '@/context/CanvasContext';
4 | import AppState from '@/store/store';
5 |
6 | export const InteractiveScene = () => {
7 | const { interactiveCanvasRef } = useContext(CanvasContext);
8 | const editor = AppState.getState().editor;
9 | const activeTool = editor.activeTool();
10 |
11 | return (
12 | {
17 | activeTool.onMouseDown(e);
18 | }}
19 | onMouseUp={(e) => {
20 | activeTool.onMouseUp(e);
21 | }}
22 | onMouseMove={(e) => {
23 | activeTool.onMouseMove(e);
24 | }}
25 | onDoubleClick={(e) => {
26 | if (!activeTool.onDoubleClick) return;
27 |
28 | activeTool.onDoubleClick(e);
29 | }}
30 | />
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/apps/desktop/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | export const Layout = () => {};
2 |
--------------------------------------------------------------------------------
/apps/desktop/components/Metrics.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | import { getEditor } from '@/store/store';
4 |
5 | export const Metrics = () => {
6 | const xRef = useRef(null);
7 | const yRef = useRef(null);
8 | const editor = getEditor();
9 |
10 | useEffect(() => {
11 | const updateMouseMetrics = () => {
12 | if (!xRef.current || !yRef.current) return;
13 |
14 | const { x, y } = editor.getUpmMousePosition();
15 |
16 | xRef.current.textContent = Math.round(x).toString();
17 | yRef.current.textContent = Math.round(y).toString();
18 | };
19 |
20 | window.addEventListener('mousemove', updateMouseMetrics);
21 |
22 | return () => {
23 | window.removeEventListener('mousemove', updateMouseMetrics);
24 | };
25 | }, []);
26 |
27 | return (
28 | <>
29 |
30 |
x
31 |
32 |
y
33 |
34 |
35 | >
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/apps/desktop/components/NavigationPane.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | import { useNavigate } from 'react-router-dom';
4 |
5 | import { routes } from '@/app/routes';
6 | import { Svg } from '@/types/common';
7 |
8 | export interface NavigationItemProps {
9 | Icon: Svg;
10 | onClick: () => void;
11 | }
12 | export const NavigationItem: FC = ({ Icon, onClick }) => {
13 | return (
14 |
15 |
16 |
17 | );
18 | };
19 | export const NavigationPane = () => {
20 | const navigate = useNavigate();
21 |
22 | return (
23 |
24 |
25 | {routes.map((route) => {
26 | if (route.icon) {
27 | return (
28 | navigate(route.path)}
32 | />
33 | );
34 | }
35 | return null;
36 | })}
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/apps/desktop/components/SidePane.tsx:
--------------------------------------------------------------------------------
1 | export const SidePane = () => {
2 | return (
3 |
4 |
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/apps/desktop/components/StaticScene.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { CanvasContext } from '@/context/CanvasContext';
4 |
5 | export const StaticScene = () => {
6 | const { staticCanvasRef } = useContext(CanvasContext);
7 |
8 | return (
9 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/apps/desktop/components/Titlebar.tsx:
--------------------------------------------------------------------------------
1 | export const Titlebar = () => {};
2 |
--------------------------------------------------------------------------------
/apps/desktop/components/Toolbar.tsx:
--------------------------------------------------------------------------------
1 | import { NavigationPane } from './NavigationPane';
2 | import { ToolsPane } from './ToolsPane';
3 |
4 | export const Toolbar = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/apps/desktop/components/ToolsPane.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from 'react';
2 |
3 | import { FontLoadedEvent } from '@shift/shared';
4 | import { listen } from '@tauri-apps/api/event';
5 |
6 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
7 | import { tools } from '@/lib/tools/tools';
8 | import AppState, { getEditor } from '@/store/store';
9 | import { Svg } from '@/types/common';
10 | import { ToolName } from '@/types/tool';
11 |
12 | interface ToolbarIconProps {
13 | Icon: Svg;
14 | name: ToolName;
15 | tooltip: string;
16 | activeTool: ToolName;
17 | onClick: () => void;
18 | }
19 | export const ToolbarIcon: FC = ({ Icon, name, tooltip, activeTool, onClick }) => {
20 | return (
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 | {tooltip}
34 |
35 |
36 | );
37 | };
38 |
39 | export const ToolsPane: FC = () => {
40 | const activeTool = AppState((state) => state.activeTool);
41 | const setActiveTool = AppState((state) => state.setActiveTool);
42 | const editor = getEditor();
43 |
44 | const fileName = AppState((state) => state.fileName);
45 |
46 | useEffect(() => {
47 | const unsubscribe = listen('font:loaded', (event) => {
48 | AppState.setState({ fileName: event.payload.fileName });
49 | });
50 |
51 | return () => {
52 | unsubscribe.then((fn) => fn());
53 | };
54 | }, []);
55 |
56 | return (
57 |
58 | {fileName}
59 |
60 |
61 | {Array.from(tools.entries()).map(([name, { icon, tooltip }]) => (
62 | {
69 | setActiveTool(name);
70 | editor.activeTool().setReady();
71 | }}
72 | />
73 | ))}
74 |
75 |
76 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/apps/desktop/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | function TooltipProvider({
8 | delayDuration = 0,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
17 | );
18 | }
19 |
20 | function Tooltip({ ...props }: React.ComponentProps) {
21 | return (
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | function TooltipTrigger({ ...props }: React.ComponentProps) {
29 | return ;
30 | }
31 |
32 | function TooltipContent({
33 | className,
34 | sideOffset = 0,
35 | children,
36 | ...props
37 | }: React.ComponentProps) {
38 | return (
39 |
40 |
49 | {children}
50 |
51 |
52 | );
53 | }
54 |
55 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
56 |
--------------------------------------------------------------------------------
/apps/desktop/context/CanvasContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useEffect, useRef, useState } from 'react';
2 |
3 | import { CanvasKit } from 'canvaskit-wasm';
4 |
5 | import { CanvasKitContext, initCanvasKit } from '@/lib/graphics/backends/CanvasKitRenderer';
6 | import AppState from '@/store/store';
7 | import { CanvasRef } from '@/types/graphics';
8 |
9 | interface CanvasContext {
10 | interactiveCanvasRef: CanvasRef;
11 | staticCanvasRef: CanvasRef;
12 | }
13 |
14 | export const CanvasContext = createContext({
15 | interactiveCanvasRef: { current: null },
16 | staticCanvasRef: { current: null },
17 | });
18 |
19 | export const CanvasContextProvider = ({ children }: { children: React.ReactNode }) => {
20 | const interactiveCanvasRef = useRef(null);
21 | const staticCanvasRef = useRef(null);
22 |
23 | useEffect(() => {
24 | const initCanvas = (canvasKit: CanvasKit, canvas: HTMLCanvasElement) => {
25 | const ctx = new CanvasKitContext(canvasKit);
26 |
27 | ctx.resizeCanvas(canvas);
28 |
29 | return ctx;
30 | };
31 |
32 | const editor = AppState.getState().editor;
33 |
34 | const setUpContexts = async ({
35 | interactiveCanvas,
36 | staticCanvas,
37 | }: {
38 | interactiveCanvas: HTMLCanvasElement;
39 | staticCanvas: HTMLCanvasElement;
40 | }) => {
41 | const canvasKit = await initCanvasKit();
42 | const interactiveContext = initCanvas(canvasKit, interactiveCanvas);
43 | const staticContext = initCanvas(canvasKit, staticCanvas);
44 |
45 | editor.setInteractiveContext(interactiveContext);
46 | editor.setStaticContext(staticContext);
47 |
48 | const resizeCanvas = (entries: ResizeObserverEntry[]) => {
49 | const [interactiveCanvas, staticCanvas] = entries;
50 |
51 | interactiveContext.resizeCanvas(interactiveCanvas.target as HTMLCanvasElement);
52 | staticContext.resizeCanvas(staticCanvas.target as HTMLCanvasElement);
53 |
54 | editor.requestImmediateRedraw();
55 | };
56 |
57 | const observer = new ResizeObserver(resizeCanvas);
58 |
59 | observer.observe(interactiveCanvas);
60 | observer.observe(staticCanvas);
61 |
62 | return () => {
63 | observer.disconnect();
64 | };
65 | };
66 |
67 | if (!interactiveCanvasRef.current || !staticCanvasRef.current) return;
68 |
69 | setUpContexts({
70 | interactiveCanvas: interactiveCanvasRef.current,
71 | staticCanvas: staticCanvasRef.current,
72 | });
73 | }, []);
74 |
75 | return (
76 |
82 | {children}
83 |
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/apps/desktop/data/charsets/index.ts:
--------------------------------------------------------------------------------
1 | import { ADOBE_LATIN_1 } from './adobe-latin-1';
2 |
3 | export { ADOBE_LATIN_1 };
4 |
5 | export const CHARSETS = {
6 | ADOBE_LATIN_1,
7 | };
8 |
--------------------------------------------------------------------------------
/apps/desktop/index.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @import 'tw-animate-css';
3 |
4 | @theme {
5 | --font-ui: 'Inter';
6 | --text-ui: 0.85rem;
7 | --color-secondary: #3a3a3a;
8 | }
9 |
--------------------------------------------------------------------------------
/apps/desktop/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Shift
8 |
9 |
10 |
11 |
12 |
13 |
14 |