diff --git a/include/candidate_window.hpp b/include/candidate_window.hpp index be4fddd..ff72019 100644 --- a/include/candidate_window.hpp +++ b/include/candidate_window.hpp @@ -49,9 +49,12 @@ class CandidateWindow { select_callback = callback; } + void set_cursor_text(const std::string &text) { cursor_text_ = text; } + protected: std::function init_callback = []() {}; std::function select_callback = [](size_t) {}; + std::string cursor_text_ = ""; }; } // namespace candidate_window #endif diff --git a/page/customize.ts b/page/customize.ts index ac8955f..c503e19 100644 --- a/page/customize.ts +++ b/page/customize.ts @@ -1,4 +1,7 @@ -import { setBlur } from './ux' +import { + setBlur, + setBlink +} from './ux' type CONFIG_BOOL = 'False' | 'True' @@ -36,6 +39,10 @@ type STYLE_JSON = { PreeditFontFamily: FONT_FAMILY PreeditFontSize: string } + Cursor: { + Style: 'Blink' | 'Static' | 'Text' + Text: string + } BorderWidth: string BorderRadius: string HorizontalDividerWidth: string @@ -48,6 +55,7 @@ const CANDIDATES = `${PANEL} .candidates` const LABEL = `${PANEL} .label` const TEXT = `${PANEL} .text` const PREEDIT = `${PANEL} .preedit` +const CURSOR_NO_TEXT = `${PANEL} .cursor.no-text` const PANEL_LIGHT = `${PANEL}.light` const PANEL_LIGHT_HIGHLIGHT = `${PANEL_LIGHT} .candidate.highlighted` @@ -59,6 +67,7 @@ const PREEDIT_LIGHT = `${PANEL_LIGHT} .preedit` const HEADER_LIGHT_BACKGROUND = `${PANEL_LIGHT} .header` const CANDIDATE_LIGHT_BACKGROUND = `${PANEL_LIGHT} .candidate` const PANEL_LIGHT_HORIZONTAL_DIVIDER = `${PANEL_LIGHT} ${HORIZONTAL_DIVIDER}` +const CURSOR_NO_TEXT_LIGHT = `${PANEL_LIGHT} .cursor.no-text` const PANEL_DARK = `${PANEL}.dark` const PANEL_DARK_HIGHLIGHT = `${PANEL_DARK} .candidate.highlighted` @@ -70,6 +79,7 @@ const PREEDIT_DARK = `${PANEL_DARK} .preedit` const HEADER_DARK_BACKGROUND = `${PANEL_DARK} .header` const CANDIDATE_DARK_BACKGROUND = `${PANEL_DARK} .candidate` const PANEL_DARK_HORIZONTAL_DIVIDER = `${PANEL_DARK} ${HORIZONTAL_DIVIDER}` +const CURSOR_NO_TEXT_DARK = `${PANEL_DARK} .cursor.no-text` function px (n: string) { return `${n}px` @@ -91,6 +101,7 @@ export function setStyle (style: string) { rules[TEXT] = {} rules[LABEL] = {} rules[PREEDIT] = {} + rules[CURSOR_NO_TEXT] = {} if (j.LightMode.OverrideDefault === 'True') { rules[PANEL_LIGHT_HIGHLIGHT] = { @@ -118,6 +129,9 @@ export function setStyle (style: string) { rules[PREEDIT_LIGHT] = { color: j.LightMode.PreeditColor } + rules[CURSOR_NO_TEXT_LIGHT] = { + 'background-color': j.LightMode.PreeditColor + } rules[PANEL_LIGHT] = { 'border-color': j.LightMode.BorderColor } @@ -136,6 +150,7 @@ export function setStyle (style: string) { rules[TEXT_DARK] = rules[TEXT_LIGHT] rules[LABEL_DARK] = rules[LABEL_LIGHT] rules[PREEDIT_DARK] = rules[PREEDIT_LIGHT] + rules[CURSOR_NO_TEXT_DARK] = rules[CURSOR_NO_TEXT_LIGHT] rules[PANEL_DARK] = rules[PANEL_LIGHT] rules[PANEL_DARK_HORIZONTAL_DIVIDER] = rules[PANEL_LIGHT_HORIZONTAL_DIVIDER] } else { @@ -163,6 +178,9 @@ export function setStyle (style: string) { rules[PREEDIT_DARK] = { color: j.DarkMode.PreeditColor } + rules[CURSOR_NO_TEXT_DARK] = { + 'background-color': j.DarkMode.PreeditColor + } rules[PANEL_DARK] = { 'border-color': j.DarkMode.BorderColor } @@ -199,6 +217,10 @@ export function setStyle (style: string) { setFontFamily(rules[PREEDIT], j.Font.PreeditFontFamily) rules[PREEDIT]['font-size'] = px(j.Font.PreeditFontSize) + // Cursor height should be the same with preedit + rules[CURSOR_NO_TEXT].height = px(j.Font.PreeditFontSize) + + setBlink(j.Cursor.Style === 'Blink') rules[PANEL]['border-width'] = px(j.BorderWidth) rules[PANEL]['border-radius'] = px(j.BorderRadius) diff --git a/page/generic.scss b/page/generic.scss index d603297..fa8f3af 100644 --- a/page/generic.scss +++ b/page/generic.scss @@ -16,6 +16,7 @@ body { display: inline-flex; align-items: center; justify-content: center; + line-height: 1em; /* align preedit and text cursor */ &.hidden { display: none; /* needed because the above display has higher precedence than .hidden's */ diff --git a/page/macos.scss b/page/macos.scss index 5d61821..4d3060a 100644 --- a/page/macos.scss +++ b/page/macos.scss @@ -43,6 +43,13 @@ $dark-graphite: rgb(105 105 105); transform: translate(25px, 25px); } + .cursor.no-text { + width: 1px; + height: 16px; + margin-left: 1px; + margin-right: 1px; + } + .candidate, .preedit, .aux-up, .aux-down { min-height: 24px; /* compromise to 🀄's height */ min-width: 16px; @@ -61,6 +68,10 @@ $dark-graphite: rgb(105 105 105); border-color: $panel-border-color-light; } + .cursor.no-text { + background-color: $text-color-light; + } + /* stylelint-disable-next-line no-descending-specificity */ .candidate, .header { color: $text-color-light; @@ -113,6 +124,10 @@ $dark-graphite: rgb(105 105 105); border-color: $panel-border-color-dark; } + .cursor.no-text { + background-color: $text-color-dark; + } + /* stylelint-disable-next-line no-descending-specificity */ .candidate, .header { color: $text-color-dark; diff --git a/page/ux.ts b/page/ux.ts index 0e7765c..e9acb7f 100644 --- a/page/ux.ts +++ b/page/ux.ts @@ -123,3 +123,27 @@ function redrawBlur () { blurSwitch = !blurSwitch } setInterval(redrawBlur, 40) + +export function showCursor (show: boolean) { + const cursor = document.querySelector('.cursor') + if (cursor) { + (cursor).style.opacity = show ? '1' : '0' + } +} + +let blinkEnabled = true +export function setBlink (enabled: boolean) { + blinkEnabled = enabled + if (!enabled) { + showCursor(true) + } +} + +let blinkSwitch = false +setInterval(() => { + if (!blinkEnabled) { + return + } + showCursor(blinkSwitch) + blinkSwitch = !blinkSwitch +}, 500) diff --git a/src/webview_candidate_window.mm b/src/webview_candidate_window.mm index 8aeb6e8..6a39859 100644 --- a/src/webview_candidate_window.mm +++ b/src/webview_candidate_window.mm @@ -192,11 +192,32 @@ static void build_html_close_tags(std::stringstream &ss, int flags) { ss << ""; } -static std::string formatted_to_html(const formatted &f) { +static std::string formatted_to_html(const formatted &f, + const std::string &cursor_text = "", + int cursor = -1) { std::stringstream ss; + int cursor_pos = 0; for (const auto &slice : f) { build_html_open_tags(ss, slice.second); - ss << escape_html(slice.first); + auto size = + (int)slice.first + .size(); // ensure signed comparison since cursor may be -1 + if (cursor_pos <= cursor && cursor <= cursor_pos + size) { + ss << escape_html(slice.first.substr(0, cursor - cursor_pos)); + if (cursor_text.empty()) { + ss << "
"; + } else { + ss << "
"; + ss << escape_html(cursor_text); + } + ss << "
"; + ss << escape_html(slice.first.substr(cursor - cursor_pos)); + // Do not draw cursor again when it's at the end of current slice + cursor = -1; + } else { + ss << escape_html(slice.first); + cursor_pos += size; + } build_html_close_tags(ss, slice.second); } return ss.str(); @@ -206,7 +227,8 @@ static void build_html_close_tags(std::stringstream &ss, int flags) { const formatted &preedit, int preedit_cursor, const formatted &auxUp, const formatted &auxDown) { - invoke_js("updateInputPanel", formatted_to_html(preedit), + invoke_js("updateInputPanel", + formatted_to_html(preedit, cursor_text_, preedit_cursor), formatted_to_html(auxUp), formatted_to_html(auxDown)); }