@@ -7,21 +7,30 @@ let y = 0
77let preedit = ''
88let preeditIndex = 0
99
10+ // compared with macOS pinyin
11+ const UNDERLINE_OFFSET = 1
12+ const CANDIDATE_WINDOW_OFFSET = 6
13+
1014const textEncoder = new TextEncoder ( )
1115
16+ function getFontSize ( element : HTMLElement ) {
17+ return Number . parseFloat ( getComputedStyle ( element ) . fontSize )
18+ }
19+
1220export function placePanel ( dx : number , dy : number , anchorTop : number , anchorLeft : number , dragging : boolean ) {
1321 const input = getInputElement ( )
1422 if ( ! input ) {
1523 return
1624 }
1725 const rect = input . getBoundingClientRect ( )
1826 const { top, left, height } = getCaretCoordinates ( input , input . selectionStart ! - ( window . fcitx . followCursor ? 0 : preeditIndex ) )
19- const h = height /* NaN if no line-height is set */ || Number ( getComputedStyle ( input ) . fontSize . slice ( 0 , - 'px' . length ) )
27+ const h = height /* NaN if no line-height is set */ || getFontSize ( input ) + CANDIDATE_WINDOW_OFFSET
2028 const panel = < HTMLElement > document . querySelector ( '#fcitx-theme' )
2129 const frame = panel . getBoundingClientRect ( )
2230 panel . style . opacity = '1'
2331 panel . style . position = 'absolute'
2432 panel . style . height = '0' // let mouse event pass through
33+ panel . style . zIndex = '2147483647' // absolutely above preedit underline
2534 if ( dragging ) {
2635 x += dx
2736 y += dy
@@ -40,6 +49,59 @@ export function hidePanel() {
4049 panel . style . opacity = '0'
4150}
4251
52+ function clearPreeditUnderline ( ) {
53+ document . querySelectorAll ( '.fcitx-preedit-underline' ) . forEach ( div => div . remove ( ) )
54+ }
55+
56+ function drawUnderline ( top : number , left : number , width : number , color : string ) {
57+ const div = document . createElement ( 'div' )
58+ div . className = 'fcitx-preedit-underline'
59+ div . style . position = 'absolute'
60+ div . style . top = `${ top } px`
61+ div . style . left = `${ left } px`
62+ div . style . height = '1px'
63+ div . style . width = `${ width } px`
64+ div . style . backgroundColor = color
65+ document . body . appendChild ( div )
66+ }
67+
68+ function getTextWidth ( input : HTMLElement , text : string ) {
69+ const div = document . createElement ( 'div' )
70+ const style = getComputedStyle ( input )
71+ div . style . position = 'absolute'
72+ div . style . opacity = '0'
73+ div . style . font = style . font
74+ div . textContent = text
75+ document . body . append ( div )
76+ const { width } = div . getBoundingClientRect ( )
77+ div . remove ( )
78+ return width
79+ }
80+
81+ function drawPreeditUnderline ( input : HTMLElement , start : number ) {
82+ const box = input . getBoundingClientRect ( )
83+ const color = getComputedStyle ( input ) . color
84+ const fontSize = getFontSize ( input )
85+ const { top : startTop , left : startLeft } = getCaretCoordinates ( input , start )
86+ const { top : endTop , left : endLeft } = getCaretCoordinates ( input , start + preedit . length )
87+ let lastLeft = startLeft
88+ for ( let i = 1 , rowLeft = startLeft , rowTop = startTop ; i <= preedit . length && rowTop < endTop ; ++ i ) {
89+ const { top, left } = getCaretCoordinates ( input , start + i )
90+ if ( top !== rowTop ) {
91+ // getCaretCoordinates can't tell the position of the end of previous line,
92+ // because it's equivalent to the start of next line, which is the actual place
93+ // that new character is written. So we need to calculate width of the last character.
94+ drawUnderline ( box . top + rowTop + fontSize + UNDERLINE_OFFSET , box . left + rowLeft , lastLeft - rowLeft + getTextWidth ( input , preedit [ i - 1 ] ) , color )
95+ rowTop = top
96+ rowLeft = lastLeft
97+ }
98+ lastLeft = left
99+ }
100+ if ( lastLeft !== endLeft ) {
101+ drawUnderline ( box . top + endTop + fontSize + UNDERLINE_OFFSET , box . left + lastLeft , endLeft - lastLeft , color )
102+ }
103+ }
104+
43105function changeInput ( commitText : string , preeditText : string , index : number ) {
44106/*
45107____ pre|edit ____
@@ -64,15 +126,20 @@ ____ commit pre|edit ____
64126
65127 const start = input . selectionStart ! - preeditIndex
66128 const end = preedit ? start + preedit . length : input . selectionEnd !
129+ const newStart = start + commitText . length
67130 input . value = input . value . slice ( 0 , start ) + commitText + preeditText + input . value . slice ( end )
68131 // This may be triggered by user clicking panel. Focus to ensure setting selectionEnd works.
69132 input . focus ( )
70- input . selectionStart = input . selectionEnd = start + commitText . length + i
133+ input . selectionStart = input . selectionEnd = newStart + i
71134 // For vue-based input, this is needed to synchronize state.
72135 input . dispatchEvent ( new Event ( 'change' ) )
73136 preedit = preeditText
74137 preeditIndex = i
75138 setSpellCheck ( ! preedit )
139+ clearPreeditUnderline ( )
140+ if ( preedit ) {
141+ drawPreeditUnderline ( input , newStart )
142+ }
76143}
77144
78145export function setPreedit ( text : string , index : number ) {
@@ -86,4 +153,5 @@ export function commit(text: string) {
86153export function resetPreedit ( ) {
87154 preedit = ''
88155 preeditIndex = 0
156+ clearPreeditUnderline ( )
89157}
0 commit comments