1 /*
2  * Copyright (c) 2015-2021 Hanspeter Portner (dev@open-music-kontrollers.ch)
3  *
4  * This is free software: you can redistribute it and/or modify
5  * it under the terms of the Artistic License 2.0 as published by
6  * The Perl Foundation.
7  *
8  * This source is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * Artistic License 2.0 for more details.
12  *
13  * You should have received a copy of the Artistic License 2.0
14  * along the source as a COPYING file. If not, obtain it from
15  * http://www.perlfoundation.org/artistic_license_2_0.
16  */
17 
18 #include <inttypes.h>
19 
20 #include <sherlock.h>
21 #include <sherlock_nk.h>
22 #include <encoder.h>
23 
24 typedef struct _midi_msg_t midi_msg_t;
25 
26 struct _midi_msg_t {
27 	uint8_t type;
28 	const char *key;
29 };
30 
31 #define COMMANDS_NUM 18
32 static const midi_msg_t commands [COMMANDS_NUM] = {
33 	{ LV2_MIDI_MSG_NOTE_OFF							, "NoteOff" },
34 	{ LV2_MIDI_MSG_NOTE_ON							, "NoteOn" },
35 	{ LV2_MIDI_MSG_NOTE_PRESSURE				, "NotePressure" },
36 	{ LV2_MIDI_MSG_CONTROLLER						, "Controller" },
37 	{ LV2_MIDI_MSG_PGM_CHANGE						, "ProgramChange" },
38 	{ LV2_MIDI_MSG_CHANNEL_PRESSURE			, "ChannelPressure" },
39 	{ LV2_MIDI_MSG_BENDER								, "Bender" },
40 	{ LV2_MIDI_MSG_SYSTEM_EXCLUSIVE			, "SystemExclusive" },
41 	{ LV2_MIDI_MSG_MTC_QUARTER					, "QuarterFrame" },
42 	{ LV2_MIDI_MSG_SONG_POS							, "SongPosition" },
43 	{ LV2_MIDI_MSG_SONG_SELECT					, "SongSelect" },
44 	{ LV2_MIDI_MSG_TUNE_REQUEST					, "TuneRequest" },
45 	{ LV2_MIDI_MSG_CLOCK								, "Clock" },
46 	{ LV2_MIDI_MSG_START								, "Start" },
47 	{ LV2_MIDI_MSG_CONTINUE							, "Continue" },
48 	{ LV2_MIDI_MSG_STOP									, "Stop" },
49 	{ LV2_MIDI_MSG_ACTIVE_SENSE					, "ActiveSense" },
50 	{ LV2_MIDI_MSG_RESET								, "Reset" },
51 };
52 
53 #define CONTROLLERS_NUM 72
54 static const midi_msg_t controllers [CONTROLLERS_NUM] = {
55 	{ LV2_MIDI_CTL_MSB_BANK             , "BankSelection_MSB" },
56 	{ LV2_MIDI_CTL_MSB_MODWHEEL         , "Modulation_MSB" },
57 	{ LV2_MIDI_CTL_MSB_BREATH           , "Breath_MSB" },
58 	{ LV2_MIDI_CTL_MSB_FOOT             , "Foot_MSB" },
59 	{ LV2_MIDI_CTL_MSB_PORTAMENTO_TIME  , "PortamentoTime_MSB" },
60 	{ LV2_MIDI_CTL_MSB_DATA_ENTRY       , "DataEntry_MSB" },
61 	{ LV2_MIDI_CTL_MSB_MAIN_VOLUME      , "MainVolume_MSB" },
62 	{ LV2_MIDI_CTL_MSB_BALANCE          , "Balance_MSB" },
63 	{ LV2_MIDI_CTL_MSB_PAN              , "Panpot_MSB" },
64 	{ LV2_MIDI_CTL_MSB_EXPRESSION       , "Expression_MSB" },
65 	{ LV2_MIDI_CTL_MSB_EFFECT1          , "Effect1_MSB" },
66 	{ LV2_MIDI_CTL_MSB_EFFECT2          , "Effect2_MSB" },
67 	{ LV2_MIDI_CTL_MSB_GENERAL_PURPOSE1 , "GeneralPurpose1_MSB" },
68 	{ LV2_MIDI_CTL_MSB_GENERAL_PURPOSE2 , "GeneralPurpose2_MSB" },
69 	{ LV2_MIDI_CTL_MSB_GENERAL_PURPOSE3 , "GeneralPurpose3_MSB" },
70 	{ LV2_MIDI_CTL_MSB_GENERAL_PURPOSE4 , "GeneralPurpose4_MSB" },
71 
72 	{ LV2_MIDI_CTL_LSB_BANK             , "BankSelection_LSB" },
73 	{ LV2_MIDI_CTL_LSB_MODWHEEL         , "Modulation_LSB" },
74 	{ LV2_MIDI_CTL_LSB_BREATH           , "Breath_LSB" },
75 	{ LV2_MIDI_CTL_LSB_FOOT             , "Foot_LSB" },
76 	{ LV2_MIDI_CTL_LSB_PORTAMENTO_TIME  , "PortamentoTime_LSB" },
77 	{ LV2_MIDI_CTL_LSB_DATA_ENTRY       , "DataEntry_LSB" },
78 	{ LV2_MIDI_CTL_LSB_MAIN_VOLUME      , "MainVolume_LSB" },
79 	{ LV2_MIDI_CTL_LSB_BALANCE          , "Balance_LSB" },
80 	{ LV2_MIDI_CTL_LSB_PAN              , "Panpot_LSB" },
81 	{ LV2_MIDI_CTL_LSB_EXPRESSION       , "Expression_LSB" },
82 	{ LV2_MIDI_CTL_LSB_EFFECT1          , "Effect1_LSB" },
83 	{ LV2_MIDI_CTL_LSB_EFFECT2          , "Effect2_LSB" },
84 	{ LV2_MIDI_CTL_LSB_GENERAL_PURPOSE1 , "GeneralPurpose1_LSB" },
85 	{ LV2_MIDI_CTL_LSB_GENERAL_PURPOSE2 , "GeneralPurpose2_LSB" },
86 	{ LV2_MIDI_CTL_LSB_GENERAL_PURPOSE3 , "GeneralPurpose3_LSB" },
87 	{ LV2_MIDI_CTL_LSB_GENERAL_PURPOSE4 , "GeneralPurpose4_LSB" },
88 
89 	{ LV2_MIDI_CTL_SUSTAIN              , "SustainPedal" },
90 	{ LV2_MIDI_CTL_PORTAMENTO           , "Portamento" },
91 	{ LV2_MIDI_CTL_SOSTENUTO            , "Sostenuto" },
92 	{ LV2_MIDI_CTL_SOFT_PEDAL           , "SoftPedal" },
93 	{ LV2_MIDI_CTL_LEGATO_FOOTSWITCH    , "LegatoFootSwitch" },
94 	{ LV2_MIDI_CTL_HOLD2                , "Hold2" },
95 
96 	{ LV2_MIDI_CTL_SC1_SOUND_VARIATION  , "SC1_SoundVariation" },
97 	{ LV2_MIDI_CTL_SC2_TIMBRE           , "SC2_Timbre" },
98 	{ LV2_MIDI_CTL_SC3_RELEASE_TIME     , "SC3_ReleaseTime" },
99 	{ LV2_MIDI_CTL_SC4_ATTACK_TIME      , "SC4_AttackTime" },
100 	{ LV2_MIDI_CTL_SC5_BRIGHTNESS       , "SC5_Brightness" },
101 	{ LV2_MIDI_CTL_SC6                  , "SC6" },
102 	{ LV2_MIDI_CTL_SC7                  , "SC7" },
103 	{ LV2_MIDI_CTL_SC8                  , "SC8" },
104 	{ LV2_MIDI_CTL_SC9                  , "SC9" },
105 	{ LV2_MIDI_CTL_SC10                 , "SC10" },
106 
107 	{ LV2_MIDI_CTL_GENERAL_PURPOSE5     , "GeneralPurpose5" },
108 	{ LV2_MIDI_CTL_GENERAL_PURPOSE6     , "GeneralPurpose6" },
109 	{ LV2_MIDI_CTL_GENERAL_PURPOSE7     , "GeneralPurpose7" },
110 	{ LV2_MIDI_CTL_GENERAL_PURPOSE8     , "GeneralPurpose8" },
111 	{ LV2_MIDI_CTL_PORTAMENTO_CONTROL   , "PortamentoControl" },
112 
113 	{ LV2_MIDI_CTL_E1_REVERB_DEPTH      , "E1_ReverbDepth" },
114 	{ LV2_MIDI_CTL_E2_TREMOLO_DEPTH     , "E2_TremoloDepth" },
115 	{ LV2_MIDI_CTL_E3_CHORUS_DEPTH      , "E3_ChorusDepth" },
116 	{ LV2_MIDI_CTL_E4_DETUNE_DEPTH      , "E4_DetuneDepth" },
117 	{ LV2_MIDI_CTL_E5_PHASER_DEPTH      , "E5_PhaserDepth" },
118 
119 	{ LV2_MIDI_CTL_DATA_INCREMENT       , "DataIncrement" },
120 	{ LV2_MIDI_CTL_DATA_DECREMENT       , "DataDecrement" },
121 
122 	{ LV2_MIDI_CTL_NRPN_LSB             , "NRPN_LSB" },
123 	{ LV2_MIDI_CTL_NRPN_MSB             , "NRPN_MSB" },
124 
125 	{ LV2_MIDI_CTL_RPN_LSB              , "RPN_LSB" },
126 	{ LV2_MIDI_CTL_RPN_MSB              , "RPN_MSB" },
127 
128 	{ LV2_MIDI_CTL_ALL_SOUNDS_OFF       , "AllSoundsOff" },
129 	{ LV2_MIDI_CTL_RESET_CONTROLLERS    , "ResetControllers" },
130 	{ LV2_MIDI_CTL_LOCAL_CONTROL_SWITCH , "LocalControlSwitch" },
131 	{ LV2_MIDI_CTL_ALL_NOTES_OFF        , "AllNotesOff" },
132 	{ LV2_MIDI_CTL_OMNI_OFF             , "OmniOff" },
133 	{ LV2_MIDI_CTL_OMNI_ON              , "OmniOn" },
134 	{ LV2_MIDI_CTL_MONO1                , "Mono1" },
135 	{ LV2_MIDI_CTL_MONO2                , "Mono2" },
136 };
137 
138 #define TIMECODES_NUM 8
139 static const midi_msg_t timecodes [TIMECODES_NUM] = {
140 	{ 0 , "FrameNumber_LSB" },
141 	{ 1 , "FrameNumber_MSB" },
142 	{ 2 , "Second_LSB" },
143 	{ 3 , "Second_MSB" },
144 	{ 4 , "Minute_LSB" },
145 	{ 5 , "Minute_MSB" },
146 	{ 6 , "Hour_LSB" },
147 	{ 7 , "RateAndHour_MSB" },
148 };
149 
150 static int
_cmp_search(const void * itm1,const void * itm2)151 _cmp_search(const void *itm1, const void *itm2)
152 {
153 	const midi_msg_t *msg1 = itm1;
154 	const midi_msg_t *msg2 = itm2;
155 
156 	if(msg1->type < msg2->type)
157 		return -1;
158 	else if(msg1->type > msg2->type)
159 		return 1;
160 
161 	return 0;
162 }
163 
164 static inline const midi_msg_t *
_search_command(uint8_t type)165 _search_command(uint8_t type)
166 {
167 	return bsearch(&type, commands, COMMANDS_NUM, sizeof(midi_msg_t), _cmp_search);
168 }
169 
170 static inline const midi_msg_t *
_search_controller(uint8_t type)171 _search_controller(uint8_t type)
172 {
173 	return bsearch(&type, controllers, CONTROLLERS_NUM, sizeof(midi_msg_t), _cmp_search);
174 }
175 
176 static inline const midi_msg_t *
_search_timecode(uint8_t type)177 _search_timecode(uint8_t type)
178 {
179 	return bsearch(&type, timecodes, TIMECODES_NUM, sizeof(midi_msg_t), _cmp_search);
180 }
181 
182 static const char *keys [12] = {
183 	"C", "C#",
184 	"D", "D#",
185 	"E",
186 	"F", "F#",
187 	"G", "G#",
188 	"A", "A#",
189 	"B"
190 };
191 
192 static inline const char *
_note(uint8_t val,int8_t * octave)193 _note(uint8_t val, int8_t *octave)
194 {
195 	*octave = val / 12 - 1;
196 
197 	return keys[val % 12];
198 }
199 
200 static inline void
_shadow(struct nk_context * ctx,bool * shadow)201 _shadow(struct nk_context *ctx, bool *shadow)
202 {
203 	if(*shadow)
204 	{
205 		struct nk_style *style = &ctx->style;
206 		const struct nk_vec2 group_padding = style->window.group_padding;
207 		struct nk_command_buffer *canvas = nk_window_get_canvas(ctx);
208 
209 		struct nk_rect b = nk_widget_bounds(ctx);
210 		b.x -= group_padding.x;
211 		b.w *= 10;
212 		b.w += 5*group_padding.x;
213 		nk_fill_rect(canvas, b, 0.f, nk_rgb(0x28, 0x28, 0x28));
214 	}
215 
216 	*shadow = !*shadow;
217 }
218 
219 void
_midi_inspector_expose(struct nk_context * ctx,struct nk_rect wbounds,void * data)220 _midi_inspector_expose(struct nk_context *ctx, struct nk_rect wbounds, void *data)
221 {
222 	plughandle_t *handle = data;
223 
224 	handle->dy = 20.f * _get_scale(handle);
225 	const float widget_h = handle->dy;
226 	struct nk_style *style = &ctx->style;
227   const struct nk_vec2 window_padding = style->window.padding;
228   const struct nk_vec2 group_padding = style->window.group_padding;
229 
230 	const char *window_name = "Sherlock";
231 	if(nk_begin(ctx, window_name, wbounds, NK_WINDOW_NO_SCROLLBAR))
232 	{
233 		struct nk_panel *panel = nk_window_get_panel(ctx);
234 		struct nk_command_buffer *canvas = nk_window_get_canvas(ctx);
235 
236 		const float body_h = panel->bounds.h - 4*window_padding.y - 2*widget_h;
237 		nk_layout_row_dynamic(ctx, body_h, 1);
238 		nk_flags flags = NK_WINDOW_BORDER;
239 		if(handle->state.follow)
240 		{
241 			flags |= NK_WINDOW_NO_SCROLLBAR;
242 		}
243 		else
244 		{
245 			handle->shadow = false;
246 		}
247 		struct nk_list_view lview;
248 		if(nk_list_view_begin(ctx, &lview, "Events", flags, widget_h, NK_MIN(handle->n_item, MAX_LINES)))
249 		{
250 			if(handle->state.follow)
251 			{
252 				lview.end = NK_MAX(handle->n_item, 0);
253 				lview.begin = NK_MAX(lview.end - lview.count, 0);
254 			}
255 			for(int l = lview.begin; (l < lview.end) && (l < handle->n_item); l++)
256 			{
257 				item_t *itm = handle->items[l];
258 
259 				switch(itm->type)
260 				{
261 					case ITEM_TYPE_NONE:
262 					{
263 						// skip, was sysex payload
264 					} break;
265 					case ITEM_TYPE_FRAME:
266 					{
267 						nk_layout_row_dynamic(ctx, widget_h, 3);
268 						{
269 							struct nk_rect b = nk_widget_bounds(ctx);
270 							b.x -= group_padding.x;
271 							b.w *= 3;
272 							b.w += 4*group_padding.x;
273 							nk_fill_rect(canvas, b, 0.f, nk_rgb(0x18, 0x18, 0x18));
274 						}
275 
276 						nk_labelf_colored(ctx, NK_TEXT_LEFT, orange, "@%"PRIi64, itm->frame.offset);
277 						nk_labelf_colored(ctx, NK_TEXT_CENTERED, green, "-%"PRIu32"-", itm->frame.counter);
278 						nk_labelf_colored(ctx, NK_TEXT_RIGHT, violet, "%"PRIi32, itm->frame.nsamples);
279 
280 						handle->shadow = false;
281 					} break;
282 
283 					case ITEM_TYPE_EVENT:
284 					{
285 						LV2_Atom_Event *ev = &itm->event.ev;
286 						const LV2_Atom *body = &ev->body;
287 						const int64_t frames = ev->time.frames;
288 						const uint8_t *msg = LV2_ATOM_BODY_CONST(body);
289 						const uint8_t cmd = (msg[0] & 0xf0) == 0xf0
290 							? msg[0]
291 							: msg[0] & 0xf0;
292 
293 						const midi_msg_t *command_msg = _search_command(cmd);
294 						const char *command_str = command_msg
295 							? command_msg->key
296 							: "Unknown";
297 
298 						char tmp [16];
299 						nk_layout_row_begin(ctx, NK_DYNAMIC, widget_h, 7);
300 						{
301 							nk_layout_row_push(ctx, 0.1);
302 							_shadow(ctx, &handle->shadow);
303 							nk_labelf_colored(ctx, NK_TEXT_LEFT, yellow, "+%04"PRIi64, frames);
304 
305 							nk_layout_row_push(ctx, 0.2);
306 							const unsigned rem = body->size;
307 							const unsigned to = rem >= 4 ? 4 : rem % 4;
308 							for(unsigned i=0, ptr=0; i<to; i++, ptr+=3)
309 								sprintf(&tmp[ptr], "%02"PRIX8" ", msg[i]);
310 							tmp[to*3 - 1] = '\0';
311 							nk_label_colored(ctx, tmp, NK_TEXT_LEFT, cwhite);
312 
313 							nk_layout_row_push(ctx, 0.2);
314 							nk_label_colored(ctx, command_str, NK_TEXT_LEFT, magenta);
315 
316 							switch(cmd)
317 							{
318 								case LV2_MIDI_MSG_NOTE_OFF:
319 									// fall-through
320 								case LV2_MIDI_MSG_NOTE_ON:
321 									// fall-through
322 								case LV2_MIDI_MSG_NOTE_PRESSURE:
323 								{
324 									nk_layout_row_push(ctx, 0.1);
325 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "Ch:%2"PRIu8,
326 										(msg[0] & 0x0f) + 1);
327 
328 									nk_layout_row_push(ctx, 0.2);
329 									int8_t octave;
330 									const char *key = _note(msg[1], &octave);
331 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%s%+"PRIi8"=%3"PRIu8,
332 										key, octave, msg[1]);
333 
334 									nk_layout_row_push(ctx, 0.1);
335 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%"PRIu8, msg[2]);
336 								} break;
337 								case LV2_MIDI_MSG_CONTROLLER:
338 								{
339 									nk_layout_row_push(ctx, 0.1);
340 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "Ch:%2"PRIu8,
341 										(msg[0] & 0x0f) + 1);
342 
343 									const midi_msg_t *controller_msg = _search_controller(msg[1]);
344 									const char *controller_str = controller_msg
345 										? controller_msg->key
346 										: "Unknown";
347 									nk_layout_row_push(ctx, 0.2);
348 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%s=%3"PRIu8"",
349 										controller_str, msg[1]);
350 
351 									nk_layout_row_push(ctx, 0.1);
352 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%"PRIu8, msg[2]);
353 								} break;
354 								case LV2_MIDI_MSG_PGM_CHANGE:
355 									// fall-through
356 								case LV2_MIDI_MSG_CHANNEL_PRESSURE:
357 								{
358 									nk_layout_row_push(ctx, 0.1);
359 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "Ch:%2"PRIu8,
360 										(msg[0] & 0x0f) + 1);
361 
362 									nk_layout_row_push(ctx, 0.2);
363 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%"PRIu8, msg[1]);
364 
365 									nk_layout_row_push(ctx, 0.1);
366 									_empty(ctx);
367 								}	break;
368 								case LV2_MIDI_MSG_BENDER:
369 								{
370 									const int16_t bender = (((int16_t)msg[2] << 7) | msg[1]) - 0x2000;
371 
372 									nk_layout_row_push(ctx, 0.1);
373 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "Ch:%2"PRIu8,
374 										(msg[0] & 0x0f) + 1);
375 
376 									nk_layout_row_push(ctx, 0.2);
377 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%"PRIi16, bender);
378 
379 									nk_layout_row_push(ctx, 0.1);
380 									_empty(ctx);
381 								}	break;
382 								case LV2_MIDI_MSG_MTC_QUARTER:
383 								{
384 									const uint8_t msg_type = msg[1] >> 4;
385 									const uint8_t msg_val = msg[1] & 0xf;
386 
387 									nk_layout_row_push(ctx, 0.1);
388 									_empty(ctx);
389 
390 									const midi_msg_t *timecode_msg = _search_timecode(msg_type);
391 									const char *timecode_str = timecode_msg
392 										? timecode_msg->key
393 										: "Unknown";
394 									nk_layout_row_push(ctx, 0.2);
395 									nk_label_colored(ctx, timecode_str, NK_TEXT_RIGHT, cwhite);
396 
397 									nk_layout_row_push(ctx, 0.1);
398 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%"PRIu8, msg_val);
399 								} break;
400 								case LV2_MIDI_MSG_SONG_POS:
401 								{
402 									const int16_t song_pos= (((int16_t)msg[2] << 7) | msg[1]);
403 
404 									nk_layout_row_push(ctx, 0.1);
405 									_empty(ctx);
406 
407 									nk_layout_row_push(ctx, 0.2);
408 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%"PRIu16, song_pos);
409 
410 									nk_layout_row_push(ctx, 0.1);
411 									_empty(ctx);
412 								} break;
413 								case LV2_MIDI_MSG_SONG_SELECT:
414 								{
415 									nk_layout_row_push(ctx, 0.1);
416 									_empty(ctx);
417 
418 									nk_layout_row_push(ctx, 0.2);
419 									nk_labelf_colored(ctx, NK_TEXT_RIGHT, cwhite, "%"PRIu8, msg[1]);
420 
421 									nk_layout_row_push(ctx, 0.1);
422 									_empty(ctx);
423 								} break;
424 								case LV2_MIDI_MSG_SYSTEM_EXCLUSIVE:
425 									// fall-throuh
426 								case LV2_MIDI_MSG_TUNE_REQUEST:
427 									// fall-throuh
428 								case LV2_MIDI_MSG_CLOCK:
429 									// fall-throuh
430 								case LV2_MIDI_MSG_START:
431 									// fall-throuh
432 								case LV2_MIDI_MSG_CONTINUE:
433 									// fall-throuh
434 								case LV2_MIDI_MSG_STOP:
435 									// fall-throuh
436 								case LV2_MIDI_MSG_ACTIVE_SENSE:
437 									// fall-throuh
438 								case LV2_MIDI_MSG_RESET:
439 								{
440 									nk_layout_row_push(ctx, 0.1);
441 									_empty(ctx);
442 
443 									nk_layout_row_push(ctx, 0.2);
444 									_empty(ctx);
445 
446 									nk_layout_row_push(ctx, 0.1);
447 									_empty(ctx);
448 								} break;
449 							}
450 
451 							nk_layout_row_push(ctx, 0.1);
452 							nk_labelf_colored(ctx, NK_TEXT_RIGHT, blue, "%"PRIu32, body->size);
453 						}
454 						nk_layout_row_end(ctx);
455 
456 						for(unsigned j=4; j<body->size; j+=4)
457 						{
458 							nk_layout_row_begin(ctx, NK_DYNAMIC, widget_h, 7);
459 							{
460 								nk_layout_row_push(ctx, 0.1);
461 								_shadow(ctx, &handle->shadow);
462 								_empty(ctx);
463 
464 								nk_layout_row_push(ctx, 0.2);
465 								const unsigned rem = body->size - j;
466 								const unsigned to = rem >= 4 ? 4 : rem % 4;
467 								for(unsigned i=0, ptr=0; i<to; i++, ptr+=3)
468 									sprintf(&tmp[ptr], "%02"PRIX8" ", msg[j+i]);
469 								tmp[to*3 - 1] = '\0';
470 								nk_label_colored(ctx, tmp, NK_TEXT_LEFT, cwhite);
471 
472 								nk_layout_row_push(ctx, 0.2);
473 								_empty(ctx);
474 
475 								nk_layout_row_push(ctx, 0.1);
476 								_empty(ctx);
477 
478 								nk_layout_row_push(ctx, 0.2);
479 								_empty(ctx);
480 
481 								nk_layout_row_push(ctx, 0.1);
482 								_empty(ctx);
483 
484 								nk_layout_row_push(ctx, 0.1);
485 								_empty(ctx);
486 							}
487 						}
488 					} break;
489 				}
490 			}
491 
492 			nk_list_view_end(&lview);
493 		}
494 
495 		const float n = 3;
496 		const float r0 = 1.f / n;
497 		const float r1 = 0.1f / 3;
498 		const float r2 = r0 - r1;
499 		const float footer [6] = {r1, r2, r1, r2, r1, r2};
500 		nk_layout_row(ctx, NK_DYNAMIC, widget_h, 6, footer);
501 		{
502 			const int32_t state_overwrite = _check(ctx, handle->state.overwrite);
503 			if(state_overwrite != handle->state.overwrite)
504 			{
505 				handle->state.overwrite = state_overwrite;
506 				_set_bool(handle, handle->urid.overwrite, handle->state.overwrite);
507 			}
508 			nk_label(ctx, "overwrite", NK_TEXT_LEFT);
509 
510 			const int32_t state_block = _check(ctx, handle->state.block);
511 			if(state_block != handle->state.block)
512 			{
513 				handle->state.block = state_block;
514 				_set_bool(handle, handle->urid.block, handle->state.block);
515 			}
516 			nk_label(ctx, "block", NK_TEXT_LEFT);
517 
518 			const int32_t state_follow = _check(ctx, handle->state.follow);
519 			if(state_follow != handle->state.follow)
520 			{
521 				handle->state.follow = state_follow;
522 				_set_bool(handle, handle->urid.follow, handle->state.follow);
523 			}
524 			nk_label(ctx, "follow", NK_TEXT_LEFT);
525 		}
526 
527 		const bool max_reached = handle->n_item >= MAX_LINES;
528 		nk_layout_row_dynamic(ctx, widget_h, 2);
529 		if(nk_button_symbol_label(ctx,
530 			max_reached ? NK_SYMBOL_TRIANGLE_RIGHT: NK_SYMBOL_NONE,
531 			"clear", NK_TEXT_LEFT))
532 		{
533 			_clear(handle);
534 		}
535 		nk_label(ctx, "Sherlock.lv2: "SHERLOCK_VERSION, NK_TEXT_RIGHT);
536 	}
537 	nk_end(ctx);
538 }
539