kumiki 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +256 -0
- data/lib/kumiki/animation/animated_state.rb +83 -0
- data/lib/kumiki/animation/easing.rb +62 -0
- data/lib/kumiki/animation/value_tween.rb +69 -0
- data/lib/kumiki/app.rb +381 -0
- data/lib/kumiki/box.rb +40 -0
- data/lib/kumiki/chart/area_chart.rb +308 -0
- data/lib/kumiki/chart/bar_chart.rb +291 -0
- data/lib/kumiki/chart/base_chart.rb +213 -0
- data/lib/kumiki/chart/chart_helpers.rb +74 -0
- data/lib/kumiki/chart/gauge_chart.rb +174 -0
- data/lib/kumiki/chart/heatmap_chart.rb +223 -0
- data/lib/kumiki/chart/line_chart.rb +292 -0
- data/lib/kumiki/chart/pie_chart.rb +222 -0
- data/lib/kumiki/chart/scales.rb +79 -0
- data/lib/kumiki/chart/scatter_chart.rb +306 -0
- data/lib/kumiki/chart/stacked_bar_chart.rb +279 -0
- data/lib/kumiki/column.rb +351 -0
- data/lib/kumiki/core.rb +2511 -0
- data/lib/kumiki/dsl.rb +408 -0
- data/lib/kumiki/frame_ranma.rb +570 -0
- data/lib/kumiki/markdown/ast.rb +127 -0
- data/lib/kumiki/markdown/mermaid/layout.rb +389 -0
- data/lib/kumiki/markdown/mermaid/models.rb +235 -0
- data/lib/kumiki/markdown/mermaid/parser.rb +522 -0
- data/lib/kumiki/markdown/mermaid/renderer.rb +339 -0
- data/lib/kumiki/markdown/parser.rb +808 -0
- data/lib/kumiki/markdown/renderer.rb +642 -0
- data/lib/kumiki/markdown/theme.rb +168 -0
- data/lib/kumiki/render_node.rb +262 -0
- data/lib/kumiki/row.rb +288 -0
- data/lib/kumiki/spacer.rb +20 -0
- data/lib/kumiki/style.rb +799 -0
- data/lib/kumiki/theme.rb +567 -0
- data/lib/kumiki/themes/material.rb +40 -0
- data/lib/kumiki/themes/tokyo_night.rb +11 -0
- data/lib/kumiki/version.rb +5 -0
- data/lib/kumiki/widgets/button.rb +105 -0
- data/lib/kumiki/widgets/calendar.rb +1028 -0
- data/lib/kumiki/widgets/checkbox.rb +119 -0
- data/lib/kumiki/widgets/container.rb +111 -0
- data/lib/kumiki/widgets/data_table.rb +670 -0
- data/lib/kumiki/widgets/divider.rb +31 -0
- data/lib/kumiki/widgets/image.rb +105 -0
- data/lib/kumiki/widgets/input.rb +485 -0
- data/lib/kumiki/widgets/markdown.rb +58 -0
- data/lib/kumiki/widgets/modal.rb +165 -0
- data/lib/kumiki/widgets/multiline_input.rb +970 -0
- data/lib/kumiki/widgets/multiline_text.rb +180 -0
- data/lib/kumiki/widgets/net_image.rb +100 -0
- data/lib/kumiki/widgets/progress_bar.rb +72 -0
- data/lib/kumiki/widgets/radio_buttons.rb +93 -0
- data/lib/kumiki/widgets/slider.rb +135 -0
- data/lib/kumiki/widgets/switch.rb +84 -0
- data/lib/kumiki/widgets/tabs.rb +175 -0
- data/lib/kumiki/widgets/text.rb +120 -0
- data/lib/kumiki/widgets/tree.rb +434 -0
- data/lib/kumiki/widgets/webview.rb +87 -0
- data/lib/kumiki.rb +130 -0
- metadata +113 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# Image widget - displays an image from a file path
|
|
3
|
+
|
|
4
|
+
# Image fit constants
|
|
5
|
+
IMAGE_FIT_FILL = 0
|
|
6
|
+
IMAGE_FIT_CONTAIN = 1
|
|
7
|
+
IMAGE_FIT_COVER = 2
|
|
8
|
+
|
|
9
|
+
class ImageWidget < Widget
|
|
10
|
+
def initialize(file_path)
|
|
11
|
+
super()
|
|
12
|
+
@file_path = file_path
|
|
13
|
+
@image_id = 0
|
|
14
|
+
@img_width = 0.0
|
|
15
|
+
@img_height = 0.0
|
|
16
|
+
@fit_mode = IMAGE_FIT_CONTAIN
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fit(mode)
|
|
20
|
+
@fit_mode = mode
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set_path(path)
|
|
25
|
+
@file_path = path
|
|
26
|
+
@image_id = 0
|
|
27
|
+
@img_width = 0.0
|
|
28
|
+
@img_height = 0.0
|
|
29
|
+
mark_dirty
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def load_if_needed(painter)
|
|
33
|
+
if @image_id == 0
|
|
34
|
+
@image_id = painter.load_image(@file_path)
|
|
35
|
+
if @image_id != 0
|
|
36
|
+
@img_width = painter.get_image_width(@image_id) * 1.0
|
|
37
|
+
@img_height = painter.get_image_height(@image_id) * 1.0
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def measure(painter)
|
|
43
|
+
load_if_needed(painter)
|
|
44
|
+
if @image_id != 0
|
|
45
|
+
Size.new(@img_width, @img_height)
|
|
46
|
+
else
|
|
47
|
+
Size.new(100.0, 100.0)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def redraw(painter, completely)
|
|
52
|
+
load_if_needed(painter)
|
|
53
|
+
if @image_id == 0
|
|
54
|
+
# Draw placeholder
|
|
55
|
+
painter.fill_round_rect(0.0, 0.0, @width, @height, 4.0, 0x40FFFFFF)
|
|
56
|
+
painter.stroke_round_rect(0.0, 0.0, @width, @height, 4.0, 0x80FFFFFF, 1.0)
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if @fit_mode == IMAGE_FIT_FILL
|
|
61
|
+
painter.draw_image(@image_id, 0.0, 0.0, @width, @height)
|
|
62
|
+
elsif @fit_mode == IMAGE_FIT_CONTAIN
|
|
63
|
+
draw_fitted(painter, true)
|
|
64
|
+
else
|
|
65
|
+
draw_fitted(painter, false)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def draw_fitted(painter, contain)
|
|
70
|
+
if @img_width < 1.0 || @img_height < 1.0 || @width < 1.0 || @height < 1.0
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
img_aspect = @img_width / @img_height
|
|
74
|
+
widget_aspect = @width / @height
|
|
75
|
+
|
|
76
|
+
if contain
|
|
77
|
+
if img_aspect > widget_aspect
|
|
78
|
+
new_w = @width
|
|
79
|
+
new_h = @width / img_aspect
|
|
80
|
+
else
|
|
81
|
+
new_h = @height
|
|
82
|
+
new_w = @height * img_aspect
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
if img_aspect > widget_aspect
|
|
86
|
+
new_h = @height
|
|
87
|
+
new_w = @height * img_aspect
|
|
88
|
+
else
|
|
89
|
+
new_w = @width
|
|
90
|
+
new_h = @width / img_aspect
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
dx = (@width - new_w) / 2.0
|
|
95
|
+
dy = (@height - new_h) / 2.0
|
|
96
|
+
painter.draw_image(@image_id, dx, dy, new_w, new_h)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Top-level helper
|
|
101
|
+
def Image(file_path)
|
|
102
|
+
ImageWidget.new(file_path)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# Input widget - single-line text input with IME, selection, and clipboard support
|
|
3
|
+
#
|
|
4
|
+
# State is held in InputState (defined in core.rb) which persists across
|
|
5
|
+
# Component rebuilds. The widget delegates all text/cursor/selection/IME
|
|
6
|
+
# operations to InputState and handles rendering + event dispatch.
|
|
7
|
+
#
|
|
8
|
+
# Key ordinals used in input_key (see RANMA_KEY_MAP):
|
|
9
|
+
# ENTER=11, BACKSPACE=12, ESCAPE=17, END=21, HOME=22, LEFT=23, RIGHT=25, DELETE=75
|
|
10
|
+
# A=43, C=45, V=64, X=66
|
|
11
|
+
|
|
12
|
+
class Input < Widget
|
|
13
|
+
def initialize(state)
|
|
14
|
+
super()
|
|
15
|
+
@state = state
|
|
16
|
+
@focused = false
|
|
17
|
+
@font_size_val = 14.0
|
|
18
|
+
@bg_color = 0
|
|
19
|
+
@text_color = 0
|
|
20
|
+
@placeholder_color = 0
|
|
21
|
+
@border_color = 0
|
|
22
|
+
@focus_border = 0
|
|
23
|
+
@use_theme = true
|
|
24
|
+
@radius = 4.0
|
|
25
|
+
@focusable = true
|
|
26
|
+
@pad_top = 8.0
|
|
27
|
+
@pad_right = 12.0
|
|
28
|
+
@pad_bottom = 8.0
|
|
29
|
+
@pad_left = 12.0
|
|
30
|
+
# Character position cache for click-to-position
|
|
31
|
+
@char_positions = []
|
|
32
|
+
@text_start_x = 0.0
|
|
33
|
+
# on_change callback
|
|
34
|
+
@on_change_cb = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_text
|
|
38
|
+
@state.value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def set_text(t)
|
|
42
|
+
@state.set(t)
|
|
43
|
+
mark_dirty
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def font_size(s)
|
|
47
|
+
@font_size_val = s
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_change(&block)
|
|
52
|
+
@on_change_cb = block
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def measure(painter)
|
|
57
|
+
th = painter.measure_text_height(Kumiki.theme.font_family, @font_size_val)
|
|
58
|
+
Size.new(@width, th + @pad_top + @pad_bottom)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# --- Rendering ---
|
|
62
|
+
|
|
63
|
+
def redraw(painter, completely)
|
|
64
|
+
# Resolve colors from theme
|
|
65
|
+
bg_c = @use_theme ? Kumiki.theme.bg_primary : @bg_color
|
|
66
|
+
tc = @use_theme ? Kumiki.theme.text_primary : @text_color
|
|
67
|
+
pc = @use_theme ? Kumiki.theme.text_secondary : @placeholder_color
|
|
68
|
+
brd_c = @use_theme ? Kumiki.theme.border : @border_color
|
|
69
|
+
fbc = @use_theme ? Kumiki.theme.border_focus : @focus_border
|
|
70
|
+
|
|
71
|
+
bc = @focused ? fbc : brd_c
|
|
72
|
+
painter.fill_round_rect(0.0, 0.0, @width, @height, @radius, bg_c)
|
|
73
|
+
painter.stroke_round_rect(0.0, 0.0, @width, @height, @radius, bc, 1.0)
|
|
74
|
+
|
|
75
|
+
ascent = painter.get_text_ascent(Kumiki.theme.font_family, @font_size_val)
|
|
76
|
+
display_text = @state.get_display_text
|
|
77
|
+
@text_start_x = @pad_left
|
|
78
|
+
|
|
79
|
+
if display_text.length > 0
|
|
80
|
+
# Draw selection highlight first (behind text)
|
|
81
|
+
if @state.has_selection
|
|
82
|
+
draw_selection_highlight(painter, display_text, ascent)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
painter.draw_text(display_text, @text_start_x, @pad_top + ascent, Kumiki.theme.font_family, @font_size_val, tc)
|
|
86
|
+
|
|
87
|
+
# Build character position cache for click-to-position
|
|
88
|
+
@char_positions = [0.0]
|
|
89
|
+
i = 0
|
|
90
|
+
while i < display_text.length
|
|
91
|
+
sub = display_text[0, i + 1]
|
|
92
|
+
w = painter.measure_text_width(sub, Kumiki.theme.font_family, @font_size_val)
|
|
93
|
+
@char_positions.push(w)
|
|
94
|
+
i = i + 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Draw preedit underline
|
|
98
|
+
if @state.has_preedit && @focused
|
|
99
|
+
draw_preedit_underline(painter, ascent, tc)
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
painter.draw_text(@state.get_placeholder, @text_start_x, @pad_top + ascent, Kumiki.theme.font_family, @font_size_val, pc)
|
|
103
|
+
@char_positions = [0.0]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Cursor (when focused)
|
|
107
|
+
if @focused
|
|
108
|
+
draw_cursor(painter, tc)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def draw_preedit_underline(painter, ascent, tc)
|
|
113
|
+
text_before_preedit = ""
|
|
114
|
+
cursor = @state.get_cursor
|
|
115
|
+
if cursor > 0
|
|
116
|
+
text_before_preedit = @state.value[0, cursor]
|
|
117
|
+
end
|
|
118
|
+
preedit_start_x = @pad_left + painter.measure_text_width(text_before_preedit, Kumiki.theme.font_family, @font_size_val)
|
|
119
|
+
preedit_width = painter.measure_text_width(@state.get_preedit_text, Kumiki.theme.font_family, @font_size_val)
|
|
120
|
+
underline_y = @pad_top + ascent + 2.0
|
|
121
|
+
painter.fill_rect(preedit_start_x, underline_y, preedit_width, 2.0, tc)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def draw_cursor(painter, tc)
|
|
125
|
+
text_before_caret = compute_text_before_caret
|
|
126
|
+
cursor_x = @pad_left + painter.measure_text_width(text_before_caret, Kumiki.theme.font_family, @font_size_val)
|
|
127
|
+
painter.draw_line(cursor_x, @pad_top, cursor_x, @height - @pad_bottom, tc, 1.0)
|
|
128
|
+
|
|
129
|
+
# Notify IME of cursor position
|
|
130
|
+
notify_ime_cursor_rect(cursor_x)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def compute_text_before_caret
|
|
134
|
+
result = ""
|
|
135
|
+
cursor = @state.get_cursor
|
|
136
|
+
if @state.has_preedit
|
|
137
|
+
if cursor > 0
|
|
138
|
+
result = @state.value[0, cursor]
|
|
139
|
+
end
|
|
140
|
+
result = result + @state.get_preedit_text[0, @state.get_preedit_cursor]
|
|
141
|
+
else
|
|
142
|
+
if cursor > 0
|
|
143
|
+
result = @state.value[0, cursor]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def draw_selection_highlight(painter, display_text, ascent)
|
|
150
|
+
if @state.has_selection
|
|
151
|
+
range = @state.get_selection_range
|
|
152
|
+
s = range[0]
|
|
153
|
+
e = range[1]
|
|
154
|
+
# Clamp to display text
|
|
155
|
+
if s > display_text.length
|
|
156
|
+
s = display_text.length
|
|
157
|
+
end
|
|
158
|
+
if e > display_text.length
|
|
159
|
+
e = display_text.length
|
|
160
|
+
end
|
|
161
|
+
if s < e
|
|
162
|
+
x_start = @pad_left
|
|
163
|
+
if s > 0
|
|
164
|
+
x_start = @pad_left + painter.measure_text_width(display_text[0, s], Kumiki.theme.font_family, @font_size_val)
|
|
165
|
+
end
|
|
166
|
+
x_end = @pad_left + painter.measure_text_width(display_text[0, e], Kumiki.theme.font_family, @font_size_val)
|
|
167
|
+
|
|
168
|
+
sel_color = Kumiki.theme.bg_selected
|
|
169
|
+
painter.fill_rect(x_start, @pad_top, x_end - x_start, @height - @pad_top - @pad_bottom, sel_color)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def notify_ime_cursor_rect(cursor_x)
|
|
175
|
+
app = App.current
|
|
176
|
+
if app != nil
|
|
177
|
+
app.set_ime_cursor_rect(
|
|
178
|
+
(@x + cursor_x).to_i,
|
|
179
|
+
@y.to_i,
|
|
180
|
+
1,
|
|
181
|
+
@height.to_i
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# --- Focus ---
|
|
187
|
+
|
|
188
|
+
def focused
|
|
189
|
+
@focused = true
|
|
190
|
+
@state.start_editing
|
|
191
|
+
app = App.current
|
|
192
|
+
if app != nil
|
|
193
|
+
app.enable_text_input
|
|
194
|
+
end
|
|
195
|
+
mark_dirty
|
|
196
|
+
update
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def restore_focus
|
|
200
|
+
@focused = true
|
|
201
|
+
app = App.current
|
|
202
|
+
if app != nil
|
|
203
|
+
app.enable_text_input
|
|
204
|
+
end
|
|
205
|
+
mark_dirty
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def unfocused
|
|
209
|
+
@focused = false
|
|
210
|
+
@state.finish_editing
|
|
211
|
+
app = App.current
|
|
212
|
+
if app != nil
|
|
213
|
+
app.disable_text_input
|
|
214
|
+
end
|
|
215
|
+
mark_dirty
|
|
216
|
+
update
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# --- Mouse events ---
|
|
220
|
+
|
|
221
|
+
def mouse_down(ev)
|
|
222
|
+
@focused = true
|
|
223
|
+
# Click-to-position
|
|
224
|
+
click_x = ev.pos.x
|
|
225
|
+
rel_x = click_x - @text_start_x
|
|
226
|
+
char_pos = pos_from_click(rel_x)
|
|
227
|
+
|
|
228
|
+
# Clear preedit on click
|
|
229
|
+
if @state.has_preedit
|
|
230
|
+
@state.clear_preedit
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Start selection
|
|
234
|
+
@state.start_selection(char_pos)
|
|
235
|
+
|
|
236
|
+
mark_dirty
|
|
237
|
+
update
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def mouse_drag(ev)
|
|
241
|
+
if @state.is_selecting
|
|
242
|
+
rel_x = ev.pos.x - @text_start_x
|
|
243
|
+
char_pos = pos_from_click(rel_x)
|
|
244
|
+
@state.update_selection(char_pos)
|
|
245
|
+
mark_dirty
|
|
246
|
+
update
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def mouse_up(ev)
|
|
251
|
+
@state.end_selection
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def pos_from_click(rel_x)
|
|
255
|
+
result = @state.value.length
|
|
256
|
+
if rel_x <= 0.0
|
|
257
|
+
result = 0
|
|
258
|
+
else
|
|
259
|
+
found = false
|
|
260
|
+
i = 1
|
|
261
|
+
while i < @char_positions.length && !found
|
|
262
|
+
pos = @char_positions[i]
|
|
263
|
+
if pos > rel_x
|
|
264
|
+
prev_pos = @char_positions[i - 1]
|
|
265
|
+
if (rel_x - prev_pos) < (pos - rel_x)
|
|
266
|
+
result = i - 1
|
|
267
|
+
else
|
|
268
|
+
result = i
|
|
269
|
+
end
|
|
270
|
+
found = true
|
|
271
|
+
end
|
|
272
|
+
i = i + 1
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
result
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# --- IME ---
|
|
279
|
+
|
|
280
|
+
def ime_preedit(text, sel_start, sel_end)
|
|
281
|
+
if text != nil && text.length > 0
|
|
282
|
+
@state.set_preedit(text, sel_start)
|
|
283
|
+
else
|
|
284
|
+
@state.clear_preedit
|
|
285
|
+
end
|
|
286
|
+
mark_dirty
|
|
287
|
+
update
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# --- Text input ---
|
|
291
|
+
|
|
292
|
+
def input_char(text)
|
|
293
|
+
# Clear preedit when text is committed
|
|
294
|
+
if @state.has_preedit
|
|
295
|
+
@state.clear_preedit
|
|
296
|
+
end
|
|
297
|
+
# Delete selection if any
|
|
298
|
+
if @state.has_selection
|
|
299
|
+
@state.delete_selection
|
|
300
|
+
end
|
|
301
|
+
@state.insert(text)
|
|
302
|
+
@on_change_cb.call(@state.value) if @on_change_cb
|
|
303
|
+
mark_dirty
|
|
304
|
+
update
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# --- Key input ---
|
|
308
|
+
|
|
309
|
+
def input_key(key_code, modifiers)
|
|
310
|
+
# During IME preedit, let IME handle key events
|
|
311
|
+
if @state.has_preedit
|
|
312
|
+
handle_preedit_key(key_code)
|
|
313
|
+
return
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Check for Cmd (bit 3 = MAC_COMMAND) or Ctrl (bit 1) modifier
|
|
317
|
+
is_cmd = (modifiers & 8) != 0 || (modifiers & 2) != 0
|
|
318
|
+
|
|
319
|
+
if is_cmd
|
|
320
|
+
handle_cmd_key(key_code)
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Clear selection on navigation keys
|
|
325
|
+
if key_code == 23 || key_code == 25
|
|
326
|
+
@state.clear_selection
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Delete selection on content-modifying keys
|
|
330
|
+
if (key_code == 12 || key_code == 75) && @state.has_selection
|
|
331
|
+
@state.delete_selection
|
|
332
|
+
@on_change_cb.call(@state.value) if @on_change_cb
|
|
333
|
+
mark_dirty
|
|
334
|
+
update
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
handle_navigation_key(key_code)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def handle_preedit_key(key_code)
|
|
342
|
+
# Workaround: single-char preedit + backspace/escape
|
|
343
|
+
if @state.get_preedit_text.length == 1
|
|
344
|
+
if key_code == 12 || key_code == 17
|
|
345
|
+
@state.clear_preedit
|
|
346
|
+
mark_dirty
|
|
347
|
+
update
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def handle_cmd_key(key_code)
|
|
353
|
+
# Cmd+C (Copy) - C ordinal = 45
|
|
354
|
+
if key_code == 45
|
|
355
|
+
handle_copy
|
|
356
|
+
# Cmd+X (Cut) - X ordinal = 66
|
|
357
|
+
elsif key_code == 66
|
|
358
|
+
handle_cut
|
|
359
|
+
# Cmd+V (Paste) - V ordinal = 64
|
|
360
|
+
elsif key_code == 64
|
|
361
|
+
handle_paste
|
|
362
|
+
# Cmd+A (Select All) - A ordinal = 43
|
|
363
|
+
elsif key_code == 43
|
|
364
|
+
@state.select_all
|
|
365
|
+
mark_dirty
|
|
366
|
+
update
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def handle_navigation_key(key_code)
|
|
371
|
+
# Backspace (key ordinal 12)
|
|
372
|
+
if key_code == 12
|
|
373
|
+
if @state.delete_prev
|
|
374
|
+
@on_change_cb.call(@state.value) if @on_change_cb
|
|
375
|
+
mark_dirty
|
|
376
|
+
update
|
|
377
|
+
end
|
|
378
|
+
# Delete (key ordinal 75)
|
|
379
|
+
elsif key_code == 75
|
|
380
|
+
if @state.delete_next
|
|
381
|
+
@on_change_cb.call(@state.value) if @on_change_cb
|
|
382
|
+
mark_dirty
|
|
383
|
+
update
|
|
384
|
+
end
|
|
385
|
+
# Left arrow (key ordinal 23)
|
|
386
|
+
elsif key_code == 23
|
|
387
|
+
if @state.move_prev
|
|
388
|
+
mark_dirty
|
|
389
|
+
update
|
|
390
|
+
end
|
|
391
|
+
# Right arrow (key ordinal 25)
|
|
392
|
+
elsif key_code == 25
|
|
393
|
+
if @state.move_next
|
|
394
|
+
mark_dirty
|
|
395
|
+
update
|
|
396
|
+
end
|
|
397
|
+
# Home (key ordinal 22) - move to beginning
|
|
398
|
+
elsif key_code == 22
|
|
399
|
+
if @state.move_home
|
|
400
|
+
mark_dirty
|
|
401
|
+
update
|
|
402
|
+
end
|
|
403
|
+
# End (key ordinal 21) - move to end
|
|
404
|
+
elsif key_code == 21
|
|
405
|
+
if @state.move_end
|
|
406
|
+
mark_dirty
|
|
407
|
+
update
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# --- Clipboard ---
|
|
413
|
+
|
|
414
|
+
def handle_copy
|
|
415
|
+
text = @state.get_selected_text
|
|
416
|
+
if text.length > 0
|
|
417
|
+
app = App.current
|
|
418
|
+
if app != nil
|
|
419
|
+
app.set_clipboard_text(text)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def handle_cut
|
|
425
|
+
text = @state.get_selected_text
|
|
426
|
+
if text.length > 0
|
|
427
|
+
@state.delete_selection
|
|
428
|
+
app = App.current
|
|
429
|
+
if app != nil
|
|
430
|
+
app.set_clipboard_text(text)
|
|
431
|
+
end
|
|
432
|
+
@on_change_cb.call(@state.value) if @on_change_cb
|
|
433
|
+
mark_dirty
|
|
434
|
+
update
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def handle_paste
|
|
439
|
+
app = App.current
|
|
440
|
+
if app == nil
|
|
441
|
+
return
|
|
442
|
+
end
|
|
443
|
+
text = app.get_clipboard_text
|
|
444
|
+
if text == nil
|
|
445
|
+
return
|
|
446
|
+
end
|
|
447
|
+
if text.length == 0
|
|
448
|
+
return
|
|
449
|
+
end
|
|
450
|
+
if @state.has_selection
|
|
451
|
+
@state.delete_selection
|
|
452
|
+
end
|
|
453
|
+
paste_text(text)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def paste_text(text)
|
|
457
|
+
# Single line: take first line only
|
|
458
|
+
first_line = find_first_line(text)
|
|
459
|
+
@state.insert(first_line)
|
|
460
|
+
@on_change_cb.call(@state.value) if @on_change_cb
|
|
461
|
+
mark_dirty
|
|
462
|
+
update
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def find_first_line(text)
|
|
466
|
+
result = text
|
|
467
|
+
found = false
|
|
468
|
+
newline_idx = 0
|
|
469
|
+
while newline_idx < text.length && !found
|
|
470
|
+
if text[newline_idx] == "\n"
|
|
471
|
+
result = text[0, newline_idx]
|
|
472
|
+
found = true
|
|
473
|
+
end
|
|
474
|
+
newline_idx = newline_idx + 1
|
|
475
|
+
end
|
|
476
|
+
result
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Top-level helper — accepts placeholder string for backward compatibility
|
|
481
|
+
def Input(placeholder)
|
|
482
|
+
Input.new(InputState.new(placeholder))
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# Markdown widget - renders markdown text
|
|
3
|
+
# Combines MarkdownParser + MarkdownRenderer
|
|
4
|
+
|
|
5
|
+
class Markdown < Widget
|
|
6
|
+
def initialize(text)
|
|
7
|
+
super()
|
|
8
|
+
@source = text
|
|
9
|
+
@md_theme = MarkdownTheme.new
|
|
10
|
+
@parser = MarkdownParser.new
|
|
11
|
+
@renderer = MarkdownRenderer.new(@md_theme)
|
|
12
|
+
@ast = nil
|
|
13
|
+
@content_height = 0.0
|
|
14
|
+
@padding_val = 12.0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def padding(p)
|
|
18
|
+
@padding_val = p
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set_text(t)
|
|
23
|
+
@source = t
|
|
24
|
+
@ast = nil
|
|
25
|
+
mark_dirty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get_text
|
|
29
|
+
@source
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def measure(painter)
|
|
33
|
+
ensure_parsed
|
|
34
|
+
h = @renderer.measure_height(painter, @ast, @width, @padding_val)
|
|
35
|
+
Size.new(@width, h)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def redraw(painter, completely)
|
|
39
|
+
ensure_parsed
|
|
40
|
+
# Background
|
|
41
|
+
painter.fill_rect(0.0, 0.0, @width, @height, Kumiki.theme.bg_canvas)
|
|
42
|
+
# Render markdown
|
|
43
|
+
@content_height = @renderer.render(painter, @ast, @width, @padding_val)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ensure_parsed
|
|
47
|
+
if @ast == nil
|
|
48
|
+
@ast = @parser.parse(@source)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Top-level helper
|
|
54
|
+
def Markdown(text)
|
|
55
|
+
Markdown.new(text)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|