diff --git a/include/candidate_window.hpp b/include/candidate_window.hpp index 7d0f298..e763b52 100644 --- a/include/candidate_window.hpp +++ b/include/candidate_window.hpp @@ -58,12 +58,21 @@ class CandidateWindow { page_callback = callback; } + void set_paging_buttons(bool pageable, bool has_prev, bool has_next) { + pageable_ = pageable; + has_prev_ = has_prev; + has_next_ = has_next; + } + protected: std::function init_callback = []() {}; std::function select_callback = [](size_t) {}; std::function page_callback = [](bool) {}; std::string cursor_text_ = ""; std::string highlight_mark_text_ = ""; + bool pageable_ = false; + bool has_prev_ = false; + bool has_next_ = false; }; } // namespace candidate_window #endif diff --git a/page/api.ts b/page/api.ts index 6a4e671..2243b79 100644 --- a/page/api.ts +++ b/page/api.ts @@ -1,5 +1,5 @@ import { - candidates, + hoverables, preedit, auxUp, auxDown @@ -22,15 +22,28 @@ function div (...classList: string[]) { return element } +function divider (paging: boolean = false) { + const e = div('divider') + // Is this divider between candidates and paging buttons? + if (paging) { + e.classList.add('divider-paging') + } + const dividerStart = div('divider-side') + const dividerMiddle = div('divider-middle') + const dividerEnd = div('divider-side') + e.append(dividerStart, dividerMiddle, dividerEnd) + return e +} + function setLayout (layout : 0 | 1) { switch (layout) { case 0: - candidates.classList.remove('vertical') - candidates.classList.add('horizontal') + hoverables.classList.remove('vertical') + hoverables.classList.add('horizontal') break case 1: - candidates.classList.remove('horizontal') - candidates.classList.add('vertical') + hoverables.classList.remove('horizontal') + hoverables.classList.add('vertical') } } @@ -44,29 +57,35 @@ function moveHighlight (from: Element | null, to: Element | null) { } } -function setCandidates (cands: string[], labels: string[], highlighted: number, markText: string) { - candidates.innerHTML = '' +// font-awesome +// Use 2 icons instead of flipping one to avoid 1-pixel shift bug. +const common = '' +const caretLeft = common.replace('{}', 'M192 127.338v257.324c0 17.818-21.543 26.741-34.142 14.142L29.196 270.142c-7.81-7.81-7.81-20.474 0-28.284l128.662-128.662c12.599-12.6 34.142-3.676 34.142 14.142z') +const caretRight = common.replace('{}', 'M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z') + +function setCandidates (cands: string[], labels: string[], highlighted: number, markText: string, pageable: boolean, hasPrev: boolean, hasNext: boolean) { + hoverables.innerHTML = '' for (let i = 0; i < cands.length; ++i) { - const candidate = div('candidate') + const candidate = div('candidate', 'hoverable') + if (i === 0) { + candidate.classList.add('candidate-first') + } else { + hoverables.append(divider()) + } if (i === highlighted) { candidate.classList.add('highlighted', 'highlighted-original') } - - candidate.addEventListener('mouseenter', () => { - const hoverBehavior = getHoverBehavior() - if (hoverBehavior === 'Move') { - const lastHighlighted = candidates.querySelector('.highlighted') - moveHighlight(lastHighlighted, candidate) - } - }) + if (i === cands.length - 1) { + candidate.classList.add('candidate-last') + } const label = div('label') label.innerHTML = labels[i] const text = div('text') text.innerHTML = cands[i] - const candidateInner = div('candidate-inner') + const candidateInner = div('candidate-inner', 'hoverable-inner') // Render placeholder for vertical non-highlighted candidates - if (candidates.classList.contains('vertical') || i === highlighted) { + if (hoverables.classList.contains('vertical') || i === highlighted) { const mark = div('mark') if (markText === '') { mark.classList.add('no-text') @@ -77,7 +96,41 @@ function setCandidates (cands: string[], labels: string[], highlighted: number, } candidateInner.append(label, text) candidate.append(candidateInner) - candidates.append(candidate) + hoverables.append(candidate) + } + if (pageable) { + hoverables.append(divider(true)) + + const prev = div('prev', 'hoverable') + const prevInner = div('paging-inner') + if (hasPrev) { + prevInner.classList.add('hoverable-inner') + } + prevInner.innerHTML = caretLeft + prev.appendChild(prevInner) + + const next = div('next', 'hoverable') + const nextInner = div('paging-inner') + if (hasNext) { + nextInner.classList.add('hoverable-inner') + } + nextInner.innerHTML = caretRight + next.appendChild(nextInner) + + const paging = div('paging') + paging.appendChild(prev) + paging.appendChild(next) + hoverables.appendChild(paging) + } + + for (const hoverable of hoverables.querySelectorAll('.hoverable')) { + hoverable.addEventListener('mouseenter', () => { + const hoverBehavior = getHoverBehavior() + if (hoverBehavior === 'Move') { + const lastHighlighted = hoverables.querySelector('.highlighted') + moveHighlight(lastHighlighted, hoverable) + } + }) } } @@ -96,16 +149,16 @@ function updateInputPanel (preeditHTML: string, auxUpHTML: string, auxDownHTML: updateElement(auxDown, auxDownHTML) } -candidates.addEventListener('mouseleave', () => { +hoverables.addEventListener('mouseleave', () => { const hoverBehavior = getHoverBehavior() if (hoverBehavior === 'Move') { - const lastHighlighted = candidates.querySelector('.highlighted') - const originalHighlighted = candidates.querySelector('.highlighted-original') + const lastHighlighted = hoverables.querySelector('.highlighted') + const originalHighlighted = hoverables.querySelector('.highlighted-original') moveHighlight(lastHighlighted, originalHighlighted) } }) -candidates.addEventListener('wheel', e => { +hoverables.addEventListener('wheel', e => { window._page((e).deltaY > 0) }) diff --git a/page/customize.ts b/page/customize.ts index b912fad..260e6ee 100644 --- a/page/customize.ts +++ b/page/customize.ts @@ -18,9 +18,11 @@ type LIGHT_MODE = { PanelColor: string TextColor: string LabelColor: string + PagingButtonColor: string + DisabledPagingButtonColor: string PreeditColor: string BorderColor: string - HorizontalDividerColor: string + DividerColor: string } type FONT_FAMILY = {[key: string]: string} @@ -65,55 +67,77 @@ type STYLE_JSON = { } } +function lightToDark (light: string) { + return light.replace(PANEL_LIGHT, PANEL_DARK) +} + const PANEL = '.panel.basic' -const HORIZONTAL_DIVIDER = '.candidates.vertical .candidate:not(:first-child)' -const PANEL_HORIZONTAL_DIVIDER = `${PANEL} ${HORIZONTAL_DIVIDER}` -const CANDIDATES = `${PANEL} .candidates` +const PANEL_HORIZONTAL_DIVIDER = `${PANEL} .hoverables.vertical .divider` +const PANEL_HORIZONTAL_DIVIDER_SIDE = `${PANEL} .hoverables.vertical .divider-side` +// left of prev paging button, same with MSPY +const PANEL_VERTICAL_DIVIDER_SIDE = `${PANEL} .hoverables.horizontal .divider-paging .divider-side` +const HOVERABLES = `${PANEL} .hoverables` const LABEL = `${PANEL} .label` const TEXT = `${PANEL} .text` const PREEDIT = `${PANEL} .preedit` const CURSOR_NO_TEXT = `${PANEL} .cursor.no-text` const CANDIDATE_INNER = `${PANEL} .candidate-inner` +const FIRST_CANDIDATE_INNER = '.candidate-first .candidate-inner' +const LAST_CANDIDATE_INNER = '.candidate-last .candidate-inner' +const VERTICAL_CANDIDATE_INNER = `${PANEL} .hoverables.vertical .candidate-inner` +const VERTICAL_FIRST_CANDIDATE_INNER = `${PANEL} .hoverables.vertical ${FIRST_CANDIDATE_INNER}` +const VERTICAL_LAST_CANDIDATE_INNER = `${PANEL} .hoverables.vertical ${LAST_CANDIDATE_INNER}` +const HORIZONTAL_CANDIDATE_INNER = `${PANEL} .hoverables.horizontal .candidate-inner` +const HORIZONTAL_FIRST_CANDIDATE_INNER = `${PANEL} .hoverables.horizontal ${FIRST_CANDIDATE_INNER}` +const HORIZONTAL_LAST_CANDIDATE_INNER = `${PANEL} .hoverables.horizontal ${LAST_CANDIDATE_INNER}` +const PAGING_OUTER = `${PANEL} :is(.prev, .next)` +const PAGING_INNER = `${PANEL} .paging-inner` const HIGHLIGHT_MARK = `${PANEL} .highlighted .mark` const HIGHLIGHT_ORIGINAL_MARK = `${PANEL} .highlighted-original .mark` const PANEL_LIGHT = `${PANEL}.light` -const PANEL_LIGHT_HIGHLIGHT = `${PANEL_LIGHT} .candidate.highlighted .candidate-inner` -const PANEL_LIGHT_HIGHLIGHT_HOVER = `${PANEL_LIGHT} .candidate.highlighted .candidate-inner:hover` -const PANEL_LIGHT_HIGHLIGHT_PRESS = `${PANEL_LIGHT} .candidate.highlighted .candidate-inner:active` -const PANEL_LIGHT_OTHER_HOVER = `${PANEL_LIGHT} .candidate:not(.highlighted) .candidate-inner:hover` -const PANEL_LIGHT_OTHER_PRESS = `${PANEL_LIGHT} .candidate:not(.highlighted) .candidate-inner:active` +const PANEL_LIGHT_HIGHLIGHT = `${PANEL_LIGHT} .hoverable.highlighted .hoverable-inner` +const PANEL_LIGHT_HIGHLIGHT_HOVER = `${PANEL_LIGHT} .hoverable.highlighted:hover .hoverable-inner` +const PANEL_LIGHT_HIGHLIGHT_PRESS = `${PANEL_LIGHT} .hoverable.highlighted:active .hoverable-inner` +const PANEL_LIGHT_OTHER_HOVER = `${PANEL_LIGHT} .hoverable:not(.highlighted):hover .hoverable-inner` +const PANEL_LIGHT_OTHER_PRESS = `${PANEL_LIGHT} .hoverable:not(.highlighted):active .hoverable-inner` const TEXT_LIGHT_HIGHLIGHT = `${PANEL_LIGHT} .candidate.highlighted .text` -const TEXT_LIGHT_PRESS = `${PANEL_LIGHT} .candidate .candidate-inner:active .text` +const TEXT_LIGHT_PRESS = `${PANEL_LIGHT} .candidate:active .candidate-inner .text` const LABEL_LIGHT_HIGHLIGHT = `${PANEL_LIGHT} .candidate.highlighted .label` const TEXT_LIGHT = `${PANEL_LIGHT} .text` const LABEL_LIGHT = `${PANEL_LIGHT} .label` +const PAGING_BUTTON_LIGHT = `${PANEL_LIGHT} .paging .hoverable-inner svg` +const PAGING_BUTTON_DISABLED_LIGHT = `${PANEL_LIGHT} .paging svg` 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 HOVERABLES_LIGHT_BACKGROUND = `${PANEL_LIGHT} .hoverables :is(.candidate, .paging)` +const PANEL_LIGHT_DIVIDER_MIDDLE = `${PANEL_LIGHT} .hoverables .divider .divider-middle` +const PANEL_LIGHT_DIVIDER_SIDE = `${PANEL_LIGHT} .hoverables .divider .divider-side` const CURSOR_NO_TEXT_LIGHT = `${PANEL_LIGHT} .cursor.no-text` const HIGHLIGHT_MARK_LIGHT = `${PANEL_LIGHT} .highlighted .mark` const PANEL_DARK = `${PANEL}.dark` -const PANEL_DARK_HIGHLIGHT = `${PANEL_DARK} .candidate.highlighted .candidate-inner` -const PANEL_DARK_HIGHLIGHT_HOVER = `${PANEL_DARK} .candidate.highlighted .candidate-inner:hover` -const PANEL_DARK_HIGHLIGHT_PRESS = `${PANEL_DARK} .candidate.highlighted .candidate-inner:active` -const PANEL_DARK_OTHER_HOVER = `${PANEL_DARK} .candidate:not(.highlighted) .candidate-inner:hover` -const PANEL_DARK_OTHER_PRESS = `${PANEL_DARK} .candidate:not(.highlighted) .candidate-inner:active` -const TEXT_DARK_HIGHLIGHT = `${PANEL_DARK} .candidate.highlighted .text` -const TEXT_DARK_PRESS = `${PANEL_DARK} .candidate .candidate-inner:active .text` -const LABEL_DARK_HIGHLIGHT = `${PANEL_DARK} .candidate.highlighted .label` -const TEXT_DARK = `${PANEL_DARK} .text` -const LABEL_DARK = `${PANEL_DARK} .label` -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` -const HIGHLIGHT_MARK_DARK = `${PANEL_DARK} .highlighted .mark` +const PANEL_DARK_HIGHLIGHT = lightToDark(PANEL_LIGHT_HIGHLIGHT) +const PANEL_DARK_HIGHLIGHT_HOVER = lightToDark(PANEL_LIGHT_HIGHLIGHT_HOVER) +const PANEL_DARK_HIGHLIGHT_PRESS = lightToDark(PANEL_LIGHT_HIGHLIGHT_PRESS) +const PANEL_DARK_OTHER_HOVER = lightToDark(PANEL_LIGHT_OTHER_HOVER) +const PANEL_DARK_OTHER_PRESS = lightToDark(PANEL_LIGHT_OTHER_PRESS) +const TEXT_DARK_HIGHLIGHT = lightToDark(TEXT_LIGHT_HIGHLIGHT) +const TEXT_DARK_PRESS = lightToDark(TEXT_LIGHT_PRESS) +const LABEL_DARK_HIGHLIGHT = lightToDark(LABEL_LIGHT_HIGHLIGHT) +const TEXT_DARK = lightToDark(TEXT_LIGHT) +const LABEL_DARK = lightToDark(LABEL_LIGHT) +const PAGING_BUTTON_DARK = lightToDark(PAGING_BUTTON_LIGHT) +const PAGING_BUTTON_DISABLED_DARK = lightToDark(PAGING_BUTTON_DISABLED_LIGHT) +const PREEDIT_DARK = lightToDark(PREEDIT_LIGHT) +const HEADER_DARK_BACKGROUND = lightToDark(HEADER_LIGHT_BACKGROUND) +const HOVERABLES_DARK_BACKGROUND = lightToDark(HOVERABLES_LIGHT_BACKGROUND) +const PANEL_DARK_DIVIDER_MIDDLE = lightToDark(PANEL_LIGHT_DIVIDER_MIDDLE) +const PANEL_DARK_DIVIDER_SIDE = lightToDark(PANEL_LIGHT_DIVIDER_SIDE) +const CURSOR_NO_TEXT_DARK = lightToDark(CURSOR_NO_TEXT_LIGHT) +const HIGHLIGHT_MARK_DARK = lightToDark(HIGHLIGHT_MARK_LIGHT) -function px (n: string) { +function px (n: string | number) { return `${n}px` } @@ -130,7 +154,7 @@ export function setStyle (style: string) { const hasBackgroundImage = j.Background.ImageUrl.trim() !== '' const markKey = j.Highlight.MarkStyle === 'Text' ? 'color' : 'background-color' rules[PANEL] = {} - rules[CANDIDATES] = {} + rules[HOVERABLES] = {} rules[TEXT] = {} rules[LABEL] = {} rules[PREEDIT] = {} @@ -138,8 +162,12 @@ export function setStyle (style: string) { rules[HIGHLIGHT_MARK] = {} rules[HIGHLIGHT_ORIGINAL_MARK] = {} rules[CANDIDATE_INNER] = {} + rules[PAGING_OUTER] = {} + rules[PAGING_INNER] = {} if (j.LightMode.OverrideDefault === 'True') { + const lightBackgroundColor = hasBackgroundImage ? 'inherit' : j.LightMode.PanelColor + rules[PANEL_LIGHT_HIGHLIGHT] = { 'background-color': j.LightMode.HighlightColor } @@ -161,9 +189,9 @@ export function setStyle (style: string) { rules[HEADER_LIGHT_BACKGROUND] = { 'background-color': j.LightMode.PanelColor } - rules[CANDIDATE_LIGHT_BACKGROUND] = { + rules[HOVERABLES_LIGHT_BACKGROUND] = { // With background image, discard panel color for unselected candidates - 'background-color': hasBackgroundImage ? 'inherit' : j.LightMode.PanelColor + 'background-color': lightBackgroundColor } rules[TEXT_LIGHT] = { color: j.LightMode.TextColor @@ -171,6 +199,12 @@ export function setStyle (style: string) { rules[LABEL_LIGHT] = { color: j.LightMode.LabelColor } + rules[PAGING_BUTTON_LIGHT] = { + color: j.LightMode.PagingButtonColor + } + rules[PAGING_BUTTON_DISABLED_LIGHT] = { + color: j.LightMode.DisabledPagingButtonColor + } rules[PREEDIT_LIGHT] = { color: j.LightMode.PreeditColor } @@ -180,8 +214,11 @@ export function setStyle (style: string) { rules[PANEL_LIGHT] = { 'border-color': j.LightMode.BorderColor } - rules[PANEL_LIGHT_HORIZONTAL_DIVIDER] = { - 'border-top-color': j.LightMode.HorizontalDividerColor + rules[PANEL_LIGHT_DIVIDER_MIDDLE] = { + 'background-color': j.LightMode.DividerColor + } + rules[PANEL_LIGHT_DIVIDER_SIDE] = { + 'background-color': lightBackgroundColor } rules[HIGHLIGHT_MARK_LIGHT] = { [markKey]: j.LightMode.HighlightMarkColor @@ -198,27 +235,36 @@ export function setStyle (style: string) { if (j.DarkMode.OverrideDefault === 'True') { if (j.DarkMode.SameWithLightMode === 'True' && j.LightMode.OverrideDefault === 'True') { - rules[PANEL_DARK_HIGHLIGHT] = rules[PANEL_LIGHT_HIGHLIGHT] - rules[PANEL_DARK_HIGHLIGHT_HOVER] = rules[PANEL_LIGHT_HIGHLIGHT_HOVER] - rules[PANEL_DARK_HIGHLIGHT_PRESS] = rules[PANEL_LIGHT_HIGHLIGHT_PRESS] - rules[TEXT_DARK_HIGHLIGHT] = rules[TEXT_LIGHT_HIGHLIGHT] - rules[TEXT_DARK_PRESS] = rules[TEXT_LIGHT_PRESS] - rules[LABEL_DARK_HIGHLIGHT] = rules[LABEL_LIGHT_HIGHLIGHT] - rules[HEADER_DARK_BACKGROUND] = rules[HEADER_LIGHT_BACKGROUND] - rules[CANDIDATE_DARK_BACKGROUND] = rules[CANDIDATE_LIGHT_BACKGROUND] - 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] - rules[HIGHLIGHT_MARK_DARK] = rules[HIGHLIGHT_MARK_LIGHT] + const keys = [ + PANEL_LIGHT_HIGHLIGHT, + PANEL_LIGHT_HIGHLIGHT_HOVER, + PANEL_LIGHT_HIGHLIGHT_PRESS, + TEXT_LIGHT_HIGHLIGHT, + TEXT_LIGHT_PRESS, + LABEL_LIGHT_HIGHLIGHT, + HEADER_LIGHT_BACKGROUND, + HOVERABLES_LIGHT_BACKGROUND, + TEXT_LIGHT, + LABEL_LIGHT, + PAGING_BUTTON_LIGHT, + PAGING_BUTTON_DISABLED_LIGHT, + PREEDIT_LIGHT, + CURSOR_NO_TEXT_LIGHT, + PANEL_LIGHT, + PANEL_LIGHT_DIVIDER_MIDDLE, + PANEL_LIGHT_DIVIDER_SIDE, + HIGHLIGHT_MARK_LIGHT + ] if (j.Highlight.HoverBehavior === 'Add') { // This is the behavior of MSPY - rules[PANEL_DARK_OTHER_HOVER] = rules[PANEL_LIGHT_OTHER_HOVER] - rules[PANEL_DARK_OTHER_PRESS] = rules[PANEL_LIGHT_OTHER_PRESS] + keys.push(PANEL_LIGHT_OTHER_HOVER, PANEL_LIGHT_OTHER_PRESS) + } + for (const key of keys) { + rules[lightToDark(key)] = rules[key] } } else { + const darkBackgroundColor = hasBackgroundImage ? 'inherit' : j.DarkMode.PanelColor + rules[PANEL_DARK_HIGHLIGHT] = { 'background-color': j.DarkMode.HighlightColor } @@ -240,8 +286,8 @@ export function setStyle (style: string) { rules[HEADER_DARK_BACKGROUND] = { 'background-color': j.DarkMode.PanelColor } - rules[CANDIDATE_DARK_BACKGROUND] = { - 'background-color': hasBackgroundImage ? 'inherit' : j.DarkMode.PanelColor + rules[HOVERABLES_DARK_BACKGROUND] = { + 'background-color': darkBackgroundColor } rules[TEXT_DARK] = { color: j.DarkMode.TextColor @@ -249,6 +295,12 @@ export function setStyle (style: string) { rules[LABEL_DARK] = { color: j.DarkMode.LabelColor } + rules[PAGING_BUTTON_DARK] = { + color: j.DarkMode.PagingButtonColor + } + rules[PAGING_BUTTON_DISABLED_DARK] = { + color: j.DarkMode.DisabledPagingButtonColor + } rules[PREEDIT_DARK] = { color: j.DarkMode.PreeditColor } @@ -258,8 +310,11 @@ export function setStyle (style: string) { rules[PANEL_DARK] = { 'border-color': j.DarkMode.BorderColor } - rules[PANEL_DARK_HORIZONTAL_DIVIDER] = { - 'border-top-color': j.DarkMode.HorizontalDividerColor + rules[PANEL_DARK_DIVIDER_MIDDLE] = { + 'background-color': j.DarkMode.DividerColor + } + rules[PANEL_DARK_DIVIDER_SIDE] = { + 'background-color': darkBackgroundColor } rules[HIGHLIGHT_MARK_DARK] = { [markKey]: j.DarkMode.HighlightMarkColor @@ -277,8 +332,8 @@ export function setStyle (style: string) { if (j.Background.ImageUrl) { // Background image should not affect aux - rules[CANDIDATES]['background-image'] = `url(${JSON.stringify(j.Background.ImageUrl)})` - rules[CANDIDATES]['background-size'] = 'cover' + rules[HOVERABLES]['background-image'] = `url(${JSON.stringify(j.Background.ImageUrl)})` + rules[HOVERABLES]['background-size'] = 'cover' } if (j.Background.Blur === 'True') { @@ -312,15 +367,49 @@ export function setStyle (style: string) { rules[PANEL]['border-width'] = px(j.Size.BorderWidth) rules[PANEL]['border-radius'] = px(j.Size.BorderRadius) + + const halfMargin = px(Number(j.Size.Margin) / 2) rules[CANDIDATE_INNER].margin = px(j.Size.Margin) - rules[CANDIDATE_INNER]['border-radius'] = px(j.Size.HighlightRadius) + + if (j.Size.HorizontalDividerWidth === '0') { + rules[VERTICAL_CANDIDATE_INNER] = { + 'margin-top': halfMargin, + 'margin-bottom': halfMargin + } + rules[VERTICAL_FIRST_CANDIDATE_INNER] = { + 'margin-top': px(j.Size.Margin) + } + rules[VERTICAL_LAST_CANDIDATE_INNER] = { + 'margin-bottom': px(j.Size.Margin) + } + } + // Unconditional since there is no vertical divider between candidates. + rules[HORIZONTAL_CANDIDATE_INNER] = { + 'margin-left': halfMargin, + 'margin-right': halfMargin + } + rules[HORIZONTAL_FIRST_CANDIDATE_INNER] = { + 'margin-left': px(j.Size.Margin) + } + rules[HORIZONTAL_LAST_CANDIDATE_INNER] = { + 'margin-right': px(j.Size.Margin) + } + + rules[PAGING_OUTER].margin = px(j.Size.Margin) + rules[CANDIDATE_INNER]['border-radius'] = rules[PAGING_INNER]['border-radius'] = px(j.Size.HighlightRadius) rules[CANDIDATE_INNER]['padding-top'] = px(j.Size.TopPadding) rules[CANDIDATE_INNER]['padding-right'] = px(j.Size.RightPadding) rules[CANDIDATE_INNER]['padding-bottom'] = px(j.Size.BottomPadding) rules[CANDIDATE_INNER]['padding-left'] = px(j.Size.LeftPadding) rules[CANDIDATE_INNER].gap = px(j.Size.LabelTextGap) rules[PANEL_HORIZONTAL_DIVIDER] = { - 'border-top-width': px(j.Size.HorizontalDividerWidth) + height: px(j.Size.HorizontalDividerWidth) + } + rules[PANEL_HORIZONTAL_DIVIDER_SIDE] = { + width: px(j.Size.Margin) + } + rules[PANEL_VERTICAL_DIVIDER_SIDE] = { + height: px(j.Size.Margin) } const basic = document.head.querySelector('#basic') diff --git a/page/generic.scss b/page/generic.scss index 0dac11a..2729de8 100644 --- a/page/generic.scss +++ b/page/generic.scss @@ -23,15 +23,27 @@ body { } } -.candidates { +.hoverables { display: flex; + .divider { + display: flex; + } + &.vertical { flex-direction: column; + + .divider { + flex-direction: row; + } } &.horizontal { flex-direction: row; + + .divider { + flex-direction: column; + } } } @@ -43,6 +55,28 @@ body { position: relative; /* for absolute position of mark */ } +.paging { + display: flex; +} + +/* When horizontal, paging is shorter than candidates, so need to centralize them. */ +.prev, .next { + display: flex; + align-items: center; +} + +.paging-inner { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + + svg { + height: 16px; + } +} + .mark { opacity: 0; diff --git a/page/global.d.ts b/page/global.d.ts index 166dbcf..16b0ad1 100644 --- a/page/global.d.ts +++ b/page/global.d.ts @@ -7,7 +7,7 @@ declare global { _resize: (dx: number, dy: number, shadowTop: number, shadowRight: number, shadowBottom: number, shadowLeft: number, fullWidth: number, fullHeight: number, dragging: boolean) => void // JavaScript APIs that webview_candidate_window.mm calls - setCandidates: (cands: string[], labels: string[], highlighted: number, markText: string) => void + setCandidates: (cands: string[], labels: string[], highlighted: number, markText: string, pageable: boolean, hasPrev: boolean, hasNext: boolean) => void setLayout: (layout: 0 | 1) => void updateInputPanel: (preeditHTML: string, auxUpHTML: string, auxDownHTML: string) => void resize: (dx: number, dy: number, dragging: boolean) => void diff --git a/page/index.html b/page/index.html index 7ccafac..77ce20a 100644 --- a/page/index.html +++ b/page/index.html @@ -21,7 +21,7 @@ -
+
diff --git a/page/macos.scss b/page/macos.scss index 97a7f3b..b0650a2 100644 --- a/page/macos.scss +++ b/page/macos.scss @@ -57,9 +57,28 @@ $dark-graphite: rgb(105 105 105); background-clip: padding-box; } - .candidates.vertical .candidate:not(:first-child) { - border-top-width: 1px; - border-top-style: solid; + /* Use a dedicated div because + * 1. divider color is not overlaid by panel color + * 2. divider may not be full-length + */ + .hoverables.vertical .divider { + height: 1px; + + .divider-middle { + width: 100%; + } + } + + .hoverables.horizontal .divider { + width: 0; + + &.divider-paging { + width: 1px; + } + + .divider-middle { + height: 100%; + } } } @@ -78,44 +97,59 @@ $dark-graphite: rgb(105 105 105); background-color: $panel-color-light; } + .paging { + color: gray; // disabled + background-color: $panel-color-light; + + .hoverable-inner { + color: $text-color-light; // enabled + } + } + .candidate.highlighted { color: white; } - &.blue .candidate.highlighted .candidate-inner { + &.blue .hoverable.highlighted .hoverable-inner { background-color: $light-blue; } - &.purple .candidate.highlighted .candidate-inner { + &.purple .hoverable.highlighted .hoverable-inner { background-color: $light-purple; } - &.pink .candidate.highlighted .candidate-inner { + &.pink .hoverable.highlighted .hoverable-inner { background-color: $light-pink; } - &.red .candidate.highlighted .candidate-inner { + &.red .hoverable.highlighted .hoverable-inner { background-color: $light-red; } - &.orange .candidate.highlighted .candidate-inner { + &.orange .hoverable.highlighted .hoverable-inner { background-color: $light-orange; } - &.yellow .candidate.highlighted .candidate-inner { + &.yellow .hoverable.highlighted .hoverable-inner { background-color: $light-yellow; } - &.green .candidate.highlighted .candidate-inner { + &.green .hoverable.highlighted .hoverable-inner { background-color: $light-green; } - &.graphite .candidate.highlighted .candidate-inner { + &.graphite .hoverable.highlighted .hoverable-inner { background-color: $light-graphite; } - .candidates.vertical .candidate:not(:first-child) { - border-top-color: $vertical-border-color-light; + .hoverables .divider { + .divider-side { + background-color: $panel-color-light; + } + + .divider-middle { + background-color: $vertical-border-color-light; + } } } @@ -134,44 +168,60 @@ $dark-graphite: rgb(105 105 105); background-color: $panel-color-dark; } + .paging { + color: gray; // disabled + background-color: $panel-color-dark; + + /* stylelint-disable-next-line no-descending-specificity */ + .hoverable-inner { + color: $text-color-dark; // enabled + } + } + /* stylelint-disable-next-line no-descending-specificity */ .candidate.highlighted { color: white; } - &.blue .candidate.highlighted .candidate-inner { + &.blue .hoverable.highlighted .hoverable-inner { background-color: $dark-blue; } - &.purple .candidate.highlighted .candidate-inner { + &.purple .hoverable.highlighted .hoverable-inner { background-color: $dark-purple; } - &.pink .candidate.highlighted .candidate-inner { + &.pink .hoverable.highlighted .hoverable-inner { background-color: $dark-pink; } - &.red .candidate.highlighted .candidate-inner { + &.red .hoverable.highlighted .hoverable-inner { background-color: $dark-red; } - &.orange .candidate.highlighted .candidate-inner { + &.orange .hoverable.highlighted .hoverable-inner { background-color: $dark-orange; } - &.yellow .candidate.highlighted .candidate-inner { + &.yellow .hoverable.highlighted .hoverable-inner { background-color: $dark-yellow; } - &.green .candidate.highlighted .candidate-inner { + &.green .hoverable.highlighted .hoverable-inner { background-color: $dark-green; } - &.graphite .candidate.highlighted .candidate-inner { + &.graphite .hoverable.highlighted .hoverable-inner { background-color: $dark-graphite; } - .candidates.vertical .candidate:not(:first-child) { - border-top-color: $vertical-border-color-dark; + .hoverables .divider { + .divider-side { + background-color: $panel-color-dark; + } + + .divider-middle { + background-color: $vertical-border-color-dark; + } } } diff --git a/page/selector.ts b/page/selector.ts index 2abfcaa..d6de396 100644 --- a/page/selector.ts +++ b/page/selector.ts @@ -1,5 +1,5 @@ export const panel = document.querySelector('.panel')! -export const candidates = panel.querySelector('.candidates')! +export const hoverables = panel.querySelector('.hoverables')! export const preedit = document.querySelector('.preedit')! export const auxUp = document.querySelector('.aux-up')! export const auxDown = document.querySelector('.aux-down')! diff --git a/page/ux.ts b/page/ux.ts index 135b497..8f15348 100644 --- a/page/ux.ts +++ b/page/ux.ts @@ -1,6 +1,6 @@ import { panel, - candidates + hoverables } from './selector' let pressed = false @@ -87,14 +87,20 @@ document.addEventListener('mouseup', e => { return } let target = e.target as Element - if (target === candidates || !candidates.contains(target)) { + if (target === hoverables || !hoverables.contains(target)) { return } - while (target.parentElement !== candidates) { + while (target.parentElement !== hoverables) { + if (target.classList.contains('prev')) { + return window._page(false) + } else if (target.classList.contains('next')) { + return window._page(true) + } target = target.parentElement! } - for (let i = 0; i < candidates.childElementCount; ++i) { - if (candidates.children[i] === target) { + const allCandidates = hoverables.querySelectorAll('.candidate') + for (let i = 0; i < allCandidates.length; ++i) { + if (allCandidates[i] === target) { return window._select(i) } } diff --git a/preview/preview.mm b/preview/preview.mm index 279863f..22f76b2 100644 --- a/preview/preview.mm +++ b/preview/preview.mm @@ -16,9 +16,13 @@ int main(int argc, const char *argv[]) { }); candidateWindow->set_init_callback( []() { std::cout << "Window loaded" << std::endl; }); + candidateWindow->set_page_callback([](bool next) { + std::cout << (next ? "next" : "prev") << " page" << std::endl; + }); auto t = std::thread([&] { std::this_thread::sleep_for(std::chrono::seconds(1)); candidateWindow->set_layout(candidate_window::layout_t::vertical); + candidateWindow->set_paging_buttons(true, false, true); candidateWindow->set_candidates({"

防注入

", "候选词"}, {"1", "2"}, 0); candidateWindow->set_theme(candidate_window::theme_t::light); diff --git a/src/webview_candidate_window.mm b/src/webview_candidate_window.mm index 2f59689..b01c570 100644 --- a/src/webview_candidate_window.mm +++ b/src/webview_candidate_window.mm @@ -201,7 +201,8 @@ NSRect getNearestScreenFrame(double x, double y) { std::transform(labels.begin(), labels.end(), std::back_inserter(escaped_labels), escape_html); invoke_js("setCandidates", escaped_candidates, escaped_labels, highlighted, - escape_html(highlight_mark_text_)); + escape_html(highlight_mark_text_), pageable_, has_prev_, + has_next_); } void WebviewCandidateWindow::set_theme(theme_t theme) { diff --git a/tests/test-generic.spec.ts b/tests/test-generic.spec.ts index ad0b5a8..6f2926e 100644 --- a/tests/test-generic.spec.ts +++ b/tests/test-generic.spec.ts @@ -33,16 +33,21 @@ test('HTML structure', async ({ page }) => { -
-
-
+
+
+
1
页面结构
-
-
+
+
+
+
+
+
+
2
测试
diff --git a/tests/util.ts b/tests/util.ts index 02a5da0..fbf05ff 100644 --- a/tests/util.ts +++ b/tests/util.ts @@ -25,7 +25,7 @@ export async function init (page: Page) { export function setCandidates (page: Page, cands: string[], labels: string[], highlighted: number) { return page.evaluate(({ cands, labels, highlighted }) => - window.setCandidates(cands, labels, highlighted, ''), { cands, labels, highlighted }) + window.setCandidates(cands, labels, highlighted, '', false, false, false), { cands, labels, highlighted }) } export function setLayout (page: Page, layout: 0 | 1) {