1 // Copyright (c) 2015 Sergio Gonzalez. All rights reserved.
2 // License: https://github.com/serge-rgb/milton#license
3
4
5 #include "gui.h"
6
7 #include "localization.h"
8 #include "color.h"
9 #include "renderer.h"
10 #include "milton.h"
11 #include "persist.h"
12 #include "platform.h"
13
14
15 #define NUM_BUTTONS 5
16 #define BOUNDS_RADIUS_PX 80
17
18 // If reset_gui is true, the default window position and size will be set.
19 void
gui_layer_window(MiltonInput * input,PlatformState * platform,Milton * milton,f32 brush_window_height,PlatformSettings * prefs,b32 reset_gui)20 gui_layer_window(MiltonInput* input, PlatformState* platform, Milton* milton, f32 brush_window_height, PlatformSettings* prefs, b32 reset_gui)
21 {
22 float ui_scale = milton->gui->scale;
23 MiltonGui* gui = milton->gui;
24 const Rect pbounds = get_bounds_for_picker_and_colors(&gui->picker);
25 CanvasState* canvas = milton->canvas;
26
27 // Layer window
28
29 // Use default size on first program start on this computer.
30 f32 left = ui_scale*10;
31 f32 top = ui_scale*20 + (float)pbounds.bottom + brush_window_height;
32 f32 width = ui_scale*300;
33 f32 height = ui_scale*230;
34 if ( reset_gui ) {
35 ImGui::SetNextWindowPos(ImVec2(left, top));
36 ImGui::SetNextWindowSize(ImVec2(width, height));
37 }
38 else {
39 if ( prefs->layer_window_width != 0 && prefs->layer_window_height != 0 ) {
40 // If there are preferences already, use those for the layer window.
41 left = prefs->layer_window_left;
42 top = prefs->layer_window_top;
43 width = prefs->layer_window_width;
44 height = prefs->layer_window_height;
45 }
46
47 ImGui::SetNextWindowPos(ImVec2(left, top), ImGuiSetCond_FirstUseEver);
48 ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiSetCond_FirstUseEver);
49 }
50
51 if ( ImGui::Begin(loc(TXT_layers)) ) {
52 i32 angle = (milton->view->angle / PI) * 180;
53 while (angle < 0) { angle += 360; }
54 while (angle > 360) { angle -= 360; }
55 if (ImGui::SliderInt(loc(TXT_rotation), &angle, 0.0f, 360)) {
56 milton->view->angle = (f32)angle / 180.0f * PI;
57 input->flags |= (i32)MiltonInputFlags_PANNING;
58 gpu_update_canvas(milton->renderer, milton->canvas, milton->view);
59 }
60
61 CanvasView* view = milton->view;
62 // left
63 ImGui::BeginChild("left pane", ImVec2(150, 0), true);
64
65 static b32 is_renaming = false;
66 static i32 layer_renaming_idx = -1;
67 static b32 focus_rename_field = false;
68
69
70 Layer* layer = milton->canvas->root_layer;
71 while ( layer->next ) { layer = layer->next; } // Move to the top layer.
72 while ( layer ) {
73
74 bool v = layer->flags & LayerFlags_VISIBLE;
75 ImGui::PushID(layer->id);
76
77 if ( ImGui::Checkbox("##select", &v) ) {
78 layer::layer_toggle_visibility(layer);
79 input->flags |= (i32)MiltonInputFlags_FULL_REFRESH;
80 }
81
82 ImGui::PopID();
83 ImGui::SameLine();
84
85 // Draw the layers list. If in renaming mode, draw the layer that's being renamed as an InputText.
86 // Else just draw them as a list of Selectables.
87 if ( !ImGui::IsWindowFocused() ) {
88 is_renaming = false;
89 }
90
91 if ( is_renaming ) {
92 if ( layer->id == layer_renaming_idx ) {
93 if ( focus_rename_field ) {
94 ImGui::SetKeyboardFocusHere(0);
95 focus_rename_field = false;
96 }
97 if ( ImGui::InputText("##rename",
98 milton->canvas->working_layer->name,
99 13,
100 //MAX_LAYER_NAME_LEN,
101 ImGuiInputTextFlags_EnterReturnsTrue
102 //,ImGuiInputTextFlags flags = 0, ImGuiTextEditCallback callback = NULL, void* user_data = NULL
103 )) {
104 is_renaming = false;
105 }
106 }
107 else {
108 if ( ImGui::Selectable(layer->name,
109 milton->canvas->working_layer == layer,
110 ImGuiSelectableFlags_AllowDoubleClick) ) {
111 if ( ImGui::IsMouseDoubleClicked(0) ) {
112 layer_renaming_idx = layer->id;
113 focus_rename_field = true;
114 }
115 is_renaming = false;
116 milton_set_working_layer(milton, layer);
117 }
118 }
119 }
120 else {
121 if ( ImGui::Selectable(layer->name,
122 milton->canvas->working_layer == layer,
123 ImGuiSelectableFlags_AllowDoubleClick) ) {
124 if ( ImGui::IsMouseDoubleClicked(0) ) {
125 layer_renaming_idx = layer->id;
126 is_renaming = true;
127 focus_rename_field = true;
128 }
129 milton_set_working_layer(milton, layer);
130 }
131 }
132
133 layer = layer->prev;
134 }
135 ImGui::EndChild();
136 ImGui::SameLine();
137
138 ImGui::BeginGroup();
139 ImGui::BeginChild("item view", ImVec2(0, 25));
140 if ( ImGui::Button(loc(TXT_new_layer)) ) {
141 milton_new_layer(milton);
142 }
143 ImGui::SameLine();
144
145
146 ImGui::Separator();
147 ImGui::EndChild();
148 ImGui::BeginChild("buttons");
149
150 ImGui::Text(loc(TXT_move));
151
152 Layer* a = NULL;
153 Layer* b = NULL;
154 if ( ImGui::Button(loc(TXT_up)) ) {
155 b = milton->canvas->working_layer;
156 a = b->next;
157 }
158 ImGui::SameLine();
159 if ( ImGui::Button(loc(TXT_down)) ) {
160 a = milton->canvas->working_layer;
161 b = a->prev;
162 }
163
164
165 if ( a && b ) {
166 // n <-> a <-> b <-> p
167 // n <-> b <-> a <-> p
168 Layer* n = a->next;
169 Layer* p = b->prev;
170 b->next = n;
171 if ( n ) n->prev = b;
172 a->prev = p;
173 if ( p ) p->next = a;
174
175 a->next = b;
176 b->prev = a;
177
178 // Make sure root is first
179 while ( milton->canvas->root_layer->prev ) {
180 milton->canvas->root_layer = milton->canvas->root_layer->prev;
181 }
182 input->flags |= (i32)MiltonInputFlags_FULL_REFRESH;
183 }
184
185 if ( milton->canvas->working_layer->next
186 || milton->canvas->working_layer->prev ) {
187 static bool deleting = false;
188 if ( deleting == false ) {
189 if ( ImGui::Button(loc(TXT_delete)) ) {
190 deleting = true;
191 }
192 }
193 else if ( deleting ) {
194 ImGui::Text(loc(TXT_are_you_sure));
195 ImGui::Text(loc(TXT_cant_be_undone));
196 if ( ImGui::Button(loc(TXT_yes)) ) {
197 milton_delete_working_layer(milton);
198 input->flags |= MiltonInputFlags_FULL_REFRESH;
199 deleting = false;
200 }
201 ImGui::SameLine();
202 if ( ImGui::Button(loc(TXT_no)) ) {
203 deleting = false;
204 }
205 }
206 }
207 static b32 show_effects = false;
208 {
209 // ImGui::GetWindow(Pos|Size) works in here because we are inside Begin()/End() calls.
210 {
211 ImGui::Text(loc(TXT_opacity));
212 f32 alpha = canvas->working_layer->alpha;
213 if ( ImGui::SliderFloat("##opacity", &alpha, 0.0f, 1.0f) ) {
214 // Used the slider. Ask if it's OK to convert the binary format.
215 if ( milton->persist->mlt_binary_version < 3 ) {
216 milton_log("Modified milton file from %d to 3\n", milton->persist->mlt_binary_version);
217 milton->persist->mlt_binary_version = 3;
218 }
219 input->flags |= (i32)MiltonInputFlags_FULL_REFRESH;
220
221 alpha = clamp(alpha, 0.0f, 1.0f);
222
223 canvas->working_layer->alpha = alpha;
224 }
225
226 static b32 selecting = false;
227
228 ImGui::Separator();
229
230 if ( ImGui::Button(loc(TXT_blur)) ) {
231 LayerEffect* e = arena_alloc_elem(&milton->canvas_arena, LayerEffect);
232 e->next = milton->canvas->working_layer->effects;
233 milton->canvas->working_layer->effects = e;
234 e->enabled = true;
235 e->blur.original_scale = milton->view->scale;
236 e->blur.kernel_size = 10;
237 input->flags |= (i32)MiltonInputFlags_FULL_REFRESH;
238 }
239
240 LayerEffect* prev = NULL;
241 int effect_id = 1;
242 for ( LayerEffect* e = milton->canvas->working_layer->effects; e != NULL; e = e->next ) {
243 ImGui::PushID(effect_id);
244 static bool v = 0;
245 if ( ImGui::Checkbox(loc(TXT_enabled), (bool*)&e->enabled) ) {
246 input->flags |= MiltonInputFlags_FULL_REFRESH;
247 }
248 if ( ImGui::SliderInt(loc(TXT_level), &e->blur.kernel_size, 2, 100, 0) ) {
249 if (e->blur.kernel_size % 2 == 0) {
250 --e->blur.kernel_size;
251 }
252 input->flags |= MiltonInputFlags_FULL_REFRESH;
253 }
254 {
255 if (ImGui::Button(loc(TXT_delete_blur))) {
256 if (prev) {
257 prev->next = e->next;
258 } else { // Was the first.
259 milton->canvas->working_layer->effects = e->next;
260 }
261 input->flags |= (i32)MiltonInputFlags_FULL_REFRESH;
262 }
263 }
264 ImGui::PopID();
265 prev = e;
266 ImGui::Separator();
267 effect_id++;
268 }
269 // ImGui::Slider
270 }
271 }
272 ImGui::EndChild();
273 ImGui::EndGroup();
274
275 // Remember the current window layout for the next time the program runs.
276 ImVec2 pos = ImGui::GetWindowPos();
277 ImVec2 size = ImGui::GetWindowSize();
278 prefs->layer_window_left = pos.x;
279 prefs->layer_window_top = pos.y;
280 prefs->layer_window_width = size.x;
281 prefs->layer_window_height = size.y;
282
283 } ImGui::End();
284 }
285
286
287 // gui_brush_window returns the height of the rendered brush tool window. This can be used to position other windows below it.
288 // If reset_gui is true, the default window position and size will be set.
289 i32
gui_brush_window(MiltonInput * input,PlatformState * platform,Milton * milton,PlatformSettings * prefs,b32 reset_gui)290 gui_brush_window(MiltonInput* input, PlatformState* platform, Milton* milton, PlatformSettings* prefs, b32 reset_gui)
291 {
292 b32 show_brush_window = (current_mode_is_for_drawing(milton));
293 auto imgui_window_flags = ImGuiWindowFlags_NoCollapse;
294 MiltonGui* gui = milton->gui;
295
296 const Rect pbounds = get_bounds_for_picker_and_colors(&gui->picker);
297
298 // Use default size on first program start on this computer.
299 f32 left = milton->gui->scale * 10;
300 f32 top = milton->gui->scale * 10 + (float)pbounds.bottom;
301 f32 width = milton->gui->scale * 300;
302 f32 height = milton->gui->scale * 230;
303 if ( reset_gui ) {
304 ImGui::SetNextWindowPos(ImVec2(left, top));
305 ImGui::SetNextWindowSize({width, height});
306 }
307 else {
308 if ( prefs->brush_window_width != 0 && prefs->brush_window_height ) {
309 // If there are preferences already, use those for the layer window.
310 left = prefs->brush_window_left;
311 top = prefs->brush_window_top;
312 width = prefs->brush_window_width;
313 height = prefs->brush_window_height;
314 }
315
316 ImGui::SetNextWindowPos(ImVec2(left, top), ImGuiSetCond_FirstUseEver);
317 ImGui::SetNextWindowSize({width, height}, ImGuiSetCond_FirstUseEver);
318 }
319
320 // Brush Window
321 if ( show_brush_window ) {
322 if ( ImGui::Begin(loc(TXT_brushes), NULL, imgui_window_flags) ) {
323 if ( milton->current_mode == MiltonMode::PEN ||
324 milton->current_mode == MiltonMode::PRIMITIVE ) {
325 const float pen_alpha = milton_get_brush_alpha(milton);
326 mlt_assert(pen_alpha >= 0.0f && pen_alpha <= 1.0f);
327 float mut_alpha = pen_alpha*100;
328 ImGui::SliderFloat(loc(TXT_opacity), &mut_alpha, 1, 100, "%.0f%%");
329
330 mut_alpha /= 100.0f;
331 if (mut_alpha > 1.0f ) mut_alpha = 1.0f;
332 if ( mut_alpha != pen_alpha ) {
333 milton_set_brush_alpha(milton, mut_alpha);
334 gui->flags |= (i32)MiltonGuiFlags_SHOWING_PREVIEW;
335 }
336 }
337 const auto size = milton_get_brush_radius(milton);
338 auto mut_size = size;
339
340
341 if (ImGui::CheckboxFlags(loc(TXT_size_relative_to_canvas),
342 reinterpret_cast<u32*>(&milton->working_stroke.flags),
343 StrokeFlag_RELATIVE_TO_CANVAS)) {
344 // Just set it to be relative...
345 }
346
347 ImGui::SliderInt(loc(TXT_brush_size), &mut_size, 1, MILTON_MAX_BRUSH_SIZE);
348
349 if ( mut_size != size ) {
350 milton_set_brush_size(milton, mut_size);
351 milton->gui->flags |= (i32)MiltonGuiFlags_SHOWING_PREVIEW;
352 }
353
354 if ( milton->current_mode != MiltonMode::PEN ) {
355 if ( ImGui::Button(loc(TXT_switch_to_brush)) ) {
356 input->mode_to_set = MiltonMode::PEN;
357 }
358 }
359
360 if ( milton->current_mode != MiltonMode::PRIMITIVE ) {
361 if ( ImGui::Button(loc(TXT_switch_to_primitive)) ) {
362 input->mode_to_set = MiltonMode::PRIMITIVE;
363 }
364 }
365
366 if ( milton->current_mode != MiltonMode::ERASER ) {
367 if ( ImGui::Button(loc(TXT_switch_to_eraser)) ) {
368 input->mode_to_set = MiltonMode::ERASER;
369 }
370 }
371 }
372
373 {
374 if (!(milton->working_stroke.flags & StrokeFlag_ERASER)) {
375 ImGui::CheckboxFlags(loc(TXT_opacity_pressure), reinterpret_cast<u32*>(&milton->working_stroke.flags), StrokeFlag_PRESSURE_TO_OPACITY);
376 if (milton->working_stroke.flags & StrokeFlag_PRESSURE_TO_OPACITY) {
377 int brush_enum = milton_get_brush_enum(milton);
378 f32* min_opacity = &milton->brushes[brush_enum].pressure_opacity_min;
379
380 ImGui::SliderFloat(loc(TXT_minimum), min_opacity, 0.0f, milton->brushes[brush_enum].alpha);
381 }
382 ImGui::CheckboxFlags(loc(TXT_soft_brush), reinterpret_cast<u32*>(&milton->working_stroke.flags), StrokeFlag_DISTANCE_TO_OPACITY);
383 if (milton->working_stroke.flags & StrokeFlag_DISTANCE_TO_OPACITY) {
384 int brush_enum = milton_get_brush_enum(milton);
385 f32* hardness = &milton->brushes[brush_enum].hardness;
386
387 ImGui::SliderFloat(loc(TXT_hardness), hardness, 1.0f, k_max_hardness);
388 }
389
390 }
391 }
392
393 // Important to place this before ImGui::End(), position and size will be invalid after it.
394 ImVec2 pos = ImGui::GetWindowPos();
395 ImVec2 size = ImGui::GetWindowSize();
396 // Remember the current window layout for the next time the program runs.
397 prefs->brush_window_left = pos.x;
398 prefs->brush_window_top = pos.y;
399 prefs->brush_window_width = size.x;
400 prefs->brush_window_height = size.y;
401 ImGui::End(); // Brushes
402 if (( milton->gui->flags & MiltonGuiFlags_SHOWING_PREVIEW )) {
403 milton->gui->preview_pos = {
404 (i32)(pos.x + size.x + milton_get_brush_radius(milton)),
405 (i32)(pos.y),
406 };
407 }
408 }
409
410 return height;
411 }
412
413 void
gui_menu(MiltonInput * input,PlatformState * platform,Milton * milton,b32 & show_settings,b32 * reset_gui)414 gui_menu(MiltonInput* input, PlatformState* platform, Milton* milton, b32& show_settings, b32* reset_gui)
415 {
416 // Menu ----
417 int menu_style_stack = 0;
418
419 MiltonGui* gui = milton->gui;
420 CanvasState* canvas = milton->canvas;
421
422 // TODO: translate
423
424 if ( gui->menu_visible ) {
425 if ( ImGui::BeginMainMenuBar() ) {
426 if ( ImGui::BeginMenu(loc(TXT_file)) ) {
427 if ( ImGui::MenuItem(loc(TXT_new_milton_canvas)) ) {
428 b32 save_file = false;
429 if ( layer::count_strokes(milton->canvas->root_layer) > 0 ) {
430 if ( milton->flags & MiltonStateFlags_DEFAULT_CANVAS ) {
431 save_file = platform_dialog_yesno(loc(TXT_default_will_be_cleared), "Save?");
432 }
433 }
434 if ( save_file ) {
435 PATH_CHAR* name = platform_save_dialog(FileKind_MILTON_CANVAS);
436 if ( name ) {
437 milton_log("Saving to %s\n", name);
438 milton_set_canvas_file(milton, name);
439 milton_save(milton);
440 b32 del = platform_delete_file_at_config(TO_PATH_STR("MiltonPersist.mlt"), DeleteErrorTolerance_OK_NOT_EXIST);
441 if ( del == false ) {
442 platform_dialog("Could not delete contents. The work will be still be there even though you saved it to a file.",
443 "Info");
444 }
445 }
446 }
447
448 // New Canvas
449 milton_reset_canvas_and_set_default(milton);
450 canvas = milton->canvas;
451 input->flags |= MiltonInputFlags_FULL_REFRESH;
452 milton->flags |= MiltonStateFlags_DEFAULT_CANVAS;
453 }
454 b32 save_requested = false;
455 if ( ImGui::MenuItem(loc(TXT_open_milton_canvas)) ) {
456 // If current canvas is MiltonPersist, then prompt to save
457 if ( ( milton->flags & MiltonStateFlags_DEFAULT_CANVAS ) ) {
458 b32 save_file = false;
459 if ( layer::count_strokes(milton->canvas->root_layer) > 0 ) {
460 save_file = platform_dialog_yesno(loc(TXT_default_will_be_cleared), "Save?");
461 }
462 if ( save_file ) {
463 PATH_CHAR* name = platform_save_dialog(FileKind_MILTON_CANVAS);
464 if ( name ) {
465 milton_log("Saving to %s\n", name);
466 milton_set_canvas_file(milton, name);
467 milton_save(milton);
468 b32 del = platform_delete_file_at_config(TO_PATH_STR("MiltonPersist.mlt"),
469 DeleteErrorTolerance_OK_NOT_EXIST);
470 if ( del == false ) {
471 platform_dialog("Could not delete default canvas. Contents will be still there when you create a new canvas.",
472 "Info");
473 }
474 }
475 }
476 }
477 PATH_CHAR* fname = platform_open_dialog(FileKind_MILTON_CANVAS);
478 if ( fname ) {
479 milton_set_canvas_file(milton, fname);
480 input->flags |= MiltonInputFlags_OPEN_FILE;
481 }
482 }
483 if ( ImGui::MenuItem(loc(TXT_save_milton_canvas_as_DOTS)) || save_requested ) {
484 // NOTE(possible refactor): There is a copy of this at milton.c end of file
485 PATH_CHAR* name = platform_save_dialog(FileKind_MILTON_CANVAS);
486 if ( name ) {
487 milton_log("Saving to %s\n", name);
488 milton_set_canvas_file(milton, name);
489 input->flags |= MiltonInputFlags_SAVE_FILE;
490 b32 del = platform_delete_file_at_config(TO_PATH_STR("MiltonPersist.mlt"),
491 DeleteErrorTolerance_OK_NOT_EXIST);
492 if ( del == false ) {
493 platform_dialog(loc(TXT_could_not_delete_default_canvas),
494 "Info");
495 }
496 }
497 }
498 if ( ImGui::MenuItem(loc(TXT_export_to_image_DOTS)) ) {
499 milton_enter_mode(milton, MiltonMode::EXPORTING);
500 }
501 if ( ImGui::MenuItem(loc(TXT_settings)) && !show_settings ) {
502 milton_set_gui_visibility(milton, true);
503 show_settings = true;
504 *gui->original_settings = *milton->settings;
505 for (sz i = Action_FIRST; i < Action_COUNT; ++i) {
506 gui->scratch_binding_key[i][0] = gui->original_settings->bindings.bindings[i].bound_key;
507 }
508 }
509 if ( ImGui::MenuItem(loc(TXT_quit)) ) {
510 milton_try_quit(milton);
511 }
512 ImGui::EndMenu();
513 } // file menu
514 if ( ImGui::BeginMenu(loc(TXT_edit)) ) {
515 if ( ImGui::MenuItem(loc(TXT_undo) ) ) {
516 input->flags |= MiltonInputFlags_UNDO;
517 }
518 if ( ImGui::MenuItem(loc(TXT_redo)) ) {
519 input->flags |= MiltonInputFlags_REDO;
520 }
521 ImGui::EndMenu();
522 }
523
524 if ( ImGui::BeginMenu(loc(TXT_canvas), /*enabled=*/true) ) {
525 if ( ImGui::BeginMenu(loc(TXT_set_background_color)) ) {
526 v3f bg = milton->view->background_color;
527 if (ImGui::ColorEdit3(loc(TXT_color), bg.d))
528 {
529 milton_set_background_color(milton, clamp_01(bg));
530 input->flags |= (i32)MiltonInputFlags_FULL_REFRESH;
531 }
532 ImGui::EndMenu();
533 }
534 if ( ImGui::MenuItem(loc(TXT_zoom_in)) ) {
535 input->scale++;
536 milton_set_zoom_at_screen_center(milton);
537 }
538 if ( ImGui::MenuItem(loc(TXT_zoom_out)) ) {
539 input->scale--;
540 milton_set_zoom_at_screen_center(milton);
541 }
542 ImGui::EndMenu();
543 }
544 if ( ImGui::BeginMenu(loc(TXT_tools)) ) {
545 // Brush
546 if ( ImGui::MenuItem(loc(TXT_brush)) ) {
547 input->mode_to_set = MiltonMode::PEN;
548 }
549 if ( ImGui::BeginMenu(loc(TXT_brush_options)) ) {
550
551 // Toggling brush smoothing is barely noticeable...
552
553 // b32 smoothing_enabled = milton_brush_smoothing_enabled(milton);
554 // char* entry_str = smoothing_enabled? loc(TXT_disable_stroke_smoothing) : loc(TXT_enable_stroke_smoothing);
555
556 // if ( ImGui::MenuItem(entry_str) ) {
557 // milton_toggle_brush_smoothing(milton);
558 // }
559
560 // Decrease / increase brush size
561 if ( ImGui::MenuItem(loc(TXT_decrease_brush_size)) ) {
562 for (int i=0;i<5;++i) milton_decrease_brush_size(milton);
563 }
564 if ( ImGui::MenuItem(loc(TXT_increase_brush_size)) ) {
565 for (int i=0;i<5;++i) milton_increase_brush_size(milton);
566 }
567 // Opacity shortcuts
568
569 f32 opacities[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f };
570 for ( i32 i = 0; i < array_count(opacities); ++i ) {
571 char entry[128] = {};
572 snprintf(entry, array_count(entry), "%s %d%% - [%d]",
573 loc(TXT_set_opacity_to), (int)(100 * opacities[i]), i == 9 ? 0 : i+1);
574 if ( ImGui::MenuItem(entry) ) {
575 milton_set_brush_alpha(milton, opacities[i]);
576 }
577 }
578 ImGui::EndMenu();
579 }
580 // Eraser
581 if ( ImGui::MenuItem(loc(TXT_eraser)) ) {
582 input->mode_to_set = MiltonMode::ERASER;
583 }
584 // Panning
585 char* move_str = platform->is_panning==false? loc(TXT_move_canvas) : loc(TXT_stop_moving_canvas);
586 if ( ImGui::MenuItem(move_str) ) {
587 platform->waiting_for_pan_input = true;
588 }
589 // Eye Dropper
590 if ( ImGui::MenuItem(loc(TXT_eye_dropper)) ) {
591 input->mode_to_set = MiltonMode::EYEDROPPER;
592 }
593 ImGui::EndMenu();
594 }
595 if ( ImGui::BeginMenu(loc(TXT_view)) ) {
596 if ( ImGui::MenuItem(loc(TXT_toggle_gui_visibility)) ) {
597 milton_toggle_gui_visibility(milton);
598 }
599
600 if ( ImGui::MenuItem(loc(TXT_peek_out)) ) {
601 peek_out_trigger_start(milton, PeekOut_CLICK_TO_EXIT);
602 }
603
604 if ( ImGui::MenuItem(loc(TXT_reset_view_at_origin)) ) {
605 reset_transform_at_origin(
606 &milton->view->pan_center,
607 &milton->view->scale,
608 &milton->view->angle);
609 gpu_update_canvas(milton->renderer, milton->canvas, milton->view);
610 }
611
612 if ( ImGui::MenuItem(loc(TXT_reset_GUI)) ) {
613 *reset_gui = true;
614 }
615
616 #if MILTON_ENABLE_PROFILING
617 if ( ImGui::MenuItem("Toggle Debug Data [BACKQUOTE]") ) {
618 milton->viz_window_visible = !milton->viz_window_visible;
619 }
620 #endif
621 ImGui::EndMenu();
622 }
623 if ( ImGui::BeginMenu(loc(TXT_help)) ) {
624 if ( ImGui::MenuItem(loc(TXT_help_me)) ) {
625 //platform_open_link("https://www.youtube.com/watch?v=g27gHio2Ohk");
626 platform_open_link("http://www.miltonpaint.com/help/");
627 }
628 if ( ImGui::MenuItem(loc(TXT_milton_version)) ) {
629 char buffer[1024];
630 snprintf(buffer, array_count(buffer),
631 "Milton version %d.%d.%d",
632 MILTON_MAJOR_VERSION, MILTON_MINOR_VERSION, MILTON_MICRO_VERSION);
633 platform_dialog(buffer, "Milton Version");
634 }
635 if ( ImGui::MenuItem(loc(TXT_website)) ) {
636 platform_open_link("http://miltonpaint.com");
637 }
638 ImGui::EndMenu();
639 }
640 PATH_CHAR* utf16_name = str_trim_to_last_slash(milton->persist->mlt_file_path);
641
642 char file_name[MAX_PATH] = {};
643 utf16_to_utf8_simple(utf16_name, file_name);
644
645 char msg[1024];
646 WallTime lst = milton->persist->last_save_time;
647
648 // TODO: Translate!
649 snprintf(msg, 1024, "\t%s -- Last saved: %.2d:%.2d:%.2d\t\tZoom level %.2f",
650 (milton->flags & MiltonStateFlags_DEFAULT_CANVAS) ? loc(TXT_OPENBRACKET_default_canvas_CLOSE_BRACKET):
651 file_name,
652 lst.hours, lst.minutes, lst.seconds,
653 // We divide by MILTON_DEFAULT_SCALE to give a frame of
654 // reference to the user, where 1.0 is the default. For
655 // our calculations in other places, we don't do the
656 // divide.
657 log2(1 + milton->view->scale / (double)MILTON_DEFAULT_SCALE) / log2(SCALE_FACTOR));
658
659 if ( ImGui::BeginMenu(msg, /*bool enabled = */false) ) {
660 ImGui::EndMenu();
661 }
662 ImGui::EndMainMenuBar();
663 }
664 ImGui::PopStyleColor(menu_style_stack);
665 }
666 }
667
668
669 void
milton_imgui_tick(MiltonInput * input,PlatformState * platform,Milton * milton,PlatformSettings * prefs)670 milton_imgui_tick(MiltonInput* input, PlatformState* platform, Milton* milton, PlatformSettings* prefs)
671 {
672 CanvasState* canvas = milton->canvas;
673 MiltonGui* gui = milton->gui;
674 // ImGui Section
675
676 // Spawn below the picker
677 const Rect pbounds = get_bounds_for_picker_and_colors(&gui->picker);
678
679 int color_stack = 0;
680
681 static auto color_window_background = ImVec4{.929f, .949f, .957f, 1};
682 //static auto color_title_bg = ImVec4{.957f,.353f, .286f,1};
683 static auto color_title_bg = color_window_background;
684 static auto color_title_fg = ImVec4{151/255.f, 184/255.f, 210/255.f, 1};
685
686 static auto color_buttons = ImVec4{.686f, .796f, 1.0f, 1};
687 static auto color_buttons_active = ImVec4{.886f, .796f, 1.0f, 1};
688 static auto color_buttons_hovered = ImVec4{.706f, .816f, 1.0f, 1};
689
690 static auto color_menu_bg = ImVec4{.784f, .392f, .784f, 1};
691 static auto color_text = ImVec4{.2f,.2f,.2f,1};
692 static auto color_slider = ImVec4{ 148/255.f, 182/255.f, 182/255.f,1};
693 static auto frame_background = ImVec4{ 0.862745f, 0.862745f, 0.862745f,1};
694 static auto color_text_selected = ImVec4{ 0.509804f, 0.627451f, 0.823529f,1};
695 static auto color_header_hovered = color_buttons;
696
697 // Helper Imgui code to select color scheme
698 #if 0
699 ImGui::ColorEdit3("Window Background", (float*)&color_window_background);
700 ImGui::ColorEdit3("Title background", (float*)&color_title_bg);
701 ImGui::ColorEdit3("Title background active", (float*)&color_title_fg);
702
703 ImGui::ColorEdit3("Buttons", (float*)&color_buttons);
704 ImGui::ColorEdit3("Menu BG", (float*)&color_menu_bg);
705 ImGui::ColorEdit3("text", (float*)&color_text);
706 ImGui::ColorEdit3("frame background", (float*)&frame_background);
707 ImGui::ColorEdit3("selected", (float*)&color_text_selected);
708 if ( ImGui::Button("Print out") ) {
709 auto print_color = [&](char* label, ImVec4 c) {
710 milton_log("%s : %f, %f, %f \n", label, c.x, c.y, c.z);
711 };
712 print_color("window bg", color_window_background);
713 print_color("title bg", color_title_bg);
714 print_color("buttons", color_buttons);
715 print_color("menu bg", color_menu_bg);
716 print_color("text", color_text);
717 print_color("selected", frame_background);
718 print_color("selected", color_text_selected);
719 }
720 #endif
721
722 ImGui::PushStyleColor(ImGuiCol_WindowBg, color_window_background); ++color_stack;
723 ImGui::PushStyleColor(ImGuiCol_PopupBg, color_window_background); ++color_stack;
724 ImGui::PushStyleColor(ImGuiCol_TitleBg, color_title_bg); ++color_stack;
725 ImGui::PushStyleColor(ImGuiCol_TitleBgActive, color_title_fg); ++color_stack;
726 ImGui::PushStyleColor(ImGuiCol_Text, color_text); ++color_stack;
727 ImGui::PushStyleColor(ImGuiCol_MenuBarBg, color_title_bg); ++color_stack;
728 ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, color_buttons_active); ++color_stack;
729 ImGui::PushStyleColor(ImGuiCol_FrameBg, frame_background); ++color_stack;
730
731 ImGui::PushStyleColor(ImGuiCol_SliderGrab, color_buttons); ++color_stack;
732 ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, color_buttons_active); ++color_stack;
733
734 ImGui::PushStyleColor(ImGuiCol_Button, color_buttons); ++color_stack;
735 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, color_buttons_hovered); ++color_stack;
736 ImGui::PushStyleColor(ImGuiCol_ButtonActive, color_buttons_active); ++color_stack;
737
738 ImGui::PushStyleColor(ImGuiCol_Header, color_buttons); ++color_stack;
739 ImGui::PushStyleColor(ImGuiCol_HeaderHovered, color_buttons_hovered); ++color_stack;
740 ImGui::PushStyleColor(ImGuiCol_HeaderActive, color_buttons_active); ++color_stack;
741
742 ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, color_buttons); ++color_stack;
743 ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, color_buttons_hovered); ++color_stack;
744 ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, color_buttons_active); ++color_stack;
745
746 ImGui::PushStyleColor(ImGuiCol_CheckMark, color_slider); ++color_stack;
747
748
749 static b32 show_settings = false;
750 b32 reset_gui = false;
751
752 gui_menu(input, platform, milton, show_settings, &reset_gui);
753
754 float ui_scale = milton->gui->scale;
755
756 // GUI Windows ----
757
758 b32 should_show_windows = milton->current_mode != MiltonMode::EYEDROPPER
759 && milton->current_mode != MiltonMode::EXPORTING
760 && milton->current_mode != MiltonMode::HISTORY;
761
762 if ( gui->visible && should_show_windows ) {
763 i32 brush_window_height = gui_brush_window(input, platform, milton, prefs, reset_gui);
764
765 gui_layer_window(input, platform, milton, brush_window_height, prefs, reset_gui);
766
767 // Settings window
768 if ( show_settings ) {
769 ImGui::SetNextWindowSize(ImVec2(ui_scale*400, ui_scale*400),
770 ImGuiSetCond_FirstUseEver);
771 if ( ImGui::Begin(loc(TXT_settings)) ) {
772 if (ImGui::Button(loc(TXT_ok))) {
773 milton_settings_save(milton->settings);
774 show_settings = false;
775 }
776
777 ImGui::SameLine();
778 if (ImGui::Button(loc(TXT_cancel))) {
779 show_settings = false;
780 *milton->settings = *gui->original_settings;
781 }
782
783 ImGui::Separator();
784 ImGui::BeginChild("ScrollRegion");
785
786 ImGui::Text(loc(TXT_default_background_color));
787
788 v3f* bg = &milton->settings->background_color;
789 if (ImGui::ColorEdit3(loc(TXT_color), bg->d)) {
790 // TODO: Let milton know that we need to save the settings
791 }
792
793 if ( ImGui::Button(loc(TXT_set_current_background_color_as_default)) ) {
794 milton->settings->background_color = milton->view->background_color;
795 }
796
797 const float peek_range = 20;
798 int peek_out_percent = 100 * (milton->settings->peek_out_increment / peek_range);
799 if (ImGui::SliderInt(loc(TXT_peek_out_increment_percent), &peek_out_percent, 0, 100)) {
800 milton->settings->peek_out_increment = (peek_out_percent / 100.0f) * peek_range;
801 }
802
803 ImGui::Separator();
804
805 MiltonBindings* bs = &milton->settings->bindings;
806
807 for (sz i = Action_FIRST; i < Action_COUNT; ++i ) {
808 Binding* b = bs->bindings + i;
809
810 char control_lbl[64] = {};
811 snprintf(control_lbl, array_count(control_lbl), "control##%d", (int)i);
812
813 char alt_lbl[64] = {};
814 snprintf(alt_lbl, array_count(alt_lbl), "alt##%d", (int)i);
815
816 char win_lbl[64] = {};
817 snprintf(win_lbl, array_count(win_lbl), "win##%d", (int)i);
818
819
820 ImGui::CheckboxFlags(control_lbl, (unsigned int*)&b->modifiers, Modifier_CTRL);
821 ImGui::SameLine();
822 ImGui::CheckboxFlags(alt_lbl, (unsigned int*)&b->modifiers, Modifier_ALT);
823 ImGui::SameLine();
824 ImGui::CheckboxFlags(win_lbl, (unsigned int*)&b->modifiers, Modifier_WIN);
825 ImGui::SameLine();
826
827 ImGui::PushItemWidth(60);
828 // mlt_assert(TXT_Action_FIRST + (int)i < TXT_Count);
829 char* action_str = (char*)loc((Texts)(TXT_Action_FIRST + (int)i - Action_FIRST));
830 if (ImGui::InputText(action_str,
831 (char*)gui->scratch_binding_key[i],
832 sizeof(gui->scratch_binding_key[i]),
833 ImGuiInputTextFlags_AllowTabInput)) {
834 b->bound_key = gui->scratch_binding_key[i][0];
835 }
836 ImGui::PopItemWidth();
837 }
838
839 ImGui::EndChild();
840 } ImGui::End();
841 }
842
843 // History window
844 if ( milton->current_mode == MiltonMode::HISTORY ) {
845 {
846 Rect pb = picker_get_bounds(&gui->picker);
847 auto width = 20 + pb.right - pb.left;
848 ImGui::SetNextWindowPos(ImVec2(width, 30), ImGuiSetCond_FirstUseEver);
849 }
850 ImGui::SetNextWindowSize(ImVec2(ui_scale*500, ui_scale*100), ImGuiSetCond_FirstUseEver);
851 if ( ImGui::Begin("History Slider") ) {
852 ImGui::SliderInt("History", &gui->history, 0,
853 layer::count_strokes(milton->canvas->root_layer));
854 } ImGui::End();
855
856 }
857 } // Windows
858
859 // Note: The export window is drawn regardless of gui visibility.
860 if ( milton->current_mode == MiltonMode::EXPORTING ) {
861 bool opened = true;
862 b32 reset = false;
863
864 ImGui::SetNextWindowPos(ImVec2(100, 30), ImGuiSetCond_FirstUseEver);
865 ImGui::SetNextWindowSize({ui_scale*350, ui_scale*235}, ImGuiSetCond_FirstUseEver); // We don't want to set it *every* time, the user might have preferences
866
867 // Export window
868 if ( ImGui::Begin(loc(TXT_export_DOTS), &opened, ImGuiWindowFlags_NoCollapse) ) {
869 ImGui::Text(loc(TXT_MSG_click_and_drag_instruction));
870
871 Exporter* exporter = &milton->gui->exporter;
872 if ( exporter->state == ExporterState_SELECTED ) {
873 i32 x = min( exporter->needle.x, exporter->pivot.x );
874 i32 y = min( exporter->needle.y, exporter->pivot.y );
875 int raster_w = MLT_ABS(exporter->needle.x - exporter->pivot.x);
876 int raster_h = MLT_ABS(exporter->needle.y - exporter->pivot.y);
877
878 ImGui::Text("%s: %dx%d\n",
879 loc(TXT_current_selection),
880 raster_w, raster_h);
881 if (ImGui::InputInt(loc(TXT_scale_up), &exporter->scale, 1, /*step_fast=*/2)) {}
882 if ( exporter->scale <= 0 ) {
883 exporter->scale = 1;
884 }
885 float viewport_limits[2] = {};
886 gpu_get_viewport_limits(milton->renderer, viewport_limits);
887
888 while ( exporter->scale*raster_w > viewport_limits[0]
889 || exporter->scale*raster_h > viewport_limits[1] ) {
890 --exporter->scale;
891 }
892 i32 max_scale = milton->view->scale / 2;
893 if ( exporter->scale > max_scale) {
894 exporter->scale = max_scale;
895 }
896 ImGui::Text("%s: %dx%d\n", loc(TXT_final_image_resolution), raster_w*exporter->scale, raster_h*exporter->scale);
897
898 ImGui::Text(loc(TXT_background_COLON));
899 static int radio_v = 0;
900 ImGui::RadioButton(loc(TXT_background_color), &radio_v, 0);
901 ImGui::RadioButton(loc(TXT_transparent_background), &radio_v, 1);
902 bool transparent_background = radio_v == 1;
903
904 if ( ImGui::Button(loc(TXT_export_selection_to_image_DOTS)) ) {
905 // Render to buffer
906 int bpp = 4; // bytes per pixel
907 i32 w = raster_w * exporter->scale;
908 i32 h = raster_h * exporter->scale;
909 size_t size = (size_t)w * h * bpp;
910 u8* buffer = (u8*)mlt_calloc(1, size, "Bitmap");
911 if ( buffer ) {
912 opened = false;
913 gpu_render_to_buffer(milton, buffer, exporter->scale,
914 x,y, raster_w, raster_h, transparent_background ? 0.0f : 1.0f);
915 //milton_render_to_buffer(milton, buffer, x,y, raster_w, raster_h, exporter->scale);
916 PATH_CHAR* fname = platform_save_dialog(FileKind_IMAGE);
917 if ( fname ) {
918 milton_save_buffer_to_file(fname, buffer, w, h);
919 }
920 mlt_free (buffer, "Bitmap");
921 } else {
922 platform_dialog(loc(TXT_MSG_memerr_did_not_write), loc(TXT_error));
923 }
924 }
925 }
926 }
927
928 if ( ImGui::Button(loc(TXT_cancel)) ) {
929 reset = true;
930 milton_leave_mode(milton);
931 }
932 ImGui::End(); // Export...
933 if ( !opened ) {
934 reset = true;
935 milton_leave_mode(milton);
936 }
937 if ( reset ) {
938 exporter_init(&milton->gui->exporter);
939 }
940 } // exporting
941
942 #if MILTON_ENABLE_PROFILING
943 ImGui::SetNextWindowPos(ImVec2(ui_scale*300, ui_scale*205), ImGuiSetCond_FirstUseEver);
944 ImGui::SetNextWindowSize({ui_scale*350, ui_scale*285}, ImGuiSetCond_FirstUseEver); // We don't want to set it *every* time, the user might have preferences
945 if ( milton->viz_window_visible ) {
946 bool opened = true;
947 if ( ImGui::Begin("Debug Data ([BACKQUOTE] to toggle)", &opened, ImGuiWindowFlags_NoCollapse) ) {
948 float graph_height = 20;
949 char msg[512] = {};
950
951 float poll = perf_count_to_sec(milton->graph_frame.polling) * 1000.0f;
952 float update = perf_count_to_sec(milton->graph_frame.update) * 1000.0f;
953 float clipping = perf_count_to_sec(milton->graph_frame.clipping) * 1000.0f;
954 float raster = perf_count_to_sec(milton->graph_frame.raster) * 1000.0f;
955 float GL = perf_count_to_sec(milton->graph_frame.GL) * 1000.0f;
956 float system = perf_count_to_sec(milton->graph_frame.system) * 1000.0f;
957
958 float sum = poll + update + raster + GL + system;
959
960 snprintf(msg, array_count(msg),
961 "Input Polling %f ms\n",
962 poll);
963 ImGui::Text(msg);
964
965 snprintf(msg, array_count(msg),
966 "Milton Update %f ms\n",
967 update);
968 ImGui::Text(msg);
969
970 snprintf(msg, array_count(msg),
971 "Clipping & Update %f ms\n",
972 clipping);
973 ImGui::Text(msg);
974
975 snprintf(msg, array_count(msg),
976 "OpenGL commands %f ms\n",
977 GL);
978 ImGui::Text(msg);
979
980 snprintf(msg, array_count(msg),
981 "System %f ms \n",
982 system);
983 ImGui::Text(msg);
984
985 snprintf(msg, array_count(msg),
986 "Number of strokes in GPU memory: %d\n",
987 gpu_get_num_clipped_strokes(milton->canvas->root_layer));
988 ImGui::Text(msg);
989
990 float hist[] = { poll, update, raster, GL, system };
991 ImGui::PlotHistogram("Graph",
992 (const float*)hist, array_count(hist));
993
994 {
995 static const int window_size = 100;
996 static float moving_window[window_size] = {};
997 static int window_i = 0;
998
999 moving_window[window_i++] = sum;
1000 window_i %= window_size;
1001
1002 float mavg = 0.0f;
1003 for ( int i =0; i < window_size; ++i ) {
1004 mavg += moving_window[i];
1005 }
1006 mavg /= window_size;
1007
1008 snprintf(msg, array_count(msg),
1009 "Total %f ms (%f ms m. avg)\n",
1010 sum,
1011 mavg);
1012
1013 }
1014 ImGui::Text(msg);
1015
1016 ImGui::Dummy({0,30});
1017
1018 i64 stroke_count = layer::count_strokes(milton->canvas->root_layer);
1019
1020 auto* view = milton->view;
1021 int screen_height = view->screen_size.h * view->scale;
1022 int screen_width = view->screen_size.w * view->scale;
1023
1024 if ( screen_height>0 && screen_height>0 ) {
1025 v2l pan = view->pan_center;
1026
1027 i64 radius = ((i64)((1ull)<<63ull)-1);
1028
1029 {
1030 if ( pan.y > 0 ) {
1031 i64 n_screens_below = ((i64)(radius) - (i64)pan.y)/(i64)screen_height;
1032 snprintf(msg, array_count(msg),
1033 "Screens below: %I64d\n", n_screens_below);
1034 ImGui::Text(msg);
1035 } else {
1036 i64 n_screens_above = ((i64)(radius) + (i64)pan.y)/(i64)screen_height;
1037 snprintf(msg, array_count(msg),
1038 "Screens above: %I64d\n", n_screens_above);
1039 ImGui::Text(msg);
1040 }
1041 }
1042 {
1043 if ( pan.x > 0 ) {
1044 i64 n_screens_right = ((i64)(radius) - (i64)pan.x)/(i64)screen_width;
1045 snprintf(msg, array_count(msg),
1046 "Screens to the right: %I64d\n", n_screens_right);
1047 ImGui::Text(msg);
1048 } else {
1049 i64 n_screens_left = ((i64)(radius) + (i64)pan.x)/(i64)screen_width;
1050 snprintf(msg, array_count(msg),
1051 "Screens to the left: %I64d\n", n_screens_left);
1052 ImGui::Text(msg);
1053 }
1054 }
1055 }
1056 snprintf(msg, array_count(msg),
1057 "Scale: %lld", view->scale);
1058 ImGui::Text(msg);
1059
1060 // Average stroke size.
1061 i64 avg = 0;
1062 {
1063 for ( Layer* l = milton->canvas->root_layer;
1064 l != NULL;
1065 l = l->next ) {
1066 for ( i64 i = 0; i < l->strokes.count; ++i ) {
1067 Stroke* s = get(&l->strokes, i);
1068 avg += s->num_points;
1069 }
1070 }
1071 if ( stroke_count > 0 ) {
1072 avg /= stroke_count;
1073 }
1074 }
1075 snprintf(msg, array_count(msg),
1076 "Average stroke size: %" PRIi64, avg);
1077 ImGui::Text(msg);
1078
1079 } ImGui::End();
1080 } // profiling
1081 #endif
1082 ImGui::PopStyleColor(color_stack);
1083 }
1084
1085 static Rect
color_button_as_rect(const ColorButton * button)1086 color_button_as_rect(const ColorButton* button)
1087 {
1088 Rect rect = {};
1089 rect.left = button->x;
1090 rect.right = button->x + button->w;
1091 rect.top = button->y;
1092 rect.bottom = button->y + button->h;
1093 return rect;
1094 }
1095
1096 static void
picker_update_points(ColorPicker * picker,float angle)1097 picker_update_points(ColorPicker* picker, float angle)
1098 {
1099 picker->data.hsv.h = radians_to_degrees(angle);
1100 // Update the triangle
1101 float radius = 0.9f * (picker->wheel_radius - picker->wheel_half_width);
1102 v2f center = v2l_to_v2f(VEC2L(picker->center));
1103 {
1104 v2f point = polar_to_cartesian(-angle, radius);
1105 point = point + center;
1106 picker->data.c = point;
1107 }
1108 {
1109 v2f point = polar_to_cartesian(-angle + 2 * kPi / 3.0f, radius);
1110 point = point + center;
1111 picker->data.b = point;
1112 }
1113 {
1114 v2f point = polar_to_cartesian(-angle + 4 * kPi / 3.0f, radius);
1115 point = point + center;
1116 picker->data.a = point;
1117 }
1118 }
1119
1120
1121 static void
picker_update_wheel(ColorPicker * picker,v2f polar_point)1122 picker_update_wheel(ColorPicker* picker, v2f polar_point)
1123 {
1124 float angle = picker_wheel_get_angle(picker, polar_point);
1125 picker_update_points(picker, angle);
1126 }
1127
1128 static b32
picker_hits_triangle(ColorPicker * picker,v2f fpoint)1129 picker_hits_triangle(ColorPicker* picker, v2f fpoint)
1130 {
1131 b32 result = is_inside_triangle(fpoint, picker->data.a, picker->data.b, picker->data.c);
1132 return result;
1133 }
1134
1135 static void
picker_deactivate(ColorPicker * picker)1136 picker_deactivate(ColorPicker* picker)
1137 {
1138 picker->flags = ColorPickerFlags_NOTHING;
1139 }
1140
1141 static b32
is_inside_picker_rect(ColorPicker * picker,v2i point)1142 is_inside_picker_rect(ColorPicker* picker, v2i point)
1143 {
1144 return is_inside_rect(picker->bounds, point);
1145 }
1146
1147 static Rect
picker_color_buttons_bounds(const ColorPicker * picker)1148 picker_color_buttons_bounds(const ColorPicker* picker)
1149 {
1150 Rect bounds = {};
1151 bounds.right = INT_MIN;
1152 bounds.left = INT_MAX;
1153 bounds.top = INT_MAX;
1154 bounds.bottom = INT_MIN;
1155 const ColorButton* button = picker->color_buttons;
1156 while ( button ) {
1157 bounds = rect_union(bounds, color_button_as_rect(button));
1158 button = button->next;
1159 }
1160 return bounds;
1161 }
1162
1163 static b32
is_inside_picker_button_area(ColorPicker * picker,v2i point)1164 is_inside_picker_button_area(ColorPicker* picker, v2i point)
1165 {
1166 Rect button_rect = picker_color_buttons_bounds(picker);
1167 b32 is_inside = is_inside_rect(button_rect, point);
1168 return is_inside;
1169 }
1170
1171 b32
gui_point_hovers(MiltonGui * gui,v2i point)1172 gui_point_hovers(MiltonGui* gui, v2i point)
1173 {
1174 b32 hovers = gui->visible &&
1175 (is_inside_picker_rect(&gui->picker, point) ||
1176 is_inside_picker_button_area(&gui->picker, point));
1177 return hovers;
1178 }
1179
1180 void
gui_picker_from_rgb(ColorPicker * picker,v3f rgb)1181 gui_picker_from_rgb(ColorPicker* picker, v3f rgb)
1182 {
1183 v3f hsv = rgb_to_hsv(rgb);
1184 picker->data.hsv = hsv;
1185 float angle = hsv.h * 2*kPi;
1186 picker_update_points(picker, angle);
1187 }
1188
1189 static void
update_button_bounds(ColorPicker * picker,f32 ui_scale)1190 update_button_bounds(ColorPicker* picker, f32 ui_scale)
1191 {
1192 i32 bounds_radius_px = ui_scale*BOUNDS_RADIUS_PX;
1193
1194 i32 spacing = 4*ui_scale;
1195 i32 num_buttons = NUM_BUTTONS;
1196
1197 i32 button_size = (2*bounds_radius_px - (num_buttons - 1) * spacing) / num_buttons;
1198 i32 current_x = ui_scale*40 - button_size / 2;
1199
1200 for ( ColorButton* cur_button = picker->color_buttons;
1201 cur_button != NULL;
1202 cur_button = cur_button->next ) {
1203 cur_button->x = current_x;
1204 cur_button->y = picker->center.y + bounds_radius_px + spacing;
1205 cur_button->w = button_size;
1206 cur_button->h = button_size;
1207
1208 current_x += spacing + button_size;
1209 }
1210 }
1211
1212 static b32
picker_hit_history_buttons(ColorPicker * picker,f32 ui_scale,v2i point)1213 picker_hit_history_buttons(ColorPicker* picker, f32 ui_scale, v2i point)
1214 {
1215 b32 hits = false;
1216 ColorButton* first = picker->color_buttons;
1217 ColorButton* button = first;
1218 ColorButton* prev = NULL;
1219 while ( button ) {
1220 if ( button->rgba.a != 0 &&
1221 is_inside_rect(color_button_as_rect(button), point) ) {
1222 hits = true;
1223
1224 gui_picker_from_rgb(picker, button->rgba.rgb);
1225
1226 if ( prev ) {
1227 prev->next = button->next;
1228 }
1229 if ( button != picker->color_buttons ) {
1230 button->next = picker->color_buttons;
1231 }
1232 picker->color_buttons = button;
1233
1234 update_button_bounds(picker, ui_scale);
1235 break;
1236 }
1237 prev = button;
1238 button = button->next;
1239 }
1240 return hits;
1241 }
1242
1243 static ColorPickResult
picker_update(ColorPicker * picker,v2i point)1244 picker_update(ColorPicker* picker, v2i point)
1245 {
1246 ColorPickResult result = ColorPickResult_NOTHING;
1247 v2f fpoint = v2i_to_v2f(point);
1248 if ( picker->flags == ColorPickerFlags_NOTHING ) {
1249 if ( picker_hits_wheel(picker, fpoint) ) {
1250 picker->flags |= ColorPickerFlags_WHEEL_ACTIVE;
1251 }
1252 else if ( picker_hits_triangle(picker, fpoint) ) {
1253 picker->flags |= ColorPickerFlags_TRIANGLE_ACTIVE;
1254 }
1255 }
1256 if (( picker->flags & ColorPickerFlags_WHEEL_ACTIVE )) {
1257 if (!(picker->flags & ColorPickerFlags_TRIANGLE_ACTIVE))
1258 {
1259 picker_update_wheel(picker, fpoint);
1260 result = ColorPickResult_CHANGE_COLOR;
1261 }
1262 }
1263 if (( picker->flags & ColorPickerFlags_TRIANGLE_ACTIVE )) {
1264 PickerData& d = picker->data; // Just shortening the identifier.
1265
1266 // We don't want the chooser to "stick" if it goes outside the triangle
1267 // (i.e. picking black should be easy)
1268 f32 abp = orientation(d.a, d.b, fpoint);
1269 f32 bcp = orientation(d.b, d.c, fpoint);
1270 f32 cap = orientation(d.c, d.a, fpoint);
1271 int insideness = (abp < 0.0f) + (bcp < 0.0f) + (cap < 0.0f);
1272
1273 // inside the triangle
1274 if ( insideness == 3 ) {
1275 float inv_area = 1.0f / orientation(d.a, d.b, d.c);
1276 d.hsv.v = 1.0f - (cap * inv_area);
1277
1278 d.hsv.s = abp * inv_area / d.hsv.v;
1279 d.hsv.s = abp / (orientation(d.a, d.b, d.c) - cap);
1280 d.hsv.s = 1 - (bcp * inv_area) / d.hsv.v;
1281 }
1282 // near a corner
1283 else if ( insideness < 2 ) {
1284 if (abp < 0.0f) {
1285 d.hsv.s = 1.0f;
1286 d.hsv.v = 1.0f;
1287 }
1288 else if (bcp < 0.0f) {
1289 d.hsv.s = 0.0f;
1290 d.hsv.v = 1.0f;
1291 }
1292 else {
1293 d.hsv.s = 0.5f;
1294 d.hsv.v = 0.0f;
1295 }
1296 }
1297 // near an edge
1298 else {
1299 #define GET_T(A, B, C) \
1300 v2f perp = perpendicular(fpoint - C); \
1301 f32 t = DOT(C - A, perp) / DOT(B - A, perp);
1302 if (abp >= 0.0f) {
1303 GET_T(d.b, d.a, d.c)
1304 d.hsv.s = 0.0f;
1305 d.hsv.v = t;
1306 }
1307 else if (bcp >= 0.0f) {
1308 GET_T(d.b, d.c, d.a)
1309 d.hsv.s = 1.0f;
1310 d.hsv.v = t;
1311 }
1312 else {
1313 GET_T(d.a, d.c, d.b)
1314 d.hsv.s = t;
1315 d.hsv.v = 1.0f;
1316 }
1317 }
1318 #undef GET_T
1319 result = ColorPickResult_CHANGE_COLOR;
1320 }
1321
1322 return result;
1323 }
1324
1325
1326 b32
picker_hits_wheel(ColorPicker * picker,v2f point)1327 picker_hits_wheel(ColorPicker* picker, v2f point)
1328 {
1329 v2f center = v2i_to_v2f(picker->center);
1330 v2f arrow = point - center;
1331 float dist = magnitude(arrow);
1332
1333 b32 hits = (dist <= picker->wheel_radius + picker->wheel_half_width ) &&
1334 (dist >= picker->wheel_radius - picker->wheel_half_width );
1335
1336 return hits;
1337 }
1338
1339 float
picker_wheel_get_angle(ColorPicker * picker,v2f point)1340 picker_wheel_get_angle(ColorPicker* picker, v2f point)
1341 {
1342 v2f direction = point - v2i_to_v2f(picker->center);
1343 f32 angle = atan2f(direction.y, -direction.x) + kPi;
1344 return angle;
1345 }
1346
1347 void
picker_init(ColorPicker * picker)1348 picker_init(ColorPicker* picker)
1349 {
1350 v2f fpoint = {
1351 (f32)picker->center.x + (int)(picker->wheel_radius),
1352 (f32)picker->center.y
1353 };
1354 picker_update_wheel(picker, fpoint);
1355 picker->data.hsv = v3f{ 0, 1, 0 };
1356 }
1357
1358 Rect
picker_get_bounds(ColorPicker * picker)1359 picker_get_bounds(ColorPicker* picker)
1360 {
1361 Rect picker_rect;
1362 {
1363 picker_rect.left = picker->center.x - picker->bounds_radius_px;
1364 picker_rect.right = picker->center.x + picker->bounds_radius_px;
1365 picker_rect.bottom = picker->center.y + picker->bounds_radius_px;
1366 picker_rect.top = picker->center.y - picker->bounds_radius_px;
1367 }
1368 mlt_assert (picker_rect.left >= 0);
1369 mlt_assert (picker_rect.top >= 0);
1370
1371 return picker_rect;
1372 }
1373
1374 void
exporter_init(Exporter * exporter)1375 exporter_init(Exporter* exporter)
1376 {
1377 *exporter = Exporter{};
1378 exporter->scale = 1;
1379 }
1380
1381 b32
exporter_input(Exporter * exporter,MiltonInput const * input)1382 exporter_input(Exporter* exporter, MiltonInput const* input)
1383 {
1384 b32 changed = false;
1385 if ( input->input_count > 0 ) {
1386 changed = true;
1387 v2i point = VEC2I(input->points[input->input_count - 1]);
1388 if ( exporter->state == ExporterState_EMPTY ||
1389 exporter->state == ExporterState_SELECTED ) {
1390 exporter->pivot = point;
1391 exporter->needle = point;
1392 exporter->state = ExporterState_GROWING_RECT;
1393 }
1394 else if ( exporter->state == ExporterState_GROWING_RECT ) {
1395 exporter->needle = point;
1396 }
1397 }
1398 if ( (input->flags & MiltonInputFlags_END_STROKE) && exporter->state != ExporterState_EMPTY ) {
1399 exporter->state = ExporterState_SELECTED;
1400 changed = true;
1401 }
1402 return changed;
1403 }
1404
1405 void
gui_imgui_set_ungrabbed(MiltonGui * gui)1406 gui_imgui_set_ungrabbed(MiltonGui* gui)
1407 {
1408 gui->flags &= ~MiltonGuiFlags_SHOWING_PREVIEW;
1409 }
1410
1411 Rect
get_bounds_for_picker_and_colors(ColorPicker * picker)1412 get_bounds_for_picker_and_colors(ColorPicker* picker)
1413 {
1414 Rect result = rect_union(picker_get_bounds(picker), picker_color_buttons_bounds(picker));
1415 return result;
1416 }
1417
1418 static b32
picker_is_active(ColorPicker * picker)1419 picker_is_active(ColorPicker* picker)
1420 {
1421 b32 active = (picker->flags & ColorPickerFlags_TRIANGLE_ACTIVE) ||
1422 (picker->flags & ColorPickerFlags_WHEEL_ACTIVE);
1423 return active;
1424 }
1425
1426 v3f
picker_hsv_from_point(ColorPicker * picker,v2f point)1427 picker_hsv_from_point(ColorPicker* picker, v2f point)
1428 {
1429 float area = orientation(picker->data.a, picker->data.b, picker->data.c);
1430 v3f hsv = {};
1431 if ( area != 0 ) {
1432 float inv_area = 1.0f / area;
1433 float v = 1 - (orientation(point, picker->data.c, picker->data.a) * inv_area);
1434 float s = orientation(picker->data.b, point, picker->data.a) * inv_area / v;
1435
1436 hsv = v3f
1437 {
1438 picker->data.hsv.h,
1439 s,
1440 v,
1441 };
1442 }
1443 return hsv;
1444 }
1445
1446 v3f
gui_get_picker_rgb(MiltonGui * gui)1447 gui_get_picker_rgb(MiltonGui* gui)
1448 {
1449 v3f rgb = hsv_to_rgb(gui->picker.data.hsv);
1450 return rgb;
1451 }
1452
1453 // Returns true if the Picker consumed input. False if the GUI wasn't affected
1454 b32
gui_consume_input(MiltonGui * gui,MiltonInput const * input)1455 gui_consume_input(MiltonGui* gui, MiltonInput const* input)
1456 {
1457 b32 accepts = false;
1458 v2i point = VEC2I(input->points[0]);
1459 if ( gui->visible ) {
1460 accepts = gui_point_hovers(gui, point);
1461 if ( !picker_is_active(&gui->picker) &&
1462 !gui->did_hit_button &&
1463 picker_hit_history_buttons(&gui->picker, gui->scale, point) ) {
1464 accepts = true;
1465 gui->did_hit_button = true;
1466 gui->owns_user_input = true;
1467 }
1468 if ( accepts ) {
1469 ColorPickResult pick_result = picker_update(&gui->picker, point);
1470 if ( pick_result == ColorPickResult_CHANGE_COLOR ) {
1471 gui->owns_user_input = true;
1472 }
1473 }
1474 }
1475 return accepts;
1476 }
1477
1478 void
gui_toggle_menu_visibility(MiltonGui * gui)1479 gui_toggle_menu_visibility(MiltonGui* gui)
1480 {
1481 gui->menu_visible = !gui->menu_visible;
1482 }
1483
1484 void
gui_toggle_help(MiltonGui * gui)1485 gui_toggle_help(MiltonGui* gui)
1486 {
1487 gui->show_help_widget = !gui->show_help_widget;
1488 }
1489
1490 void
gui_init(Arena * root_arena,MiltonGui * gui,f32 ui_scale)1491 gui_init(Arena* root_arena, MiltonGui* gui, f32 ui_scale)
1492 {
1493 gui->scale = ui_scale;
1494 i32 bounds_radius_px = ui_scale*BOUNDS_RADIUS_PX;
1495 f32 wheel_half_width = ui_scale*12;
1496 gui->picker.center = v2i{ bounds_radius_px + int(ui_scale*20), bounds_radius_px + (int)(ui_scale*30) };
1497 gui->picker.bounds_radius_px = bounds_radius_px;
1498 gui->picker.wheel_half_width = wheel_half_width;
1499 gui->picker.wheel_radius = (f32)bounds_radius_px - ui_scale*5.0f - wheel_half_width;
1500 gui->picker.data.hsv = v3f{ 0.0f, 1.0f, 0.7f };
1501 Rect bounds;
1502 bounds.left = gui->picker.center.x - bounds_radius_px;
1503 bounds.right = gui->picker.center.x + bounds_radius_px;
1504 bounds.top = gui->picker.center.y - bounds_radius_px;
1505 bounds.bottom = gui->picker.center.y + bounds_radius_px;
1506 gui->picker.bounds = bounds;
1507 gui->picker.pixels = arena_alloc_array(root_arena, (4 * bounds_radius_px * bounds_radius_px), u32);
1508 gui->visible = true;
1509 gui->picker.color_buttons = arena_alloc_elem(root_arena, ColorButton);
1510 gui->original_settings = arena_alloc_elem(root_arena, MiltonSettings);
1511
1512 picker_init(&gui->picker);
1513
1514 i32 num_buttons = NUM_BUTTONS;
1515 auto* cur_button = gui->picker.color_buttons;
1516 for ( i64 i = 0; i != (num_buttons - 1); ++i ) {
1517 cur_button->next = arena_alloc_elem(root_arena, ColorButton);
1518 cur_button = cur_button->next;
1519 }
1520 update_button_bounds(&gui->picker, gui->scale);
1521
1522 gui->preview_pos = v2i{-1, -1};
1523 gui->preview_pos_prev = v2i{-1, -1};
1524
1525
1526 exporter_init(&gui->exporter);
1527 }
1528
1529 // When a selected color is used in a stroke, call this to update the color
1530 // button list.
1531 b32
gui_mark_color_used(MiltonGui * gui)1532 gui_mark_color_used(MiltonGui* gui)
1533 {
1534 b32 changed = false;
1535 ColorButton* start = gui->picker.color_buttons;
1536 v3f picker_color = hsv_to_rgb(gui->picker.data.hsv);
1537 // Search for a color that is already in the list
1538 ColorButton* button = start;
1539 while ( button ) {
1540 if ( button->rgba.a != 0 ) {
1541 v3f diff =
1542 {
1543 fabsf(button->rgba.r - picker_color.r),
1544 fabsf(button->rgba.g - picker_color.g),
1545 fabsf(button->rgba.b - picker_color.b),
1546 };
1547 float epsilon = 0.000001f;
1548 if ( diff.r < epsilon && diff.g < epsilon && diff.b < epsilon ) {
1549 // Move this button to the start and return.
1550 changed = true;
1551 v4f tmp_color = button->rgba;
1552 button->rgba = start->rgba;
1553 start->rgba = tmp_color;
1554 }
1555 }
1556 button = button->next;
1557 }
1558 button = start;
1559
1560 // If not found, add to list.
1561 if ( !changed ) {
1562 changed = true;
1563 v4f button_color = color_rgb_to_rgba(picker_color,1);
1564 // Pass data to the next one.
1565 while ( button ) {
1566 v4f tmp_color = button->rgba;
1567 button->rgba = button_color;
1568 button_color = tmp_color;
1569 button = button->next;
1570 }
1571 }
1572
1573 return changed;
1574 }
1575
1576 void
gui_deactivate(MiltonGui * gui)1577 gui_deactivate(MiltonGui* gui)
1578 {
1579 picker_deactivate(&gui->picker);
1580
1581 // Reset transient values
1582 gui->owns_user_input = false;
1583 gui->did_hit_button = false;
1584 }
1585