1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program 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 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module bioacid
is aliced
;
22 import arsd
.simpledisplay
;
29 import iv
.nanovega
.blendish
;
30 import iv
.nanovega
.textlayouter
;
38 version(sfnt_test
) import iv
.nanovega
.simplefont
;
54 // ////////////////////////////////////////////////////////////////////////// //
55 //__gshared string accountNameToLoad = "_fakeacc";
56 __gshared string accountNameToLoad
= "";
57 __gshared string globalHotkey
= "M-H-F";
58 __gshared
bool optAccountAllowCreate
= false;
59 __gshared mouseStartMarking
= false;
62 //==========================================================================
66 //==========================================================================
67 string
quoteText (const(char)[] text
, int wrapwidth
=73, const(char)[] pfx
=">") {
68 text
= text
.xstripright
;
69 if (text
.length
== 0) return null;
76 bool wasnewline
= true;
80 void flushWord (bool force
) {
81 if (currwidth
&& currwidth
+wordwidth
+1 > wrapwidth
) {
82 if (res
.length
) res
~= "\n";
90 currwidth
+= wordwidth
+1;
92 if (res
.length
) res
~= "\n";
102 void addChar (char ch
) {
103 if (!dc
.decodeSafe(cast(ubyte)ch
)) return;
104 if (dc
.codepoint
<= 32) {
105 if (wordwidth || dc
.codepoint
== 10) {
106 flushWord(dc
.codepoint
== 10);
113 if (dc
.codepoint
== lay
.SoftHyphenCh
) return;
114 if (dc
.codepoint
== lay
.NBHyphenCh
) return;
115 if (dc
.codepoint
== lay
.HyphenPointCh
) return;
116 //if (dc.codepoint == lay.HyphenCh) return;
117 if (dc
.codepoint
== lay
.WordJoinerCh
) return;
118 if (dc
.codepoint
== lay
.ZWSpaceCh
) return;
119 if (dc
.codepoint
== lay
.ZWNBSpaceCh
) return;
121 int ulen
= utf8Encode(buf
[], dc
.codepoint
);
123 currword
~= buf
[0..ulen
];
130 foreach (char ch
; text
) addChar(ch
);
131 if (wordwidth
) flushWord(false);
133 if (res
.length
) res
~= "\n";
137 if (res
.length
) res
~= "\n";
142 //==========================================================================
146 //==========================================================================
147 void main (string
[] args
) {
148 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
150 conRegVar
!accountNameToLoad("starting_account", "account to load");
151 conRegVar
!optAccountAllowCreate("account_allow_creating", "create new account if BioAcid can't load it");
152 conRegVar
!optShowOffline("show_offline", "always show offline contacts?");
153 conRegVar
!optHRLastSeen("hr_lastseen", "human-readable lastseen?");
155 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
157 //glconShowKey = "M-Grave";
158 glconSetAndSealFPS(0); // draw-on-demand
160 conProcessQueue(256*1024); // load config
161 conProcessArgs
!true(args
);
162 conProcessQueue(256*1024);
164 if (accountNameToLoad
.length
== 0) assert(0, "no account to load");
166 //setOpenGLContextVersion(3, 2); // up to GLSL 150
167 setOpenGLContextVersion(2, 0); // it's enough
172 NVGPathSet svp
= null;
174 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
175 sdpyWindowClass
= "BIOACID";
176 sdmain
= new SimpleWindow(800, 600, "BioAcid", OpenGlOptions
.yes
, Resizability
.allowResizing
);
177 glconCtlWindow
= sdmain
;
179 setupToxCoreSender();
180 setupToxEventListener(sdmain
);
182 sdmain
.visibilityChanged
= delegate (bool vis
) { mainWindowVisible
= vis
; fixUnreadIndicators(); };
183 sdmain
.onFocusChange
= delegate (bool focused
) { mainWindowActive
= focused
; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
185 MiniEdit sysedit
= new MiniEdit();
187 MiniEdit
currEdit () { return (activeContact
!is null ? activeContact
.edit
: sysedit
); }
190 if (globalHotkey
.length
> 0) {
191 GlobalHotkeyManager
.register(globalHotkey
, delegate () { concmd("win_toggle"); glconPostDoConCommands
!true(); });
193 } catch (Exception e
) {
194 conwriteln("ERROR registering hotkey!");
198 if (sdmain
!is null) sdmain
.close();
199 })("quit", "quit BioAcid");
202 import core
.memory
: GC
;
203 conwriteln("starting GC collection...");
206 conwriteln("GC collection complete.");
207 })("gc_collect", "force GC collection cycle");
210 if (sdmain
!is null && !sdmain
.closed
) {
211 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
212 if (!mainWindowVisible
) {
213 // this strange code brings window to the current desktop it if was on a different one
216 } else if (sdmain
.visible
) {
223 })("win_toggle", "show/hide main window");
225 sdmain
.addEventListener((GLConScreenRepaintEvent evt
) {
226 if (sdmain
.closed
) return;
227 if (isQuitRequested
) { sdmain
.close(); return; }
228 sdmain
.redrawOpenGlSceneNow();
231 sdmain
.addEventListener((GLConDoConsoleCommandsEvent evt
) {
232 glconProcessEventMessage();
235 sdmain
.addEventListener((PopupCheckerEvent evt
) {
236 popupCheckExpirations();
240 // ////////////////////////////////////////////////////////////////////// //
241 sdmain
.onClosing
= delegate () {
242 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
245 sdmain
.setAsCurrentOpenGlContext();
246 scope(exit
) { flushGui(); sdmain
.releaseCurrentOpenGlContext(); }
251 if (sdhint
!is null) sdhint
.close();
252 if (trayicon
!is null) trayicon
.close();
255 sdmain
.closeQuery
= delegate () {
257 glconPostDoConCommands
!true();
261 sdmain
.visibleForTheFirstTime
= delegate () {
262 if (sdmain
.width
> 1 && optCListWidth
< 0) optCListWidth
= sdmain
.width
/5;
263 sdmain
.setAsCurrentOpenGlContext(); // make this window active
264 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
265 sdmain
.vsync
= false;
267 glconInit(sdmain
.width
, sdmain
.height
);
269 nvg
= nvgCreateContext(NVGContextFlag
.Antialias
, NVGContextFlag
.StencilStrokes
, NVGContextFlag
.FontNoAA
);
270 if (nvg
is null) assert(0, "cannot initialize NanoVG");
274 static immutable skullsPng
= /*cast(immutable(ubyte)[])*/import("data/skulls.png");
275 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
276 auto xi
= loadImageFromMemory(skullsPng
[]);
277 scope(exit
) delete xi
;
278 //{ import core.stdc.stdio; printf("creating background image...\n"); }
279 nvgSkullsImg
= nvg
.createImageFromMemoryImage(xi
, NVGImageFlags
.NoFiltering
, NVGImageFlags
.RepeatX
, NVGImageFlags
.RepeatY
);
280 //{ import core.stdc.stdio; printf("background image created\n"); }
281 if (!nvgSkullsImg
.valid
) assert(0, "cannot load background image");
282 } catch (Exception e
) {
283 assert(0, "cannot load background image");
289 lay
= new LayTextClass(laf
, sdmain
.width
/2);
290 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
291 lastWindowWidth
= sdmain
.width
;
294 loadAccount(accountNameToLoad
, optAccountAllowCreate
);
295 clist
.onActivateContactCB
= delegate (Contact ct
) { doActivateContact(ct
); };
296 addContactCommands();
298 sdmain
.setMinSize(640, 480);
301 //sdmain.redrawOpenGlSceneNow();
304 sdmain
.windowResized
= delegate (int wdt
, int hgt
) {
305 if (sdmain
.closed
) return;
306 glconResize(wdt
, hgt
);
307 glconPostScreenRepaint
/*Delayed*/();
308 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
309 if (wdt
> 1 && optCListWidth
> 0 && lastWindowWidth
> 0 && lastWindowWidth
!= wdt
) {
310 immutable double frc
= lastWindowWidth
/optCListWidth
;
311 optCListWidth
= cast(int)(wdt
/frc
);
312 if (optCListWidth
< 64) optCListWidth
= 64;
313 lastWindowWidth
= wdt
;
319 int mouseX
= -666, mouseY
= -666;
322 sdmain
.handleKeyEvent
= delegate (KeyEvent event
) {
323 if (sdmain
.closed
) return;
324 scope(exit
) glconPostDoConCommands
!true();
325 if (glconKeyEvent(event
)) return;
327 auto acc
= clist
.mainAccount
;
329 if (event
== "D-Escape") {
330 if (sdmain
!is null && !sdmain
.closed
&& sdmain
.visible
) {
337 scope(exit
) { if (event
.pressed
) glconPostScreenRepaint(); }
339 if (event
== "D-C-X") { concmd("quit"); return; }
340 if (event
== "D-C-1") { acc
.status
= ContactStatus
.Online
; return; }
341 if (event
== "D-C-2") { acc
.status
= ContactStatus
.Away
; return; }
342 if (event
== "D-C-3") { acc
.status
= ContactStatus
.Busy
; return; }
343 if (event
== "D-C-0") { acc
.status
= ContactStatus
.Offline
; return; }
345 if (event
== "D-C-W") { doActivateContact(null); return; }
347 if (event
== "D-C-C" || event
== "D-C-Insert") {
348 string text
= lay
.getMarkedText().xstripright
;
350 setClipboardText(sdmain
, text
);
351 setPrimarySelection(sdmain
, text
);
352 setSecondarySelection(sdmain
, text
);
356 if (event
== "D-M-Q") {
357 string text
= quoteText(lay
.getMarkedText());
359 auto etext
= currEdit
.text
;
360 if (etext
.length
&& etext
[$-1] != '\n') currEdit
.addText("\n");
361 currEdit
.addText(text
);
365 if (clist
!is null) {
366 if (clist
.onKey(event
)) return;
370 if (event == "D-C-S-Enter") {
371 static PopupWindow.Kind kind = PopupWindow.Kind.Incoming;
372 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
373 showPopup(kind, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
374 if (kind == PopupWindow.Kind.max) kind = PopupWindow.Kind.min; else ++kind;
379 if (event
== "D-Enter") {
380 auto text
= currEdit
.text
.xstrip
;
382 if (text
.length
== 0) return;
386 while (text
.length
&& text
[0] <= ' ') text
= text
[1..$];
387 if (text
.length
== 0) return null;
388 if (text
[0] == '"' || text
[0] == '\'') {
392 while (text
.length
&& text
[0] != ech
) {
393 if (text
[0] == '\\') {
395 if (text
.length
== 0) break;
398 case '\r': res
~= '\r'; break;
399 case '\n': res
~= '\n'; break;
400 case '\t': res
~= '\t'; break;
401 default: res
~= ch
; break;
408 if (text
.length
) { assert(text
[0] == ech
); text
= text
[1..$]; }
412 while (ep
< text
.length
&& text
[ep
] > ' ') ++ep
;
413 auto res
= text
[0..ep
];
419 if (activeContact
!is null) {
421 if (text
[0] == '/' && !text
.startsWith("/me ") && !text
.startsWith("/me\t")) {
423 auto cmd
= getWord();
425 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false,
426 "/accept -- accept friend request\n"~
427 "/remove -- remove contact\n"~
428 "/kfd -- remove friend, block any further requests\n"~
429 "/status -- set account status\n"~
430 "/pubkey -- get contact public key\n"~
431 "/always -- always visible\n"~
432 "/normal -- normal visibility"~
434 } else if (cmd
== "accept") {
435 if (toxCoreAddFriend(activeContact
.acc
.toxpk
, activeContact
.info
.pubkey
)) {
436 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "accepted!", systimeNow
);
437 activeContact
.kind
= ContactInfo
.Kind
.Friend
;
438 activeContact
.save();
440 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "ERROR accepting!", systimeNow
);
442 } else if (cmd
== "kfd") {
443 toxCoreRemoveFriend(activeContact
.acc
.toxpk
, activeContact
.info
.pubkey
);
444 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "KILL! FUCK! DIE!", systimeNow
);
445 activeContact
.kind
= ContactInfo
.Kind
.KillFuckDie
;
446 activeContact
.save();
447 } else if (cmd
== "remove") {
448 acc
.removeContact(activeContact
);
449 } else if (cmd
== "friend") {
450 if (!isValidAddr(activeContact
.info
.fraddr
)) { conwriteln("invalid address"); return; }
451 string msg
= getWord();
452 if (msg
.length
== 0) { conwriteln("address: '", tox_hex(activeContact
.info
.fraddr
), "'; please, specify message!"); return; }
453 if (msg
.length
> tox_max_friend_request_length()) { conwriteln("address: '", tox_hex(activeContact
.info
.fraddr
), "'; message too long"); return; }
454 if (!acc
.sendFriendRequest(activeContact
.info
.fraddr
, msg
)) { conwriteln("address: '", tox_hex(activeContact
.info
.fraddr
), "'; error sending friend request"); return; }
455 } else if (cmd
== "status") {
456 string msg
= getWord();
457 if (msg
.length
== 0) { conwriteln("current: ", acc
.info
.statusmsg
); return; }
458 if (!toxCoreSetStatusMessage(activeContact
.acc
.toxpk
, msg
)) { conwriteln("ERROR: cannot set status message"); return; }
459 activeContact
.acc
.info
.statusmsg
= msg
;
460 activeContact
.acc
.save();
461 try { activeContact
.acc
.save(); } catch (Exception e
) { conwriteln("ERROR saving account: ", e
.msg
); }
462 } else if (cmd
== "pubkey") {
463 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, tox_hex(activeContact
.info
.pubkey
), systimeNow
);
464 } else if (cmd
== "always") {
465 activeContact
.showOffline
= TriOption
.Yes
;
466 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "always visible", systimeNow
);
467 } else if (cmd
== "normal") {
468 activeContact
.showOffline
= TriOption
.Default
;
469 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "normal visibility", systimeNow
);
471 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "wut?!", systimeNow
);
474 activeContact
.send(text
);
478 if (acc
!is null && text
.length
&& text
[0] == '/') {
481 auto cmd
= getWord();
483 case "friend": // add new friend
484 string addr
= getWord();
485 ToxAddr fraddr
= decodeAddrStr(addr
);
486 if (!isValidAddr(fraddr
)) { conwriteln("invalid address: '", addr
, "'"); return; }
487 string msg
= getWord();
488 if (msg
.length
== 0) { conwriteln("address: '", tox_hex(fraddr
), "'; please, specify message!"); return; }
489 if (msg
.length
> tox_max_friend_request_length()) { conwriteln("address: '", addr
, "'; message too long"); return; }
490 //if (!toxCoreSendFriendRequest(acc.toxpk, fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request");
491 if (!acc
.sendFriendRequest(fraddr
, msg
)) { conwriteln("address: '", addr
, "'; error sending friend request"); return; }
494 string msg
= getWord();
495 if (msg
.length
== 0) { conwriteln("current: ", acc
.info
.statusmsg
); return; }
496 if (!toxCoreSetStatusMessage(acc
.toxpk
, msg
)) { conwriteln("ERROR: cannot set status message"); return; }
497 acc
.info
.statusmsg
= msg
;
498 activeContact
.acc
.save();
499 try { acc
.save(); } catch (Exception e
) { conwriteln("ERROR saving account: ", e
.msg
); }
502 currEdit
.text
= acc
.getAddress();
505 addTextToLog(null, null, LogFile
.Msg
.Kind
.Notification
, false,
506 "/friend addr msg -- send friend request\n"~
507 "/status -- set account status\n"~
508 "/myaddr -- put my address into the editor"~
512 conwriteln("unknown command: '", cmd
, "'");
516 if (text
.length
) conwriteln("NOT A COMMAND: ", text
);
522 if (currEdit
.onKey(event
)) return;
525 int msLastPressX
= -666, msLastPressY
= -666;
526 int msDoLogButton
= 0; // =0: none; 1: left; 2: middle; 3: right; negative: release
528 sdmain
.handleMouseEvent
= delegate (MouseEvent event
) {
529 if (sdmain
.closed
) return;
530 scope(exit
) glconPostDoConCommands
!true();
531 if (isConsoleVisible
) return;
535 // check for click in log
536 //FIXME: process it here, not in renderer
537 if (event
== "LMB-Down") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= 1; glconPostScreenRepaint(); }
538 if (event
== "MMB-Down") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= 2; glconPostScreenRepaint(); }
539 if (event
== "RMB-Down") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= 3; glconPostScreenRepaint(); }
540 if (event
== "LMB-Up") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= -1; glconPostScreenRepaint(); }
541 if (event
== "MMB-Up") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= -2; glconPostScreenRepaint(); }
542 if (event
== "RMB-Up") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= -3; glconPostScreenRepaint(); }
544 if (clist
!is null) {
545 if (clist
.onMouse(event
)) {
546 mouseStartMarking
= false;
549 glconPostScreenRepaint();
556 if (layWinHeight
> 0) {
557 enum ScrollHeight
= 32;
558 if (event
== "WheelUp") {
559 layOffset
+= ScrollHeight
;
560 if (layOffset
> lay
.textHeight
-layWinHeight
) layOffset
= lay
.textHeight
-layWinHeight
;
561 } else if (event
== "WheelDown") {
562 layOffset
-= ScrollHeight
;
564 if (layOffset
< 0) layOffset
= 0;
567 // don't spam with repaint events
568 if (event
.type
!= MouseEventType
.motion
) {
569 glconPostScreenRepaint();
571 if (mouseStartMarking
) {
573 msLastPressX
= mouseX
;
574 msLastPressY
= mouseY
;
575 glconPostScreenRepaint();
580 sdmain
.handleCharEvent
= delegate (dchar ch
) {
581 if (sdmain
.closed
) return;
582 scope(exit
) glconPostDoConCommands
!true();
583 if (glconCharEvent(ch
)) return;
585 scope(exit
) glconPostScreenRepaint();
587 if (currEdit
.onChar(ch
)) return;
591 sdmain
.redrawOpenGlScene
= delegate () {
592 glconPostDoConCommands
!true();
593 if (sdmain
.closed
) return;
594 sdmain
.setAsCurrentOpenGlContext(); // make this window active
595 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
596 glViewport(0, 0, sdmain
.width
, sdmain
.height
);
599 scope(exit
) glconDraw();
601 if (nvg
is null) return;
603 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
604 glViewport(0, 0, sdmain
.width
, sdmain
.height
);
605 glMatrixMode(GL_MODELVIEW
);
606 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
608 glClearColor(0, 0, 0, 0);
609 glClear(glNVGClearFlags
/*|GL_COLOR_BUFFER_BIT*/);
612 nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
613 scope(exit
) nvg
.endFrame();
615 if (clist
!is null && optCListWidth
> 0) {
619 int wdt
= optCListWidth
;
620 int hgt
= nvg
.height
-cy
*2;
622 nvg
.shapeAntiAlias
= true;
628 scope(exit
) nvg
.restore();
631 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
634 nvg
.imageSize(nvgSkullsImg
, w
, h
);
635 nvg
.fillPaint(nvg
.imagePattern(0, 0, w
, h
, 0, nvgSkullsImg
));
637 nvg
.strokeColor(NVGColor("#f70"));
644 scope(exit
) nvg
.restore();
645 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
646 clist
.drawAt(nvg
, cx
+3, cy
+3, wdt
-3*2, hgt
-3*2);
649 if (clist
.sbSize
> 0) {
650 nvg
.bndScrollBar(cx
+3+(wdt
-3*2)+3-BND_SCROLLBAR_WIDTH
+0.5f, cy
+1+0.5f, BND_SCROLLBAR_WIDTH
, hgt
-2, BND_DEFAULT
, clist
.sbPosition
, clist
.sbSize
);
656 scope(exit
) nvg
.restore();
660 wdt
= nvg
.width
-cx
-1;
663 // calculate editor dimensions and draw editor
666 scope(exit
) nvg
.restore();
668 currEdit
.setWidth(wdt
-3*2);
670 auto edheight
= currEdit
.calcHeight();
672 int edy
= cy
+hgt
-edheight
-3*2;
675 nvg
.roundedRect(cx
+0.5f, edy
+0.5f, wdt
, edheight
+3*2, 6);
676 nvg
.fillColor(NVGColor
.black
);
677 nvg
.strokeColor(NVGColor("#f70"));
681 nvg
.intersectScissor(cx
+2.5f, edy
+2.5f, wdt
-3*2+2, edheight
+2);
682 currEdit
.draw(nvg
, cx
+3, edy
+3);
688 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
689 nvg
.fillColor(NVGColor
.black
);
690 nvg
.strokeColor(NVGColor("#f70"));
694 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
696 immutable float scalex
= (wdt
-3*2-10*2)/BaphometDims
;
697 immutable float scaley
= (baphHgt
-3*2-10*2)/BaphometDims
;
698 immutable float scale
= (scalex
< scaley ? scalex
: scaley
)/1.5f;
699 immutable float sz
= BaphometDims
*scale
;
700 nvg
.strokeColor(NVGColor("#400"));
701 nvg
.fillColor(NVGColor("#400"));
702 nvg
.renderBaphomet(cx
+10.5f+(wdt
-3*2-10*2)/2-sz
/2, cy
+10.5f+(baphHgt
-3*2-10*2)/2-sz
/2, scale
, scale
);
705 immutable float sbx
= cx
+wdt
-BND_SCROLLBAR_WIDTH
-1.5f;
706 immutable float sby
= cy
+3.5f;
707 wdt
-= BND_SCROLLBAR_WIDTH
+1;
711 lay
.relayout(wdt
); // this is harmess if width wasn't changed
712 int ty
= lay
.textHeight
-hgt
+1-layOffset
;
714 layWinHeight
= cast(int)hgt
;
716 if (lay
.textHeight
> hgt
) {
717 float h
= lay
.textHeight
-hgt
;
718 nvg
.bndScrollBar(sbx
, sby
, BND_SCROLLBAR_WIDTH
, hgt
, BND_DEFAULT
, ty
/h
, hgt
/h
);
720 nvg
.bndScrollBar(sbx
, sby
, BND_SCROLLBAR_WIDTH
, hgt
, BND_DEFAULT
, 1, 1);
723 nvg
.intersectScissor(cx
+2.5f, cy
+2.5f, wdt
+2, hgt
+2);
724 nvg
.drawLayouter(lay
, ty
, cx
+3, cy
+3, hgt
);
726 //conwriteln("msDoLogButton=", msDoLogButton, "; mouseStartMarking=", mouseStartMarking);
728 immutable int btn
= msDoLogButton
;
730 immutable int mx
= msLastPressX
-(cx
+3);
731 immutable int my
= msLastPressY
-(cy
+3);
732 if (mx
>= 0 && my
>= 0 && mx
< wdt
&& my
< hgt
) {
733 //conwriteln("MOUSE: ", mx, " : ", my, "; ty=", ty, "; thgt=", lay.textHeight);
734 auto widx
= lay
.wordAtXY(mx
, ty
+my
);
735 //conwriteln(" widx=", widx);
737 auto w
= lay
.wordByIndex(widx
);
738 //conwriteln(" widx=", widx, "; <", lay.wordText(*w), ">; udata=", w.udata);
741 string url
= findUrlByWordIndex(widx
);
742 if (!mouseStartMarking
&& url
.length
) {
743 //conwriteln("URL CLICK: <", url, ">");
745 mouseStartMarking
= false;
748 if (!mouseStartMarking
) {
749 //conwriteln("MARK START!");
750 mouseStartMarking
= true;
751 lay
.setMark(lay
.MarkType
.Both
, mx
, ty
+my
);
752 glconPostScreenRepaint();
757 if (btn
== 3 && w
.objectIdx
>= 0) {
758 if (auto maw
= cast(MessageOutMark
)lay
.objectAtIndex(w
.objectIdx
)) {
759 assert(activeContact
!is null);
760 activeContact
.removeFromResendQueue(maw
.msgid
, maw
.digest
, maw
.time
);
763 glconPostScreenRepaint();
769 if (mouseStartMarking
&& btn
== 666) {
770 //conwriteln("MARK CONTINUE!");
771 //conwriteln(" before: ", lay.getMarkWordFirst, ":", lay.getMarkWordCount);
772 lay
.setMark(lay
.MarkType
.End
, mx
, ty
+my
);
773 //conwriteln(" after: ", lay.getMarkWordFirst, ":", lay.getMarkWordCount);
774 glconPostScreenRepaint();
778 if (mouseStartMarking
) {
779 mouseStartMarking
= false;
780 //conwriteln("MARK END!");
790 MonoTime lastCollect
= MonoTime
.currTime
;
791 sdmain
.eventLoop(16000,
792 // pulser: process resend queues here
794 if (sdmain
.closed || clist
is null) return;
795 clist
.forEachAccount(delegate (Account acc
) { acc
.processResendQueue(); });
797 immutable ctt
= MonoTime
.currTime
;
798 if ((ctt
-lastCollect
).total
!"minutes" >= 1) {
799 import core
.memory
: GC
;
807 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
808 clist
.forEachAccount(delegate (Account acc
) { acc
.saveResendQueue(); acc
.saveResendQueue(); });
809 toxCoreShutdownAll();
812 conProcessQueue(int.max
/4);