From a31a3762cb68091eebab2e901762eb8fe9825160 Mon Sep 17 00:00:00 2001 From: Qijia Liu Date: Wed, 31 Jan 2024 03:27:00 -0500 Subject: [PATCH] refactor: TypeScript, SCSS and Parcel (#7) Co-authored-by: ksqsf --- .eslintrc.cjs | 24 +++++ .github/workflows/ci.yml | 14 +++ .gitignore | 4 + .stylelintrc.json | 3 + CMakeLists.txt | 23 +++- README.md | 23 +++- index.html | 180 -------------------------------- package.json | 30 ++++++ page/api.ts | 107 +++++++++++++++++++ page/generic.scss | 46 ++++++++ page/global.d.ts | 9 ++ page/index.html | 28 +++++ page/macos.scss | 24 +++++ page/user.scss | 1 + preview/CMakeLists.txt | 2 +- src/CMakeLists.txt | 2 +- src/webview_candidate_window.mm | 4 +- tsconfig.json | 13 +++ 18 files changed, 345 insertions(+), 192 deletions(-) create mode 100644 .eslintrc.cjs create mode 100644 .stylelintrc.json delete mode 100644 index.html create mode 100644 package.json create mode 100644 page/api.ts create mode 100644 page/generic.scss create mode 100644 page/global.d.ts create mode 100644 page/index.html create mode 100644 page/macos.scss create mode 100644 page/user.scss create mode 100644 tsconfig.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..07ce29f --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,24 @@ +module.exports = { + env: { + browser: true, + es2021: true + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'standard' + ], + parserOptions: { + ecmaVersion: 'latest', + parser: '@typescript-eslint/parser', + sourceType: 'module' + }, + plugins: [ + '@typescript-eslint', + 'eslint-plugin-html' + ], + rules: { + "@typescript-eslint/no-unused-vars": "error", + "no-undef": 0, + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a04ac3..0734300 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,20 @@ jobs: with: submodules: true + - uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Install node dependencies + run: | + npm i -g pnpm + pnpm i + + - name: Lint and Check type + run: | + pnpm run lint + pnpm run check + - name: Install dependencies run: | brew install ninja diff --git a/.gitignore b/.gitignore index bab1292..e8bf6d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ build cache include/html_template.hpp +node_modules +pnpm-lock.yaml +dist +.parcel-cache diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..eff2560 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-standard-scss" +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 363f7fb..33b6ef4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,14 +15,27 @@ pkg_check_modules(NlohmannJson REQUIRED IMPORTED_TARGET "nlohmann_json") include_directories(webview) add_custom_target(patch_webview - COMMAND git reset --hard - COMMAND git apply "${PROJECT_SOURCE_DIR}/patches/webview.patch" + COMMAND + [ ! -f "${PROJECT_SOURCE_DIR}/.patched" ] + && git reset --hard + && git apply "${PROJECT_SOURCE_DIR}/patches/webview.patch" + && touch "${PROJECT_SOURCE_DIR}/.patched" + || [ -f "${PROJECT_SOURCE_DIR}/.patched" ] WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/webview" + COMMENT "Patching webview..." ) -file(READ "${PROJECT_SOURCE_DIR}/index.html" HTML_TEMPLATE) -configure_file("${PROJECT_SOURCE_DIR}/include/html_template.hpp.in" - "${PROJECT_SOURCE_DIR}/include/html_template.hpp" +file(GLOB HTML_SOURCES CONFIGURE_DEPENDS "page/*") +add_custom_command( + OUTPUT ${PROJECT_SOURCE_DIR}/include/html_template.hpp + COMMAND pnpm run clean && pnpm run build + COMMAND xxd -n HTML_TEMPLATE -i "${PROJECT_SOURCE_DIR}/dist/index.html" > "${PROJECT_SOURCE_DIR}/include/html_template.hpp" + DEPENDS ${HTML_SOURCES} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMENT "Generating the HTML template..." +) +add_custom_target(GenerateHTML ALL + DEPENDS ${PROJECT_SOURCE_DIR}/include/html_template.hpp ) add_subdirectory(src) diff --git a/README.md b/README.md index 7b557bf..1fd9d7e 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,22 @@ powered by [webview](https://github.com/webview/webview). It can be developed independently of fcitx5. -## Tweak style -To change style, you don't need to build the project. -Just edit [index.html](index.html) and view it in a browser. +## Install Node dependencies -On macOS, it's best to use Safari since the real candidate window is rendered by WebKit. +You may use [nvm](https://github.com/nvm-sh/nvm) +to install node, then + +```sh +npm i -g pnpm +pnpm i +``` + +## Tweak style +```sh +npm run dev +``` +Open http://localhost:1234 with Safari, +as the real candidate window on macOS is rendered by WebKit. Execute the following JavaScript code to show candidates and more: ```js @@ -28,6 +39,10 @@ setCandidates([], [], -1) updateInputPanel("", "A", "") ``` +To change style, just edit [user.scss](./page/user.scss) and refresh the page. +In order to override predefined style in [macos.scss](./page/macos.scss), +add `div` to the selectors so it has higher precedence. + ## Build ```sh ./install-deps.sh diff --git a/index.html b/index.html deleted file mode 100644 index 0152427..0000000 --- a/index.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - -
-
- - -
- -
-
-
- - - diff --git a/package.json b/package.json new file mode 100644 index 0000000..5bc3d2a --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "fcitx5-webview", + "version": "0.1.0", + "description": "Candidate window for desktop, based on webview", + "type": "module", + "scripts": { + "dev": "parcel page/index.html", + "lint": "pnpm run eslint && pnpm run stylelint", + "lint:fix": "pnpm run eslint:fix && pnpm run stylelint:fix", + "eslint": "eslint --ext .ts,.html page", + "eslint:fix": "eslint --fix --ext .ts,.html page", + "stylelint": "stylelint page/*.scss", + "stylelint:fix": "stylelint --fix page/*.scss", + "check": "tsc --noEmit", + "clean": "rm -rf dist .parcel-cache", + "build": "parcel build page/index.html" + }, + "license": "GPL-3.0-or-later", + "devDependencies": { + "@parcel/transformer-sass": "2.11.0", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "eslint": "^8.56.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-html": "^7.1.0", + "parcel": "^2.11.0", + "stylelint": "^16.2.0", + "stylelint-config-standard-scss": "^13.0.0", + "typescript": "^5.3.3" + } +} diff --git a/page/api.ts b/page/api.ts new file mode 100644 index 0000000..cbfc5f7 --- /dev/null +++ b/page/api.ts @@ -0,0 +1,107 @@ +const panel = document.querySelector('.panel')! +const candidates = panel.querySelector('.candidates')! +const preedit = document.querySelector('.preedit')! +const auxUp = document.querySelector('.aux-up')! +const auxDown = document.querySelector('.aux-down')! + +let cursorX = 0 +let cursorY = 0 +let pressed = false +let dragging = false +let startX = 0 +let startY = 0 + +document.addEventListener('mousedown', e => { + pressed = true + startX = e.clientX + startY = e.clientY +}) + +document.addEventListener('mousemove', e => { + if (!pressed) { + return + } + dragging = true + // minus because macOS has bottom-left (0, 0) + resize(cursorX + (e.clientX - startX), cursorY - (e.clientY - startY)) +}) + +document.addEventListener('mouseup', e => { + pressed = false + if (dragging) { + dragging = false + return + } + let target = e.target as Element + if (target === candidates || !candidates.contains(target)) { + return + } + while (target.parentElement !== candidates) { + target = target.parentElement! + } + for (let i = 0; i < candidates.childElementCount; ++i) { + if (candidates.children[i] === target) { + return window._select(i) + } + } +}) + +function setLayout (layout : 0 | 1) { + switch (layout) { + case 0: + candidates.classList.remove('vertical') + candidates.classList.add('horizontal') + break + case 1: + candidates.classList.remove('horizontal') + candidates.classList.add('vertical') + } +} + +function setCandidates (cands: string[], labels: string[], highlighted: number) { + candidates.innerHTML = '' + for (let i = 0; i < cands.length; ++i) { + const candidate = document.createElement('div') + candidate.classList.add('candidate') + if (i === highlighted) { + candidate.classList.add('highlighted') + } + const label = document.createElement('div') + label.classList.add('label') + label.innerHTML = labels[i] + const text = document.createElement('div') + text.classList.add('text') + text.innerHTML = cands[i] + candidate.appendChild(label) + candidate.appendChild(text) + candidates.appendChild(candidate) + } +} + +function resize (x: number, y: number) { + cursorX = x + cursorY = y + const rect = panel.getBoundingClientRect() + window._resize(x, y, rect.width, rect.height) +} + +function updateElement (element: Element, innerHTML: string) { + if (innerHTML === '') { + element.classList.add('hidden') + } else { + element.innerHTML = innerHTML + element.classList.remove('hidden') + } +} + +function updateInputPanel (preeditHTML: string, auxUpHTML: string, auxDownHTML: string) { + updateElement(preedit, preeditHTML) + updateElement(auxUp, auxUpHTML) + updateElement(auxDown, auxDownHTML) +} + +// JavaScript APIs that webview_candidate_window.mm calls +window.setCandidates = setCandidates +window.setLayout = setLayout +window.updateInputPanel = updateInputPanel +window.resize = resize diff --git a/page/generic.scss b/page/generic.scss new file mode 100644 index 0000000..6815a29 --- /dev/null +++ b/page/generic.scss @@ -0,0 +1,46 @@ +body { + background: rgb(0 0 0 / 0%); /* transparent, draw panel as you wish */ + margin: 0; /* default is 8px */ + overflow: hidden; /* no scrollbar */ + width: 1920px; /* big enough, disregard window size */ + height: 1080px; + user-select: none; /* disable text select */ +} + +.panel { + overflow: hidden; /* needed because of border-radius */ + display: inline-block; /* wrap content, not fill parent */ +} + +.preedit, .aux-up, .aux-down { + display: inline-flex; + align-items: center; + justify-content: center; + + &.hidden { + display: none; /* needed because the above display has higher precedence than .hidden's */ + } +} + +.candidates { + display: flex; + + &.vertical { + flex-direction: column; + } + + &.horizontal { + flex-direction: row; + } +} + +.candidate { + display: flex; + gap: 6px; + align-items: center; /* English words have lower height */ + line-height: 1em; /* align label and candidates */ +} + +.hidden { + display: none; +} diff --git a/page/global.d.ts b/page/global.d.ts new file mode 100644 index 0000000..ea3c04c --- /dev/null +++ b/page/global.d.ts @@ -0,0 +1,9 @@ +declare global { + interface Window { + // C++ APIs that api.ts calls + _select: (index: number) => void + _resize: (x: number, y: number, width: number, height: number) => void + } +} + +export {} diff --git a/page/index.html b/page/index.html new file mode 100644 index 0000000..15bca84 --- /dev/null +++ b/page/index.html @@ -0,0 +1,28 @@ + + + + + + + + +
+
+ + +
+ +
+
+
+ + + diff --git a/page/macos.scss b/page/macos.scss new file mode 100644 index 0000000..d95ee96 --- /dev/null +++ b/page/macos.scss @@ -0,0 +1,24 @@ +/* use Digital Color Meter with sRGB mode to get correct color */ +.macos { + &.panel { + border: 1px solid #5c5c5c; + border-radius: 6px; + background-color: #333; + font-family: sans-serif; + } + + .candidate, .preedit, .aux-up, .aux-down { + color: white; + min-height: 24px; /* compromise to 🀄's height */ + min-width: 16px; + padding: 3px 7px; /* combine min-height, min-width and padding to make aux-up a square for 小 and A */ + } + + .candidate.highlighted { + background-color: #0059d0; + } + + .candidates.vertical .candidate:not(:first-child) { + border-top: 1px solid #5c5c5c; + } +} diff --git a/page/user.scss b/page/user.scss new file mode 100644 index 0000000..b6a65bd --- /dev/null +++ b/page/user.scss @@ -0,0 +1 @@ +// Customize your style here. diff --git a/preview/CMakeLists.txt b/preview/CMakeLists.txt index 47627ba..7c2ddc9 100644 --- a/preview/CMakeLists.txt +++ b/preview/CMakeLists.txt @@ -7,4 +7,4 @@ target_include_directories(preview PRIVATE "${PROJECT_SOURCE_DIR}/include") target_link_libraries(preview WebviewCandidateWindow "-framework WebKit" "-framework Cocoa") target_compile_options(preview PRIVATE "-Wno-auto-var-id") -add_dependencies(preview patch_webview) +add_dependencies(preview patch_webview GenerateHTML) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 24306ad..a7e19fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,4 +4,4 @@ target_include_directories(WebviewCandidateWindow PRIVATE "${PROJECT_SOURCE_DIR} target_link_libraries(WebviewCandidateWindow "-framework WebKit" PkgConfig::NlohmannJson) target_compile_options(WebviewCandidateWindow PRIVATE "-Wno-auto-var-id") -add_dependencies(WebviewCandidateWindow patch_webview) +add_dependencies(WebviewCandidateWindow patch_webview GenerateHTML) diff --git a/src/webview_candidate_window.mm b/src/webview_candidate_window.mm index f7d2960..8d9848a 100644 --- a/src/webview_candidate_window.mm +++ b/src/webview_candidate_window.mm @@ -41,7 +41,9 @@ bind("_select", [this](size_t i) { select_callback(i); }); - w_.set_html(HTML_TEMPLATE); + std::string html_template(reinterpret_cast(HTML_TEMPLATE), + HTML_TEMPLATE_len); + w_.set_html(html_template.c_str()); } WebviewCandidateWindow::~WebviewCandidateWindow() {} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a85bab3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"] + }, + "include": ["page/*.ts"] +}