Skip to content

Commit 0dea579

Browse files
committed
IME in wasm. Closes #521
1 parent 819b6b0 commit 0dea579

File tree

6 files changed

+96
-31
lines changed

6 files changed

+96
-31
lines changed

nimx/formatted_text.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ proc getByteOffsetsForRunePositions(t: FormattedText, positions: openarray[int],
377377
r = positions[i]
378378

379379
proc uniDelete*(t: FormattedText, start, stop: int) =
380+
if stop < start: return
380381
t.cacheValid = false
381382

382383
var sa = t.attrIndexForRuneAtPos(start)

nimx/private/windows/web_window.nim

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import nimx/private/js_vk_map
1010
type WebWindow* = ref object of Window
1111
renderingContext: GraphicsContext
1212
canvasId: string
13+
textInputCompositionInProgress: bool
1314

1415
proc fullScreenAvailableAux(): int {.importwasmraw: """
1516
var d = document;
@@ -304,19 +305,33 @@ method onResize*(w: WebWindow, newSize: Size) =
304305
let vp = w.frame.size * p
305306
sharedGL().viewport(0, 0, GLSizei(vp.width), GLsizei(vp.height))
306307

307-
proc startTextInputAux(cb: proc(a: JSRef) {.cdecl.}) {.importwasmraw: """
308-
if (window.__nimx_textinput === undefined) {
309-
var i = window.__nimx_textinput = document.createElement('input');
310-
i.type = 'text';
311-
i.style.position = 'absolute';
312-
i.style.top = '-99999px';
313-
document.body.appendChild(i)
314-
}
315-
window.__nimx_textinput.oninput = () => {
316-
_nime._dvi($0, _nimok(window.__nimx_textinput.value));
317-
window.__nimx_textinput.value = ""
318-
};
319-
setTimeout(() => window.__nimx_textinput.focus(), 1)
308+
proc startTextInputAux(cb: proc(e: int32, a: JSRef) {.cdecl.}, x, y, w, h: float32, canvasId: cstring) {.importwasmraw: """
309+
if (window.__nimx_textinput) window.__nimx_textinput.remove();
310+
var d = document, i = window.__nimx_textinput = d.createElement('input'),
311+
c = d.getElementById(_nimsj($5)),
312+
l = (n, k) => i.addEventListener(n, e => _nime._dvii($0, k, _nimok(e.data)));
313+
314+
i.type = 'text';
315+
316+
Object.assign(i.style, {
317+
position: 'absolute',
318+
left: (c.offsetLeft + $1) + 'px',
319+
top: (c.offsetTop + $2) + 'px',
320+
width: $3 + 'px',
321+
height: $4 + 'px',
322+
opacity: '0', // Fully transparent
323+
pointerEvents: 'none', // Not interactive
324+
zIndex: 10 // Above canvas
325+
});
326+
327+
d.body.appendChild(i);
328+
329+
l("input", 0);
330+
l("compositionupdate", 1);
331+
l("compositionstart", 2);
332+
l("compositionend", 3);
333+
334+
setTimeout(() => i.focus(), 1)
320335
""".}
321336

322337
proc length(j: JSObj): int {.importwasmp.}
@@ -330,29 +345,45 @@ proc jsStringToStr(v: JSObj): string =
330345
let actualSz = strWriteOut(v, addr result[0], sz)
331346
result.setLen(actualSz)
332347

333-
proc onInput(a: JSRef) {.cdecl.} =
334-
var e = newEvent(etTextInput)
335-
e.text = block:
348+
proc onInput(eventKind: int32, text: JSRef) {.cdecl.} =
349+
let text = block:
336350
# Force JSRef destruction early
337-
jsStringToStr(JSObj(o: a))
338-
351+
jsStringToStr(JSObj(o: text))
339352
let a = mainApplication()
340-
e.window = a.keyWindow
341-
if not e.window.isNil:
342-
discard a.handleEvent(e)
353+
let w = WebWindow(a.keyWindow)
354+
var e = newEvent(etTextInput)
355+
e.text = text
356+
e.window = w
357+
358+
if not w.isNil:
359+
case eventKind
360+
of 0, 1: # 0 = input, 1 = compositionupdate
361+
if eventKind == 0:
362+
if not w.textInputCompositionInProgress:
363+
discard a.handleEvent(e)
364+
else:
365+
e.kind = etTextEditing
366+
discard a.handleEvent(e)
367+
368+
of 2: # compositionstart
369+
w.textInputCompositionInProgress = true
370+
else: # compositionend
371+
w.textInputCompositionInProgress = false
372+
discard a.handleEvent(e)
343373

344374
method startTextInput*(w: WebWindow, r: Rect) =
345-
defineDyncall("vi")
346-
startTextInputAux(onInput)
375+
defineDyncall("vii")
376+
startTextInputAux(onInput, r.x, r.y, r.width, r.height, w.canvasId)
347377

348378
proc stopTextInputAux() {.importwasmraw: """
349-
if (window.__nimx_textinput !== undefined) {
350-
window.__nimx_textinput.oninput = null;
351-
window.__nimx_textinput.blur()
379+
if (window.__nimx_textinput) {
380+
window.__nimx_textinput.remove();
381+
window.__nimx_textinput = null
352382
}
353383
""".}
354384

355385
method stopTextInput*(w: WebWindow) =
386+
w.textInputCompositionInProgress = false
356387
stopTextInputAux()
357388

358389
# window.onload = () => _nime._dv(p)

nimx/text_field.nim

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type
1818
selectionStartLine: int
1919
selectionEndLine: int
2020
textSelection: Slice[int]
21+
editingRange: Slice[int] # For IME input
2122
multiline*: bool
2223
hasBezel*: bool
2324

@@ -113,6 +114,7 @@ method init*(t: TextField) =
113114
t.editable = true
114115
t.selectable = true
115116
t.textSelection = -1 .. -1
117+
t.editingRange = -1 .. -1
116118
t.backgroundColor = whiteColor()
117119
t.hasBezel = true
118120
t.mText = newFormattedText()
@@ -351,7 +353,7 @@ proc clearSelection(t: TextField) =
351353
proc insertText(t: TextField, s: string) =
352354
#if t.mText.isNil: t.mText.text = ""
353355

354-
let th = t.mText.totalHeight
356+
# let th = t.mText.totalHeight
355357
if t.textSelection.len > 0:
356358
t.clearSelection()
357359

@@ -499,7 +501,34 @@ method onKeyDown*(t: TextField, e: var Event): bool =
499501
method onTextInput*(t: TextField, s: string): bool =
500502
if not t.editable: return false
501503
result = true
502-
t.insertText(s)
504+
if t.editingRange.a >= 0:
505+
# We're im IME mode. Exit it
506+
t.mText.uniDelete(t.editingRange.a, t.editingRange.b)
507+
t.mText.uniInsert(t.editingRange.a, s)
508+
cursorPos = t.editingRange.a + s.runeLen
509+
t.editingRange.a = -1
510+
t.updateCursorOffset()
511+
t.bumpCursorVisibility()
512+
t.sendAction()
513+
else:
514+
t.insertText(s)
515+
516+
method onTextEditing*(t: TextField, s: string): bool =
517+
if not t.editable: return false
518+
result = true
519+
520+
if t.textSelection.len > 0:
521+
t.clearSelection()
522+
523+
if t.editingRange.a >= 0:
524+
t.mText.uniDelete(t.editingRange.a, t.editingRange.b)
525+
else:
526+
t.editingRange.a = cursorPos
527+
t.mText.uniInsert(t.editingRange.a, s)
528+
t.editingRange.b = t.editingRange.a + s.runeLen - 1
529+
cursorPos = t.editingRange.b + 1
530+
t.updateCursorOffset()
531+
t.bumpCursorVisibility()
503532

504533
method viewShouldResignFirstResponder*(v: TextField, newFirstResponder: View): bool =
505534
result = true

nimx/unistring.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,8 @@ when isMainModule:
6060
testDelete("bye", 1, 2, "b")
6161
testDelete("asdf", 1, 1, "adf")
6262
testDelete("абвЙ", 1, 2, "аЙ")
63+
testDelete("абвЙ", 1, 0, "абвЙ")
64+
testDelete("абвЙ", 3, 1, "абвЙ")
65+
testDelete("абвЙ", 0, -1, "абвЙ")
6366

6467

nimx/view_event_handling.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export event
55
method onKeyDown*(v: View, e: var Event): bool {.base, gcsafe.} = discard
66
method onKeyUp*(v: View, e: var Event): bool {.base, gcsafe.} = discard
77
method onTextInput*(v: View, s: string): bool {.base, gcsafe.} = discard
8+
method onTextEditing*(v: View, s: string): bool {.base, gcsafe.} = discard
89
method onGestEvent*(d: GestureDetector, e: var Event): bool {.base, gcsafe.} = discard
910
method onScroll*(v: View, e: var Event): bool {.base, gcsafe.} = discard
1011

@@ -207,5 +208,7 @@ proc processKeyboardEvent*(v: View, e: var Event): bool =
207208
result = v.onKeyUp(e)
208209
of etTextInput:
209210
result = v.onTextInput(e.text)
211+
of etTextEditing:
212+
result = v.onTextEditing(e.text)
210213
else:
211214
discard

nimx/window_event_handling.nim

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ method handleEvent*(w: Window, e: var Event): bool {.base, gcsafe.} =
6969
currentDragSystem().processDragEvent(e)
7070
w.handleMouseOverEvent(e)
7171
result = w.processTouchEvent(e)
72-
of etKeyboard:
73-
result = w.propagateEventThroughResponderChain(e)
74-
of etTextInput:
72+
of etKeyboard, etTextInput, etTextEditing:
7573
result = w.propagateEventThroughResponderChain(e)
7674
of etWindowResized:
7775
result = true

0 commit comments

Comments
 (0)