-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathime.lua
More file actions
396 lines (354 loc) · 9.29 KB
/
ime.lua
File metadata and controls
396 lines (354 loc) · 9.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
-- To run this sample:
-- bin/soluna.exe entry=test/ime.lua
local soluna = require "soluna"
local app = require "soluna.app"
local mattext = require "soluna.material.text"
local matquad = require "soluna.material.quad"
local matmask = require "soluna.material.mask"
local font = require "soluna.font"
local file = require "soluna.file"
local utf8 = utf8
local math = math
local string = string
local table = table
local args = ...
local batch = assert(args.batch)
local KEY_LEFT <const> = 263
local KEY_RIGHT <const> = 262
local KEY_HOME <const> = 268
local KEY_END <const> = 269
local KEY_BACKSPACE <const> = 259
local KEY_DEL <const> = 261
local KEY_ENTER <const> = 257
local KEYSTATE_PRESS <const> = 1
local CHAR_BACKSPACE <const> = 8
local CHAR_DELETE <const> = 127
local FONT_SIZE <const> = 32
local HELP_SIZE <const> = 18
local BOX_WIDTH <const> = 720
local BOX_HEIGHT <const> = 84
local BOX_PADDING_X <const> = 18
local BOX_PADDING_Y <const> = 8
local BOX_RADIUS <const> = 10
local CURSOR_BLINK <const> = 30
local function load_font()
if soluna.platform == "wasm" then
local bundled_name = "Source Han Sans SC Regular"
local bundled_path = "asset/font/SourceHanSansSC-Regular.ttf"
local bundled_data = file.load(bundled_path)
if bundled_data then
font.import(bundled_data)
local bundled_id = font.name(bundled_name)
if bundled_id then
return bundled_id, bundled_name
end
end
end
local sysfont = require "soluna.font.system"
local candidates = {
"WenQuanYi Micro Hei", -- Linux
"Microsoft YaHei", -- Windows
"Yuanti SC", -- macOS
"Source Han Sans SC Regular", -- WASM
}
for _, name in ipairs(candidates) do
local ok, data = pcall(sysfont.ttfdata, name)
if ok and data then
font.import(data)
local fontid = font.name(name)
if fontid then
return fontid, name
end
end
end
error "No available system font for IME sample"
end
local function cache(f)
return setmetatable({}, {
__index = function(self, k)
local v = f(k)
self[k] = v
return v
end
})
end
local quad_cache = cache(function(key)
local w, h, c = key:match "^(%-?%d+):(%-?%d+):(%x+)$"
return matquad.quad(tonumber(w), tonumber(h), tonumber(c, 16))
end)
local function cached_quad(w, h, c)
local key = string.format("%d:%d:%08x", w, h, c)
return quad_cache[key]
end
local mask_cache = cache(function(key)
local sprite, color = key:match "^(%d+):(%x+)$"
return matmask.mask(tonumber(sprite), tonumber(color, 16))
end)
local function cached_mask(sprite, color)
local key = string.format("%d:%08x", sprite, color)
return mask_cache[key]
end
local function clamp(v, lo, hi)
if v < lo then
return lo
elseif v > hi then
return hi
end
return v
end
local function rounded_box_rgba(w, h, radius)
local r = math.floor(clamp(radius, 0, math.min(w, h) * 0.5))
local edge = r * r
local left = r
local right = w - r
local top = r
local bottom = h - r
local opaque = "\255\255\255\255"
local transparent = "\255\255\255\0"
local lines = {}
for y = 0, h - 1 do
local py = y + 0.5
local row = {}
for x = 0, w - 1 do
local px = x + 0.5
local qx = clamp(px, left, right)
local qy = clamp(py, top, bottom)
local dx = px - qx
local dy = py - qy
row[x + 1] = (dx * dx + dy * dy <= edge) and opaque or transparent
end
lines[y + 1] = table.concat(row)
end
return table.concat(lines)
end
local rounded_box_cache = {}
local function rounded_box_sprite(w, h, radius)
local key = string.format("%d:%d:%d", w, h, radius)
local sprite = rounded_box_cache[key]
if sprite then
return sprite
end
local filename = "@" .. "ime_round_" .. key:gsub(":", "_")
soluna.preload {
filename = filename,
content = rounded_box_rgba(w, h, radius),
w = w,
h = h,
}
local sprites = soluna.load_sprites {
{
name = "box",
filename = filename,
cw = w,
ch = h,
x = 0,
y = 0,
}
}
rounded_box_cache[key] = sprites.box
return sprites.box
end
local fontid, font_name = load_font()
local fontcobj = font.cobj()
local text_block, text_cursor = mattext.block(fontcobj, fontid, FONT_SIZE, 0x000000, "LV")
local help_block = mattext.block(fontcobj, fontid, HELP_SIZE, 0x222222, "LV")
soluna.set_window_title "soluna ime sample"
app.set_ime_font(font_name, FONT_SIZE)
local state = {
screen_w = args.width,
screen_h = args.height,
mouse_x = 0,
mouse_y = 0,
focused = true,
caret_tick = 0,
text = "",
cursor = 0,
suppress_control_char = nil,
}
local function char_count(s)
return utf8.len(s) or 0
end
local function clamp_cursor()
local n = char_count(state.text)
if state.cursor < 0 then
state.cursor = 0
elseif state.cursor > n then
state.cursor = n
end
end
local function byte_offset_for_char(index_1based)
return utf8.offset(state.text, index_1based) or (#state.text + 1)
end
local function insert_text(s)
if not s or s == "" then
return
end
local byte = byte_offset_for_char(state.cursor + 1)
state.text = state.text:sub(1, byte - 1) .. s .. state.text:sub(byte)
state.cursor = state.cursor + char_count(s)
end
local function delete_backward()
if state.cursor <= 0 then
return
end
local from = byte_offset_for_char(state.cursor)
local to = byte_offset_for_char(state.cursor + 1)
state.text = state.text:sub(1, from - 1) .. state.text:sub(to)
state.cursor = state.cursor - 1
end
local function delete_forward()
local n = char_count(state.text)
if state.cursor >= n then
return
end
local from = byte_offset_for_char(state.cursor + 1)
local to = byte_offset_for_char(state.cursor + 2)
state.text = state.text:sub(1, from - 1) .. state.text:sub(to)
end
local function is_control_char(codepoint)
return codepoint < 32 or (codepoint >= 127 and codepoint <= 159)
end
local function handle_control_delete(codepoint)
if codepoint ~= CHAR_BACKSPACE and codepoint ~= CHAR_DELETE then
return
end
if state.suppress_control_char == codepoint then
state.suppress_control_char = nil
return
end
state.suppress_control_char = nil
if codepoint == CHAR_BACKSPACE then
delete_backward()
else
delete_forward()
end
end
local function decode_char_event(value)
local t = type(value)
if t == "number" then
if is_control_char(value) then
return nil, value
end
return utf8.char(value), nil
end
if t ~= "string" or value == "" then
return nil, nil
end
local first = utf8.codepoint(value, 1, 1)
if first and is_control_char(first) then
return nil, first
end
return value, nil
end
local function box_rect()
local w = math.min(BOX_WIDTH, math.max(320, state.screen_w - 48))
local h = BOX_HEIGHT
local x = (state.screen_w - w) // 2
local y = (state.screen_h - h) // 2
return x, y, w, h
end
local function in_box(x, y, bx, by, bw, bh)
return x >= bx and x <= bx + bw and y >= by and y <= by + bh
end
local callback = {}
function callback.window_resize(w, h)
state.screen_w = w
state.screen_h = h
end
function callback.mouse_move(x, y)
state.mouse_x = x
state.mouse_y = y
end
function callback.mouse_button(button, key_state)
if button ~= 0 or key_state ~= KEYSTATE_PRESS then
return
end
local bx, by, bw, bh = box_rect()
state.focused = in_box(state.mouse_x, state.mouse_y, bx, by, bw, bh)
if not state.focused then
app.set_ime_rect(nil)
end
end
function callback.char(value)
if not state.focused then
return
end
local text_input, control = decode_char_event(value)
if control then
handle_control_delete(control)
return
end
if not text_input then
return
end
insert_text(text_input)
clamp_cursor()
state.caret_tick = 0
end
function callback.key(keycode, key_state)
if key_state ~= KEYSTATE_PRESS or not state.focused then
return
end
if keycode == KEY_LEFT then
state.cursor = state.cursor - 1
elseif keycode == KEY_RIGHT then
state.cursor = state.cursor + 1
elseif keycode == KEY_HOME then
state.cursor = 0
elseif keycode == KEY_END then
state.cursor = char_count(state.text)
elseif keycode == KEY_BACKSPACE then
delete_backward()
state.suppress_control_char = CHAR_BACKSPACE
elseif keycode == KEY_DEL then
delete_forward()
state.suppress_control_char = CHAR_DELETE
elseif keycode == KEY_ENTER then
insert_text "\n"
end
clamp_cursor()
state.caret_tick = 0
end
function callback.frame()
clamp_cursor()
local bx, by, bw, bh = box_rect()
local box_sprite = rounded_box_sprite(bw, bh, BOX_RADIUS)
local tx = bx + BOX_PADDING_X
local ty = by + BOX_PADDING_Y
local tw = bw - BOX_PADDING_X * 2
local th = bh - BOX_PADDING_Y * 2
batch:add(cached_quad(state.screen_w, state.screen_h, 0xf2f2f2ff), 0, 0)
batch:add(cached_mask(box_sprite, state.focused and 0xffffffff or 0xe8e8e8ff), bx, by)
batch:add(
cached_quad(
math.max(bw - BOX_RADIUS * 2, 2),
2,
state.focused and 0x1d6ef0ff or 0x9a9a9aff
),
bx + BOX_RADIUS,
by + bh - 2
)
local label = text_block(state.text, tw, th)
batch:add(label, tx, ty)
local cx, cy, cw, ch, n, descent = text_cursor(state.text, state.cursor, tw, th)
state.cursor = n
descent = descent or 0
if state.focused then
app.set_ime_rect {
x = tx + cx,
y = ty + cy - descent,
width = cw,
height = ch,
text_color = 0xff000000,
}
else
app.set_ime_rect(nil)
end
state.caret_tick = (state.caret_tick + 1) % (CURSOR_BLINK * 2)
if state.focused and state.caret_tick < CURSOR_BLINK then
batch:add(cached_quad(math.max(cw, 2), ch, 0x111111ff), tx + cx, ty + cy)
end
local help = help_block("Click box, type with CJK.", state.screen_w - 32, 24)
batch:add(help, 16, 16)
end
return callback