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