v / examples / term.ui
Raw file | 508 loc (487 sloc) | 11.57 KB | Latest commit hash 868908b80
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 (
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
77const (
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
91struct App {
92mut:
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
109struct Point {
110mut:
111 x int
112 y int
113}
114
115fn 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
131fn 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
143fn 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
296fn (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
307fn (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
323fn (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
335fn (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
352fn (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
377fn (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
409fn (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
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}