1 | // Copyright (c) 2020 Raúl Hernández. All rights reserved. |
2 | // Use of this source code is governed by an MIT license |
3 | // that can be found in the LICENSE file. |
4 | module main |
5 | |
6 | import term.ui |
7 | |
8 | // The color palette, taken from Google's Material design |
9 | const ( |
10 | colors = [ |
11 | [ |
12 | ui.Color{239, 154, 154}, |
13 | ui.Color{244, 143, 177}, |
14 | ui.Color{206, 147, 216}, |
15 | ui.Color{179, 157, 219}, |
16 | ui.Color{159, 168, 218}, |
17 | ui.Color{144, 202, 249}, |
18 | ui.Color{129, 212, 250}, |
19 | ui.Color{128, 222, 234}, |
20 | ui.Color{128, 203, 196}, |
21 | ui.Color{165, 214, 167}, |
22 | ui.Color{197, 225, 165}, |
23 | ui.Color{230, 238, 156}, |
24 | ui.Color{255, 245, 157}, |
25 | ui.Color{255, 224, 130}, |
26 | ui.Color{255, 204, 128}, |
27 | ui.Color{255, 171, 145}, |
28 | ui.Color{188, 170, 164}, |
29 | ui.Color{238, 238, 238}, |
30 | ui.Color{176, 190, 197}, |
31 | ], |
32 | [ |
33 | ui.Color{244, 67, 54}, |
34 | ui.Color{233, 30, 99}, |
35 | ui.Color{156, 39, 176}, |
36 | ui.Color{103, 58, 183}, |
37 | ui.Color{63, 81, 181}, |
38 | ui.Color{33, 150, 243}, |
39 | ui.Color{3, 169, 244}, |
40 | ui.Color{0, 188, 212}, |
41 | ui.Color{0, 150, 136}, |
42 | ui.Color{76, 175, 80}, |
43 | ui.Color{139, 195, 74}, |
44 | ui.Color{205, 220, 57}, |
45 | ui.Color{255, 235, 59}, |
46 | ui.Color{255, 193, 7}, |
47 | ui.Color{255, 152, 0}, |
48 | ui.Color{255, 87, 34}, |
49 | ui.Color{121, 85, 72}, |
50 | ui.Color{120, 120, 120}, |
51 | ui.Color{96, 125, 139}, |
52 | ], |
53 | [ |
54 | ui.Color{198, 40, 40}, |
55 | ui.Color{173, 20, 87}, |
56 | ui.Color{106, 27, 154}, |
57 | ui.Color{69, 39, 160}, |
58 | ui.Color{40, 53, 147}, |
59 | ui.Color{21, 101, 192}, |
60 | ui.Color{2, 119, 189}, |
61 | ui.Color{0, 131, 143}, |
62 | ui.Color{0, 105, 92}, |
63 | ui.Color{46, 125, 50}, |
64 | ui.Color{85, 139, 47}, |
65 | ui.Color{158, 157, 36}, |
66 | ui.Color{249, 168, 37}, |
67 | ui.Color{255, 143, 0}, |
68 | ui.Color{239, 108, 0}, |
69 | ui.Color{216, 67, 21}, |
70 | ui.Color{78, 52, 46}, |
71 | ui.Color{33, 33, 33}, |
72 | ui.Color{55, 71, 79}, |
73 | ], |
74 | ] |
75 | ) |
76 | |
77 | const ( |
78 | frame_rate = 30 // fps |
79 | msg_display_time = 5 * frame_rate |
80 | w = 200 |
81 | h = 100 |
82 | space = ' ' |
83 | spaces = ' ' |
84 | select_color = 'Select color: ' |
85 | select_size = 'Size: + -' |
86 | help_1 = '╭────────╮' |
87 | help_2 = '│ HELP │' |
88 | help_3 = '╰────────╯' |
89 | ) |
90 | |
91 | struct App { |
92 | mut: |
93 | ui &ui.Context = unsafe { nil } |
94 | header_text []string |
95 | mouse_pos Point |
96 | msg string |
97 | msg_hide_tick int |
98 | primary_color ui.Color = colors[1][6] |
99 | secondary_color ui.Color = colors[1][9] |
100 | primary_color_idx int = 25 |
101 | secondary_color_idx int = 28 |
102 | bg_color ui.Color = ui.Color{0, 0, 0} |
103 | drawing [][]ui.Color = [][]ui.Color{len: h, init: []ui.Color{len: w}} |
104 | size int = 1 |
105 | should_redraw bool = true |
106 | is_dragging bool |
107 | } |
108 | |
109 | struct Point { |
110 | mut: |
111 | x int |
112 | y int |
113 | } |
114 | |
115 | fn main() { |
116 | mut app := &App{} |
117 | app.ui = ui.init( |
118 | user_data: app |
119 | frame_fn: frame |
120 | event_fn: event |
121 | frame_rate: frame_rate |
122 | hide_cursor: true |
123 | window_title: 'V terminal pixelart drawing app' |
124 | ) |
125 | app.mouse_pos.x = 40 |
126 | app.mouse_pos.y = 15 |
127 | app.ui.clear() |
128 | app.ui.run()! |
129 | } |
130 | |
131 | fn frame(mut app App) { |
132 | mut redraw := app.should_redraw |
133 | if app.msg != '' && app.ui.frame_count >= app.msg_hide_tick { |
134 | app.msg = '' |
135 | redraw = true |
136 | } |
137 | if redraw { |
138 | app.render(false) |
139 | app.should_redraw = false |
140 | } |
141 | } |
142 | |
143 | fn event(event &ui.Event, mut app App) { |
144 | match event.typ { |
145 | .mouse_down { |
146 | app.is_dragging = true |
147 | if app.ui.window_height - event.y < 5 { |
148 | app.footer_click(event) |
149 | } else { |
150 | app.paint(event) |
151 | } |
152 | } |
153 | .mouse_up { |
154 | app.is_dragging = false |
155 | } |
156 | .mouse_drag { |
157 | app.mouse_pos = Point{ |
158 | x: event.x |
159 | y: event.y |
160 | } |
161 | app.paint(event) |
162 | } |
163 | .mouse_move { |
164 | app.mouse_pos = Point{ |
165 | x: event.x |
166 | y: event.y |
167 | } |
168 | } |
169 | .mouse_scroll { |
170 | app.mouse_pos = Point{ |
171 | x: event.x |
172 | y: event.y |
173 | } |
174 | d := event.direction == .down |
175 | if event.modifiers.has(.ctrl) { |
176 | p := !event.modifiers.has(.shift) |
177 | c := if d { |
178 | if p { app.primary_color_idx - 1 } else { app.secondary_color_idx - 1 } |
179 | } else { |
180 | if p { app.primary_color_idx + 1 } else { app.secondary_color_idx + 1 } |
181 | } |
182 | app.select_color(p, c) |
183 | } else { |
184 | if d { |
185 | app.inc_size() |
186 | } else { |
187 | app.dec_size() |
188 | } |
189 | } |
190 | } |
191 | .key_down { |
192 | match event.code { |
193 | .f1, ._1 { |
194 | oevent := *event |
195 | nevent := ui.Event{ |
196 | ...oevent |
197 | button: ui.MouseButton.left |
198 | x: app.mouse_pos.x |
199 | y: app.mouse_pos.y |
200 | } |
201 | app.paint(nevent) |
202 | } |
203 | .f2, ._2 { |
204 | oevent := *event |
205 | nevent := ui.Event{ |
206 | ...oevent |
207 | button: ui.MouseButton.right |
208 | x: app.mouse_pos.x |
209 | y: app.mouse_pos.y |
210 | } |
211 | app.paint(nevent) |
212 | } |
213 | .space, .enter { |
214 | oevent := *event |
215 | nevent := ui.Event{ |
216 | ...oevent |
217 | button: .left |
218 | x: app.mouse_pos.x |
219 | y: app.mouse_pos.y |
220 | } |
221 | app.paint(nevent) |
222 | } |
223 | .delete, .backspace { |
224 | oevent := *event |
225 | nevent := ui.Event{ |
226 | ...oevent |
227 | button: .middle |
228 | x: app.mouse_pos.x |
229 | y: app.mouse_pos.y |
230 | } |
231 | app.paint(nevent) |
232 | } |
233 | .j, .down { |
234 | if event.modifiers.has(.shift) { |
235 | app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color) |
236 | } |
237 | app.mouse_pos.y++ |
238 | } |
239 | .k, .up { |
240 | if event.modifiers.has(.shift) { |
241 | app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color) |
242 | } |
243 | app.mouse_pos.y-- |
244 | } |
245 | .h, .left { |
246 | if event.modifiers.has(.shift) { |
247 | app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color) |
248 | } |
249 | app.mouse_pos.x -= 2 |
250 | } |
251 | .l, .right { |
252 | if event.modifiers.has(.shift) { |
253 | app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color) |
254 | } |
255 | app.mouse_pos.x += 2 |
256 | } |
257 | .t { |
258 | p := !event.modifiers.has(.alt) |
259 | c := if event.modifiers.has(.shift) { |
260 | if p { app.primary_color_idx - 19 } else { app.secondary_color_idx - 19 } |
261 | } else { |
262 | if p { app.primary_color_idx + 19 } else { app.secondary_color_idx + 19 } |
263 | } |
264 | app.select_color(p, c) |
265 | } |
266 | .r { |
267 | p := !event.modifiers.has(.alt) |
268 | c := if event.modifiers.has(.shift) { |
269 | if p { app.primary_color_idx - 1 } else { app.secondary_color_idx - 1 } |
270 | } else { |
271 | if p { app.primary_color_idx + 1 } else { app.secondary_color_idx + 1 } |
272 | } |
273 | app.select_color(p, c) |
274 | } |
275 | .plus { |
276 | app.inc_size() |
277 | } |
278 | .minus { |
279 | app.dec_size() |
280 | } |
281 | .c { |
282 | app.drawing = [][]ui.Color{len: h, init: []ui.Color{len: w}} |
283 | } |
284 | .q, .escape { |
285 | app.render(true) |
286 | exit(0) |
287 | } |
288 | else {} |
289 | } |
290 | } |
291 | else {} |
292 | } |
293 | app.should_redraw = true |
294 | } |
295 | |
296 | fn (mut app App) render(paint_only bool) { |
297 | app.ui.clear() |
298 | app.draw_header() |
299 | app.draw_content() |
300 | if !paint_only { |
301 | app.draw_footer() |
302 | app.draw_cursor() |
303 | } |
304 | app.ui.flush() |
305 | } |
306 | |
307 | fn (mut app App) select_color(primary bool, idx int) { |
308 | c := (idx + 57) % 57 |
309 | cx := c % 19 |
310 | cy := (c / 19) % 3 |
311 | color := colors[cy][cx] |
312 | if primary { |
313 | app.primary_color_idx = c % (19 * 3) |
314 | app.primary_color = color |
315 | } else { |
316 | app.secondary_color_idx = c % (19 * 3) |
317 | app.secondary_color = color |
318 | } |
319 | c_str := if primary { 'primary' } else { 'secondary' } |
320 | app.show_msg('set ${c_str} color idx: ${idx}', 1) |
321 | } |
322 | |
323 | fn (mut app App) set_pixel(x_ int, y_ int, c ui.Color) { |
324 | // Term coords start at 1, and adjust for the header |
325 | x, y := x_ - 1, y_ - 4 |
326 | if y < 0 || app.ui.window_height - y < 3 { |
327 | return |
328 | } |
329 | if y >= app.drawing.len || x < 0 || x >= app.drawing[0].len { |
330 | return |
331 | } |
332 | app.drawing[y][x] = c |
333 | } |
334 | |
335 | fn (mut app App) paint(event &ui.Event) { |
336 | if event.y < 4 || app.ui.window_height - event.y < 4 { |
337 | return |
338 | } |
339 | x_start, y_start := int(f32((event.x - 1) / 2) - app.size / 2 + 1), event.y - app.size / 2 |
340 | color := match event.button { |
341 | .left { app.primary_color } |
342 | .right { app.secondary_color } |
343 | else { app.bg_color } |
344 | } |
345 | for x in x_start .. x_start + app.size { |
346 | for y in y_start .. y_start + app.size { |
347 | app.set_pixel(x, y, color) |
348 | } |
349 | } |
350 | } |
351 | |
352 | fn (mut app App) draw_content() { |
353 | w_, mut h_ := app.ui.window_width / 2, app.ui.window_height - 8 |
354 | if h_ > app.drawing.len { |
355 | h_ = app.drawing.len |
356 | } |
357 | for row_idx, row in app.drawing[..h_] { |
358 | app.ui.set_cursor_position(0, row_idx + 4) |
359 | mut last := ui.Color{0, 0, 0} |
360 | for cell in row[..w_] { |
361 | if cell.r == 0 && cell.g == 0 && cell.b == 0 { |
362 | if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) { |
363 | app.ui.reset() |
364 | } |
365 | } else { |
366 | if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) { |
367 | app.ui.set_bg_color(cell) |
368 | } |
369 | } |
370 | app.ui.write(spaces) |
371 | last = cell |
372 | } |
373 | app.ui.reset() |
374 | } |
375 | } |
376 | |
377 | fn (mut app App) draw_cursor() { |
378 | if app.mouse_pos.y in [3, app.ui.window_height - 5] { |
379 | // inside the horizontal separators |
380 | return |
381 | } |
382 | cursor_color := if app.is_dragging { ui.Color{220, 220, 220} } else { ui.Color{160, 160, 160} } |
383 | app.ui.set_bg_color(cursor_color) |
384 | if app.mouse_pos.y >= 3 && app.mouse_pos.y <= app.ui.window_height - 4 { |
385 | // inside the main content |
386 | mut x_start := int(f32((app.mouse_pos.x - 1) / 2) - app.size / 2 + 1) * 2 - 1 |
387 | mut y_start := app.mouse_pos.y - app.size / 2 |
388 | mut x_end := x_start + app.size * 2 - 1 |
389 | mut y_end := y_start + app.size - 1 |
390 | if x_start < 1 { |
391 | x_start = 1 |
392 | } |
393 | if y_start < 4 { |
394 | y_start = 4 |
395 | } |
396 | if x_end > app.ui.window_width { |
397 | x_end = app.ui.window_width |
398 | } |
399 | if y_end > app.ui.window_height - 5 { |
400 | y_end = app.ui.window_height - 5 |
401 | } |
402 | app.ui.draw_rect(x_start, y_start, x_end, y_end) |
403 | } else { |
404 | app.ui.draw_text(app.mouse_pos.x, app.mouse_pos.y, space) |
405 | } |
406 | app.ui.reset() |
407 | } |
408 | |
409 | fn (mut app App) draw_header() { |
410 | if app.msg != '' { |
411 | app.ui.set_color( |
412 | r: 0 |
413 | g: 0 |
414 | b: 0 |
415 | ) |
416 | app.ui.set_bg_color( |
417 | r: 220 |
418 | g: 220 |
419 | b: 220 |
420 | ) |
421 | app.ui.draw_text(0, 0, ' ${app.msg} ') |
422 | app.ui.reset() |
423 | } |
424 | //'tick: $app.ui.frame_count | ' + |
425 | app.ui.draw_text(3, 2, 'terminal size: (${app.ui.window_width}, ${app.ui.window_height}) | primary color: ${app.primary_color.hex()} | secondary color: ${app.secondary_color.hex()}') |
426 | app.ui.horizontal_separator(3) |
427 | } |
428 | |
429 | fn (mut app App) draw_footer() { |
430 | _, wh := app.ui.window_width, app.ui.window_height |
431 | app.ui.horizontal_separator(wh - 4) |
432 | for i, color_row in colors { |
433 | for j, color in color_row { |
434 | x := j * 3 + 19 |
435 | y := wh - 3 + i |
436 | app.ui.set_bg_color(color) |
437 | if app.primary_color_idx == j + (i * 19) { |
438 | app.ui.set_color(r: 0, g: 0, b: 0) |
439 | app.ui.draw_text(x, y, '><') |
440 | app.ui.reset_color() |
441 | } else if app.secondary_color_idx == j + (i * 19) { |
442 | app.ui.set_color(r: 255, g: 255, b: 255) |
443 | app.ui.draw_text(x, y, '><') |
444 | app.ui.reset_color() |
445 | } else { |
446 | app.ui.draw_rect(x, y, x + 1, y) |
447 | } |
448 | } |
449 | } |
450 | app.ui.reset_bg_color() |
451 | app.ui.draw_text(3, wh - 3, select_color) |
452 | app.ui.bold() |
453 | app.ui.draw_text(3, wh - 1, '${select_size} ${app.size}') |
454 | app.ui.reset() |
455 | |
456 | // TODO: help button |
457 | // if ww >= 90 { |
458 | // app.ui.draw_text(80, wh - 3, help_1) |
459 | // app.ui.draw_text(80, wh - 2, help_2) |
460 | // app.ui.draw_text(80, wh - 1, help_3) |
461 | // } |
462 | } |
463 | |
464 | [inline] |
465 | fn (mut app App) inc_size() { |
466 | if app.size < 30 { |
467 | app.size++ |
468 | } |
469 | app.show_msg('inc. size: ${app.size}', 1) |
470 | } |
471 | |
472 | [inline] |
473 | fn (mut app App) dec_size() { |
474 | if app.size > 1 { |
475 | app.size-- |
476 | } |
477 | app.show_msg('dec. size: ${app.size}', 1) |
478 | } |
479 | |
480 | fn (mut app App) footer_click(event &ui.Event) { |
481 | footer_y := 3 - (app.ui.window_height - event.y) |
482 | match event.x { |
483 | 8...11 { |
484 | app.inc_size() |
485 | } |
486 | 12...15 { |
487 | app.dec_size() |
488 | } |
489 | 18...75 { |
490 | if (event.x % 3) == 0 { |
491 | // Inside the gap between tiles |
492 | return |
493 | } |
494 | idx := footer_y * 19 - 6 + event.x / 3 |
495 | if idx < 0 || idx > 56 { |
496 | return |
497 | } |
498 | app.select_color(event.button == .left, idx) |
499 | } |
500 | else {} |
501 | } |
502 | } |
503 | |
504 | fn (mut app App) show_msg(text string, time int) { |
505 | frames := time * frame_rate |
506 | app.msg_hide_tick = if time > 0 { int(app.ui.frame_count) + frames } else { -1 } |
507 | app.msg = text |
508 | } |