Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions examples/react-backspace/src/MissAccumulatingApp.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { InputStroke, KeyboardLayout } from "emiel";
import { activate, build, loadPresetRuleRoman, VirtualKeys } from "emiel";
import { activate, build, createDirectInputRule, loadPresetRuleRoman, VirtualKeys } from "emiel";
import { useEffect, useMemo, useState } from "react";
import { MissAccumulatingAutomaton } from "./MissAccumulatingAutomaton";

export function MissAccumulatingApp(props: { layout: KeyboardLayout }) {
const romanRule = useMemo(() => loadPresetRuleRoman(props.layout), [props.layout]);
const rule = useMemo(
() => loadPresetRuleRoman(props.layout).merge(createDirectInputRule(props.layout)),
[props.layout],
);
const words = useMemo(() => ["おをひく", "こんとん", "がっこう", "aから@"], []);
const [index, setIndex] = useState(0);
const [lastInputKey, setLastInputKey] = useState<InputStroke | undefined>();

const wrappers = useMemo(
() => words.map((w) => new MissAccumulatingAutomaton(build(romanRule, w))),
[romanRule, words],
() => words.map((w) => new MissAccumulatingAutomaton(build(rule, w))),
[rule, words],
);
const wrapper = wrappers[index];

Expand Down Expand Up @@ -52,7 +55,7 @@ export function MissAccumulatingApp(props: { layout: KeyboardLayout }) {
>
{view.pendingRoman}
<br />
<span style={{ color: "yellow" }}>
<span style={{ color: "#e5484d" }}>
{wrapper.failedInputs
.map((f) =>
props.layout
Expand Down
13 changes: 8 additions & 5 deletions examples/react-backspace/src/MissClearingApp.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { InputStroke, KeyboardLayout } from "emiel";
import { activate, build, loadPresetRuleRoman, VirtualKeys } from "emiel";
import { activate, build, createDirectInputRule, loadPresetRuleRoman, VirtualKeys } from "emiel";
import { useEffect, useMemo, useState } from "react";
import { MissClearingAutomaton } from "./MissClearingAutomaton";

export function MissClearingApp(props: { layout: KeyboardLayout }) {
const romanRule = useMemo(() => loadPresetRuleRoman(props.layout), [props.layout]);
const rule = useMemo(
() => loadPresetRuleRoman(props.layout).merge(createDirectInputRule(props.layout)),
[props.layout],
);
const words = useMemo(() => ["おをひく", "こんとん", "がっこう", "aから@"], []);
const [index, setIndex] = useState(0);
const [lastInputKey, setLastInputKey] = useState<InputStroke | undefined>();

const wrappers = useMemo(
() => words.map((w) => new MissClearingAutomaton(build(romanRule, w))),
[romanRule, words],
() => words.map((w) => new MissClearingAutomaton(build(rule, w))),
[rule, words],
);
const wrapper = wrappers[index];

Expand Down Expand Up @@ -52,7 +55,7 @@ export function MissClearingApp(props: { layout: KeyboardLayout }) {
>
{view.pendingRoman}
<br />
<span style={{ color: "yellow" }}>
<span style={{ color: "#e5484d" }}>
{wrapper.failedInputs
.map((f) =>
props.layout
Expand Down
13 changes: 8 additions & 5 deletions examples/react-backspace/src/MissCountingApp.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { InputStroke, KeyboardLayout } from "emiel";
import { activate, build, loadPresetRuleRoman, VirtualKeys } from "emiel";
import { activate, build, createDirectInputRule, loadPresetRuleRoman, VirtualKeys } from "emiel";
import { useEffect, useMemo, useState } from "react";
import { MissCountingAutomaton } from "./MissCountingAutomaton";

export function MissCountingApp(props: { layout: KeyboardLayout }) {
const romanRule = useMemo(() => loadPresetRuleRoman(props.layout), [props.layout]);
const rule = useMemo(
() => loadPresetRuleRoman(props.layout).merge(createDirectInputRule(props.layout)),
[props.layout],
);
const words = useMemo(() => ["おをひく", "こんとん", "がっこう", "aから@"], []);
const [index, setIndex] = useState(0);
const [lastInputKey, setLastInputKey] = useState<InputStroke | undefined>();

const wrappers = useMemo(
() => words.map((w) => new MissCountingAutomaton(build(romanRule, w))),
[romanRule, words],
() => words.map((w) => new MissCountingAutomaton(build(rule, w))),
[rule, words],
);
const wrapper = wrappers[index];

Expand Down Expand Up @@ -52,7 +55,7 @@ export function MissCountingApp(props: { layout: KeyboardLayout }) {
>
{view.pendingRoman}
<br />
<span style={{ color: "yellow" }}>
<span style={{ color: "#e5484d" }}>
{wrapper.failedInputs
.map((f) =>
props.layout
Expand Down
10 changes: 6 additions & 4 deletions examples/react-keyboardguide/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,11 @@ function KeyCode(props: { placement: KeyPlacement; isKeyDowned: boolean }) {
fontSize: "12pt",
left: `${rect.x}px`,
top: `${rect.y}px`,
border: "1px yellow solid",
border: "1px #888 solid",
width: `${rect.width}px`,
height: `${rect.height}px`,
backgroundColor: props.isKeyDowned ? "yellow" : "",
backgroundColor: props.isKeyDowned ? "#ffd000" : "",
color: props.isKeyDowned ? "#000" : undefined,
}}
>
<span style={{ textAlign: "center", wordBreak: "break-all" }}>
Expand All @@ -259,10 +260,11 @@ function KeyWithLabel(props: { placement: KeyPlacement; isKeyDowned: boolean })
fontSize: "12pt",
left: `${rect.x}px`,
top: `${rect.y}px`,
border: "1px yellow solid",
border: "1px #888 solid",
width: `${rect.width}px`,
height: `${rect.height}px`,
backgroundColor: props.isKeyDowned ? "yellow" : "",
backgroundColor: props.isKeyDowned ? "#ffd000" : "",
color: props.isKeyDowned ? "#000" : undefined,
}}
>
<div
Expand Down
2 changes: 1 addition & 1 deletion examples/react-multi-word/src/word.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function Word(props: { automaton: emiel.Automaton }) {
return (
<div
style={{
border: "2px solid yellow",
border: "2px solid #888",
paddingLeft: "3rem",
paddingRight: "3rem",
}}
Expand Down
22 changes: 18 additions & 4 deletions examples/react-roman-or-kana/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { Automaton, InputEvent, InputStroke, KeyboardLayout, Rule } from "e
import {
activate,
build,
createDirectInputRule,
detectKeyboardLayout,
loadPresetRuleJisKana,
loadPresetRuleRoman,
logging,
VirtualKeys,
} from "emiel";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import "./App.css";

logging.enable("keyboard.*", "automaton.*");
Expand Down Expand Up @@ -86,16 +87,27 @@ function App() {

function Typing(props: { layout: KeyboardLayout }) {
const words = ["おをひく", "こんとん", "がっこう", "aから@"];
const romanRule = useMemo(() => loadPresetRuleRoman(props.layout), [props.layout]);
const kanaRule = useMemo(() => loadPresetRuleJisKana(), []);
const directInputRule = useMemo(() => createDirectInputRule(props.layout), [props.layout]);
const romanRule = useMemo(
() => loadPresetRuleRoman(props.layout).merge(directInputRule),
[props.layout, directInputRule],
);
const kanaRule = useMemo(() => loadPresetRuleJisKana().merge(directInputRule), [directInputRule]);
const [selectors, setSelectors] = useState<WordCandidates[]>(() =>
words.map((w) => createCandidates(w, romanRule, kanaRule)),
);
const [lastInputKey, setLastInputKey] = useState<InputStroke | undefined>();
const [wordIndex, setWordIndex] = useState(0);
// activate を打鍵ごとに再登録すると、その内部 KeyboardState がリセットされ、
// Shift など押しっぱなしの修飾キーの押下状態が失われてしまう(例: Shift を押した状態で
// 0 を打鍵しても「を」が修飾なし入力として failed になる)。
// そのためハンドラはマウント時に一度だけ登録し、最新の state は ref 経由で参照する。
const stateRef = useRef({ selectors, wordIndex });
stateRef.current = { selectors, wordIndex };
useEffect(() => {
return activate(window, (e) => {
setLastInputKey(e.input);
const { selectors, wordIndex } = stateRef.current;
if (e.input.key === VirtualKeys.Escape) {
const reset = resetCandidates(selectors[wordIndex]);
setSelectors((prev) => prev.map((c, i) => (i === wordIndex ? reset : c)));
Expand Down Expand Up @@ -126,7 +138,9 @@ function Typing(props: { layout: KeyboardLayout }) {
setSelectors((prev) => prev.map((c, i) => (i === wordIndex ? next : c)));
}
});
}, [wordIndex, selectors, words.length]);
// マウント時に一度だけ登録する(依存配列は空)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const wordCandidates = selectors[wordIndex];
// finishedWord / pendingWord は word ベース表示で roman/kana どちらから
Expand Down
112 changes: 58 additions & 54 deletions examples/react-stroke-graph/src/grpahStyle.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,64 @@
import type { StylesheetStyle } from "cytoscape";

export const cyStylesheet: StylesheetStyle[] = [
{
selector: "node",
style: {
label: "data(label)",
width: 30,
height: 30,
backgroundColor: "white",
"border-color": "yellow",
"border-width": 2,
shape: "round-rectangle",
"text-valign": "center",
"text-halign": "center",
// ライト / ダーク両モードで視認できるよう、テーマに応じた色を返す
export function getCyStylesheet(dark: boolean): StylesheetStyle[] {
// 通常ノードはページ背景と同化しないよう、テーマと逆寄りの淡色で塗る
const nodeBackgroundColor = dark ? "#ffffff" : "#f0f0f0";
// ノード番号やボーダーは背景とのコントラストを確保する
const nodeTextColor = dark ? "#000000" : "#213547";
const borderColor = dark ? "#e6e600" : "#cccc00";
// edge ラベル(ローマ字)は白固定だと light モードで消えるためテーマ追従させる
const edgeLabelColor = dark ? "#ffffff" : "#213547";

const nodeBase = {
label: "data(label)",
width: 30,
height: 30,
"border-color": borderColor,
"border-width": 2,
shape: "round-rectangle",
"text-valign": "center",
"text-halign": "center",
} as const;

return [
{
selector: "node",
style: {
...nodeBase,
backgroundColor: nodeBackgroundColor,
color: nodeTextColor,
},
},
},
{
selector: "node.success",
style: {
label: "data(label)",
width: 30,
height: 30,
backgroundColor: "green",
"border-color": "yellow",
"border-width": 2,
shape: "round-rectangle",
"text-valign": "center",
"text-halign": "center",
{
selector: "node.success",
style: {
...nodeBase,
backgroundColor: "green",
color: "#ffffff",
},
},
},
{
selector: "node.miss",
style: {
label: "data(label)",
width: 30,
height: 30,
backgroundColor: "red",
"border-color": "yellow",
"border-width": 2,
shape: "round-rectangle",
"text-valign": "center",
"text-halign": "center",
{
selector: "node.miss",
style: {
...nodeBase,
backgroundColor: "red",
color: "#ffffff",
},
},
},
{
selector: "edge",
style: {
label: "data(label)",
color: "white",
width: 3,
"line-color": "green",
"text-margin-y": -10,
"arrow-scale": 1,
"curve-style": "bezier",
"target-arrow-color": "green",
"target-arrow-shape": "triangle",
{
selector: "edge",
style: {
label: "data(label)",
color: edgeLabelColor,
width: 3,
"line-color": "green",
"text-margin-y": -10,
"arrow-scale": 1,
"curve-style": "bezier",
"target-arrow-color": "green",
"target-arrow-shape": "triangle",
},
},
},
];
];
}
21 changes: 17 additions & 4 deletions examples/react-stroke-graph/src/typingGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,29 @@ import type { Automaton, InputStroke } from "emiel";
import { activate } from "emiel";
import { useEffect, useMemo, useRef, useState } from "react";
import { buildGraphData } from "./graphData";
import { cyStylesheet } from "./grpahStyle";
import { getCyStylesheet } from "./grpahStyle";

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
cytoscape.use(dagre);

// OS のカラースキーム(ライト / ダーク)を監視し、変更に追従する
function usePrefersDark() {
const [dark, setDark] = useState(() => window.matchMedia("(prefers-color-scheme: dark)").matches);
useEffect(() => {
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = (e: MediaQueryListEvent) => setDark(e.matches);
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
}, []);
return dark;
}

export function TypingGraph(props: { automaton: Automaton; onFinished: () => void }) {
const { automaton, onFinished } = props;
const graphData = useMemo(() => buildGraphData(automaton.startNode), [automaton]);
const htmlElem = useRef(null);
const [, setLastInputKey] = useState<InputStroke | undefined>();
const dark = usePrefersDark();
useEffect(() => {
if (htmlElem.current === null) {
return;
Expand All @@ -23,7 +36,7 @@ export function TypingGraph(props: { automaton: Automaton; onFinished: () => voi
// @ts-expect-error rankDir is not defined in cytoscape
layout: { name: "dagre", rankDir: "LR" },
userZoomingEnabled: false,
style: cyStylesheet,
style: getCyStylesheet(dark),
});
cy.remove(cy.elements());
cy.add([...graphData.nodes, ...graphData.edges]);
Expand All @@ -48,7 +61,7 @@ export function TypingGraph(props: { automaton: Automaton; onFinished: () => voi
}
}
});
}, [automaton, graphData, onFinished]);
}, [automaton, graphData, onFinished, dark]);

const view = automaton.currentView();
return (
Expand All @@ -61,7 +74,7 @@ export function TypingGraph(props: { automaton: Automaton; onFinished: () => voi
style={{
width: "600px",
height: "300px",
border: "1px solid white",
border: "1px solid #888",
margin: "0 auto",
}}
/>
Expand Down