mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-07-27 19:57:11 +00:00
editor: add pybricks logo welcome
This commit is contained in:
committed by
David Lechner
parent
a0f05c00a4
commit
6a979cafa8
@@ -4,6 +4,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Added Pybricks logo when no files open.
|
||||
|
||||
### Changed
|
||||
- Firmware restore for hubs with USB is now done in-app ([pybricks-code#1104]).
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"two.js": "^0.8.10",
|
||||
"typed-redux-saga": "^1.5.0",
|
||||
"typescript": "~4.9.3",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Text,
|
||||
} from '@blueprintjs/core';
|
||||
import { ContextMenu2, ResizeSensor2 } from '@blueprintjs/popover2';
|
||||
import classNames from 'classnames';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eighties.json';
|
||||
import xcodeTheme from 'monaco-themes/themes/Xcode_default.json';
|
||||
@@ -29,6 +30,7 @@ import { compile } from '../mpy/actions';
|
||||
import { useSelector } from '../reducers';
|
||||
import { useSettingIsShowDocsEnabled } from '../settings/hooks';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import Welcome from './Welcome';
|
||||
import { editorActivateFile, editorCloseFile } from './actions';
|
||||
import { useI18n } from './i18n';
|
||||
import * as pybricksMicroPython from './pybricksMicroPython';
|
||||
@@ -465,12 +467,14 @@ const Editor: React.VFC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const isEmpty = useSelector((s) => s.editor.openFileUuids.length === 0);
|
||||
|
||||
return (
|
||||
<div className="pb-editor">
|
||||
<EditorTabs onChange={() => editor?.focus()} />
|
||||
<ResizeSensor2 onResize={() => editor?.layout()}>
|
||||
<ContextMenu2
|
||||
className="pb-editor-tabpanel"
|
||||
className={classNames('pb-editor-tabpanel', isEmpty && 'pb-empty')}
|
||||
role="tabpanel"
|
||||
// NB: we have to create a new context menu each time it is
|
||||
// shown in order to get some state, like canUndo and canRedo
|
||||
@@ -478,6 +482,7 @@ const Editor: React.VFC = () => {
|
||||
content={() => <EditorContextMenu editor={editor} />}
|
||||
popoverProps={popoverProps}
|
||||
>
|
||||
<Welcome isVisible={isEmpty} />
|
||||
<div className="pb-editor-monaco" ref={editorRef} />
|
||||
</ContextMenu2>
|
||||
</ResizeSensor2>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
// welcome screen that is shown when no editor is open.
|
||||
|
||||
import { Colors } from '@blueprintjs/core';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Two from 'two.js';
|
||||
import { useTernaryDarkMode } from 'usehooks-ts';
|
||||
import logoSvg from './logo.svg';
|
||||
|
||||
const defaultRotation = -Math.PI / 9; // radians
|
||||
const rotationSpeedIncrement = 0.1; // radians per second
|
||||
|
||||
type State = {
|
||||
rotation: number;
|
||||
rotationSpeed: number;
|
||||
};
|
||||
|
||||
enum ActionType {
|
||||
Stop,
|
||||
ChangeSpeed,
|
||||
Update,
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType.Stop;
|
||||
}
|
||||
| {
|
||||
type: ActionType.ChangeSpeed;
|
||||
amount: number;
|
||||
}
|
||||
| {
|
||||
type: ActionType.Update;
|
||||
timeDelta: number;
|
||||
};
|
||||
|
||||
function reduce(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case ActionType.Stop:
|
||||
return { ...state, rotation: defaultRotation, rotationSpeed: 0 };
|
||||
case ActionType.ChangeSpeed:
|
||||
return { ...state, rotationSpeed: state.rotationSpeed + action.amount };
|
||||
case ActionType.Update:
|
||||
return {
|
||||
...state,
|
||||
rotation: state.rotation + state.rotationSpeed * action.timeDelta,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function getFillColor(isDarkMode: boolean): string {
|
||||
return isDarkMode ? Colors.GRAY1 : Colors.GRAY5;
|
||||
}
|
||||
|
||||
type WelcomeProps = {
|
||||
isVisible: boolean;
|
||||
};
|
||||
|
||||
const Welcome: React.VoidFunctionComponent<WelcomeProps> = ({ isVisible }) => {
|
||||
const stateRef = useRef<State>({
|
||||
rotation: defaultRotation,
|
||||
rotationSpeed: 0,
|
||||
});
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const { isDarkMode } = useTernaryDarkMode();
|
||||
const fillColorRef = useRef('');
|
||||
// HACK: we don't want to image to flash when switching dark mode
|
||||
fillColorRef.current = getFillColor(isDarkMode);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// jsdom doesn't support ResizeObserver. don't really need to test animation anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
// istanbul ignore if: should not happen
|
||||
if (!elementRef.current) {
|
||||
console.error('elementRef was null!');
|
||||
return;
|
||||
}
|
||||
|
||||
const two = new Two({ fitted: true }).appendTo(elementRef.current);
|
||||
|
||||
const logo = two.load(logoSvg, (g) => {
|
||||
g.center();
|
||||
two.add(logo);
|
||||
two.play();
|
||||
});
|
||||
|
||||
two.addEventListener('update', (_count, time: number) => {
|
||||
stateRef.current = reduce(stateRef.current, {
|
||||
type: ActionType.Update,
|
||||
timeDelta: time / 1000,
|
||||
});
|
||||
|
||||
logo.fill = fillColorRef.current;
|
||||
logo.scale = Math.min(two.width, two.height) / 80;
|
||||
logo.rotation = stateRef.current.rotation;
|
||||
|
||||
two.scene.position.x = two.width / 2;
|
||||
two.scene.position.y = two.height / 2;
|
||||
});
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
two.fit();
|
||||
});
|
||||
|
||||
observer.observe(elementRef.current);
|
||||
|
||||
const handleClick = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
stateRef.current = reduce(stateRef.current, {
|
||||
type: ActionType.Stop,
|
||||
});
|
||||
};
|
||||
|
||||
two.renderer.domElement.addEventListener('pointerdown', handleClick, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
const handleWheel = (e: Event) => {
|
||||
const we = e as WheelEvent;
|
||||
|
||||
stateRef.current = reduce(stateRef.current, {
|
||||
type: ActionType.ChangeSpeed,
|
||||
amount: rotationSpeedIncrement * Math.sign(-we.deltaY),
|
||||
});
|
||||
};
|
||||
|
||||
two.renderer.domElement.addEventListener('wheel', handleWheel, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
two.renderer.domElement.removeEventListener('wheel', handleWheel);
|
||||
two.renderer.domElement.removeEventListener('pointerdown', handleClick);
|
||||
observer.disconnect();
|
||||
two.removeEventListener('update');
|
||||
elementRef.current?.removeChild(two.renderer.domElement);
|
||||
two.clear();
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pb-editor-welcome"
|
||||
ref={elementRef}
|
||||
onContextMenuCapture={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Welcome;
|
||||
@@ -63,9 +63,23 @@
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&-welcome {
|
||||
display: none;
|
||||
|
||||
.pb-editor-tabpanel.pb-empty > & {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-monaco {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.pb-editor-tabpanel.pb-empty > & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-placeholder {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="50" height="50" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0,-270.54)" fill="#ffffff"><path d="m1.9234 276.13c-1.0655 0-1.9234 0.85655-1.9234 1.9213v10.588c0 1.0648 0.85787 1.9213 1.9234 1.9213h3.3755c1.0655 0 1.9234-0.85653 1.9234-1.9213v-0.47646h12.014v0.47646c0 1.0648 0.85787 1.9213 1.9234 1.9213h3.3755c1.0655 0 1.9234-0.85653 1.9234-1.9213v-10.588c0-1.0648-0.85786-1.9213-1.9234-1.9213h-3.3755c-5.4826 0.0358-11.132 0.0125-15.861 0zm0.49609 2.4123h21.644v9.617h-2.3978v-2.3916h-2.0691v-0.89193h-0.74311v0.89193h-1.664v-0.89193h-0.74001v0.89193h-1.664v-0.89193h-0.74001v0.89193h-1.664v-0.89193h-0.74311v0.89193h-1.664v-0.89193h-0.74104v0.89193h-1.664v-0.89193h-0.68316v0.89193h-2.0691v2.3916h-2.3978v-3.2835z"/><ellipse cx="8.4499" cy="281.03" rx="1.7076" ry="1.7064"/><ellipse cx="18.033" cy="281.03" rx="1.7076" ry="1.7064"/></g></svg>
|
||||
|
After Width: | Height: | Size: 913 B |
Vendored
+4708
File diff suppressed because it is too large
Load Diff
+25
-28
@@ -1,30 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"test"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"two.js": ["src/two-override"]
|
||||
},
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
|
||||
@@ -2525,6 +2525,7 @@ __metadata:
|
||||
style-loader: ^3.3.1
|
||||
tailwindcss: ^3.2.4
|
||||
terser-webpack-plugin: ^5.3.6
|
||||
two.js: ^0.8.10
|
||||
typed-redux-saga: ^1.5.0
|
||||
typescript: ~4.9.3
|
||||
usehooks-ts: ^2.9.1
|
||||
@@ -14769,6 +14770,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"two.js@npm:^0.8.10":
|
||||
version: 0.8.10
|
||||
resolution: "two.js@npm:0.8.10"
|
||||
checksum: 1550d4a406ca7580c981c7753cc7aab7f595b63f0488ff08cd6d05938faa308e62ac2d1719c9422d4361bac83c41295f63d4e2434ca763bd10bc5258def2cb47
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "type-check@npm:0.4.0"
|
||||
|
||||
Reference in New Issue
Block a user