-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathsvggui.cpp
1922 lines (1752 loc) · 75.5 KB
/
svggui.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "svggui.h"
#include <chrono>
#include "usvg/svgparser.h"
#include "usvg/svgpainter.h"
#include "usvg/svgwriter.h"
#define LAY_IMPLEMENTATION
#define LAY_FORCE_INLINE // we don't need aggressive inlineing
#include "layout.h"
#ifdef SVGGUI_MULTIWINDOW
#error "Fix multiwindow for touch events, etc."
#endif
// seems we could use an unordered_set to do this more simply - add all ancestors of one node to set, walk
// up tree from other node until we find one in the set
static Widget* commonParent(const Widget* wa, const Widget* wb)
{
SvgNode* a = wa->node;
SvgNode* b = wb->node;
std::vector<const SvgNode*> pathA;
std::vector<const SvgNode*> pathB;
while(a) {
pathA.push_back(a);
a = a->parent();
}
while(b) {
pathB.push_back(b);
b = b->parent();
}
size_t ii = 1;
while(ii <= std::min(pathA.size(), pathB.size()) && pathA[pathA.size() - ii] == pathB[pathB.size() - ii])
++ii;
const SvgNode* p = ii > 1 ? pathA[pathA.size() - ii + 1] : NULL;
return p ? static_cast<Widget*>(p->ext()) : NULL;
}
static bool isDescendant(const Widget* child, const Widget* parent)
{
if(!child || !parent)
return false;
const SvgNode* childnode = child->node;
const SvgNode* parentnode = parent->node;
while(childnode) {
if(childnode == parentnode)
return true;
childnode = childnode->parent();
}
return false;
}
bool Widget::isDescendantOf(Widget* parent) const { return isDescendant(this, parent); }
static Widget* getPressedGroupContainer(Widget* widget)
{
Widget* container = widget;
// find top-most pressed group container
while((widget = widget->parent()))
if(widget->isPressedGroupContainer && widget->isVisible()) // and enabled? stop search at first non-visible widget?
container = widget;
return container;
}
// createExt has been disabled for Widgets; ext now created only in prepareLayout() or Widget::selectFirst()
Widget::Widget(SvgNode* n) : SvgNodeExtension(n), m_margins(Rect::ltrb(0,0,0,0)) {}
//Widget::~Widget() { window()->gui()->widgetDeleted(this); }
void Widget::removeFromParent()
{
SvgNode* p = node->parent();
if(p)
p->asContainerNode()->removeChild(node);
}
void Widget::setEnabled(bool enabled)
{
if(enabled == m_enabled)
return;
if(enabled)
node->removeClass("disabled");
else
node->addClass("disabled");
// don't like using window()->gui(), but don't want to require SvgGui::setEnabled(Widget*)
Window* win = window();
if(win && win->gui()) {
win->gui()->onHideWidget(this); // this seems appropriate since disabled widget can't receive events
sdlUserEvent(win->gui(), enabled ? SvgGui::ENABLED : SvgGui::DISABLED);
}
m_enabled = enabled;
}
void Widget::setVisible(bool visible)
{
Window* win = window();
bool displayed = isDisplayed();
if(displayed != visible && win && win->gui())
sdlUserEvent(win->gui(), visible ? SvgGui::VISIBLE : SvgGui::INVISIBLE);
if(widgetClass() == AbsPosWidgetClass) { //StringRef(node->getStringAttr("position")) == "absolute") {
AbsPosWidget* w = static_cast<AbsPosWidget*>(this);
node->setDisplayMode(visible ? SvgNode::AbsoluteMode : SvgNode::NoneMode);
if(win) {
auto& absPosNodes = win->absPosNodes;
auto it = std::find(absPosNodes.begin(), absPosNodes.end(), w);
if(!visible && it != absPosNodes.end()) {
absPosNodes.erase(it);
Rect r = node->m_renderedBounds;
if(w->m_shadow) r.rectUnion(w->m_shadow->bounds(node->m_renderedBounds));
win->gui()->closedWindowBounds.rectUnion(r.translate(win->winBounds().origin()));
}
else if(visible && it == absPosNodes.end())
absPosNodes.push_back(w);
}
}
else
node->setDisplayMode(visible ? SvgNode::BlockMode : SvgNode::NoneMode);
// do this after setting NoneMode so we don't unnecessarily call onHideWidget for any children (e.g. menus)
if(displayed && !visible && win && win->gui())
win->gui()->onHideWidget(this);
}
Widget* Widget::cloneNode() const
{
SvgNode* newnode = node->clone();
return static_cast<Widget*>(newnode->ext());
}
Widget* Widget::parent() const
{
return node->parent() ? static_cast<Widget*>(node->parent()->ext(false)) : NULL;
}
Window* Widget::window() const
{
SvgDocument* root = node->rootDocument();
return root ? static_cast<Window*>(root->ext(false)) : NULL;
}
// experimental
void Widget::setText(const char* s)
{
SvgText* textnode = NULL;
if(node->type() == SvgNode::TEXT)
textnode = static_cast<SvgText*>(node);
else if(containerNode())
textnode = static_cast<SvgText*>(containerNode()->selectFirst("text"));
if(textnode)
textnode->setText(s);
}
Widget* Widget::selectFirst(const char* selector) const
{
SvgNode* hit = node->selectFirst(selector);
return hit ? (hit->hasExt() ? static_cast<Widget*>(hit->ext()) : new Widget(hit)) : NULL;
}
std::vector<Widget*> Widget::select(const char* selector) const
{
std::vector<SvgNode*> hits = node->select(selector);
std::vector<Widget*> result;
for(SvgNode* hit : hits)
result.push_back(static_cast<Widget*>(hit->ext()));
return result;
}
static real offsetToPx(const SvgLength& len, real ref)
{
return (len.units == SvgLength::PERCENT) ? ref*len.value/100 : len.value;
}
Point AbsPosWidget::calcOffset(const Rect& parentbbox) const
{
Point dr(0,0);
Rect bbox = node->bounds();
if(offsetLeft.isValid())
dr.x = parentbbox.left + offsetToPx(offsetLeft, parentbbox.width()) - bbox.left;
else if(offsetRight.isValid())
dr.x = parentbbox.right - offsetToPx(offsetRight, parentbbox.width()) - bbox.right;
if(offsetTop.isValid())
dr.y = parentbbox.top + offsetToPx(offsetTop, parentbbox.height()) - bbox.top;
else if(offsetBottom.isValid())
dr.y = parentbbox.bottom - offsetToPx(offsetBottom, parentbbox.height()) - bbox.bottom;
return dr;
}
void Widget::addWidget(Widget* child)
{
ASSERT(containerNode() && "cannot add widget to non-container node");
ASSERT(!child->parent() && "Widget already has parent");
containerNode()->addChild(child->node);
// should we allow chaining?
//return *this;
}
// I think a better way to handle custom attributes that need to be subject to CSS is to store as
// SvgAttrs and provide either createExt or parseAttribute fn to SvgParser
// ... but for legacy reasons, we will keep layout attrs as strings and cache parsed values in Widget
// caching layout vars instead of parsing in prepareLayout or with parseAttribute (and combining in
// prepareLayout) is almost certainly a premature and useless optimization ... I'm keeping it for now to
// verify the general idea of being able to cache values computed from custom attributes (subject to CSS)
// Feel free to remove this after verifying no performance issues
void Widget::onAttrChange(const char* name)
{
static const char* layoutAttrs[] = {"left", "top", "right", "bottom", "box-anchor", "layout",
"flex-direction", "flex-wrap", "justify-content", "flex-break", "box-shadow", "border-radius"};
StringRef nameref(name);
if(layoutVarsValid && (nameref.startsWith("margin") || indexOfStr(nameref, layoutAttrs) >= 0)) {
layoutVarsValid = false;
node->setDirty(SvgNode::CHILD_DIRTY);
}
}
void Widget::updateLayoutVars()
{
StringRef marginstr = node->getStringAttr("margin");
if(!marginstr.isEmpty()) {
std::vector<real> margin;
parseNumbersList(marginstr, margin);
if(margin.size() == 0)
margin = {0, 0, 0, 0}; // top right bottom left
else if(margin.size() == 1) // all equal
margin = {margin[0], margin[0], margin[0], margin[0]};
else if(margin.size() == 2) // t&b l&r
margin = {margin[0], margin[1], margin[0], margin[1]};
else if(margin.size() == 3) // t r&l b
margin = {margin[0], margin[1], margin[2], margin[1]};
m_margins = Rect::ltrb(margin[3], margin[0], margin[1], margin[2]);
}
else
m_margins = Rect::ltrb(0, 0, 0, 0);
// we should remove margin-left, etc., since margin attribute does not cascade and default value is 0,
// unlike (offset)"left", etc. where 0 is not the same as the attribute not being set
m_margins.left = toReal(node->getStringAttr("margin-left"), m_margins.left);
m_margins.top = toReal(node->getStringAttr("margin-top"), m_margins.top);
m_margins.right = toReal(node->getStringAttr("margin-right"), m_margins.right);
m_margins.bottom = toReal(node->getStringAttr("margin-bottom"), m_margins.bottom);
//parseLength(node->getStringAttr("max-width"), NaN);
//parseLength(node->getStringAttr("max-height"), NaN);
layContain = 0;
StringRef layout = node->getStringAttr("layout", "");
if(!layout.isEmpty()) {
layContain = LAYX_HASLAYOUT;
layContain |= layout == "box" ? LAY_LAYOUT : 0;
layContain |= layout == "flex" ? LAY_FLEX : 0;
StringRef flexdir = node->getStringAttr("flex-direction", "");
layContain |= flexdir == "row" ? LAY_ROW : 0;
layContain |= flexdir == "column" ? LAY_COLUMN : 0;
// main motivation for supporting row/column-reverse is to allow item appearing to left or above another
// to come after it in SVG so that it has higher z-index
layContain |= flexdir == "row-reverse" ? (LAY_ROW | LAYX_REVERSE) : 0;
layContain |= flexdir == "column-reverse" ? (LAY_COLUMN | LAYX_REVERSE) : 0;
if(StringRef(node->getStringAttr("flex-wrap", "")) == "wrap")
layContain |= LAY_WRAP;
StringRef justify = node->getStringAttr("justify-content", "");
layContain |= justify == "flex-start" ? LAY_START : 0;
layContain |= justify == "flex-end" ? LAY_END : 0;
layContain |= justify == "center" ? LAY_CENTER : 0;
layContain |= justify == "space-between" ? LAY_JUSTIFY : 0;
}
layBehave = 0;
StringRef anchor = node->getStringAttr("box-anchor", "");
if(!anchor.isEmpty()) {
if(anchor == "fill")
layBehave |= LAY_FILL;
else {
if(anchor.contains("left") || anchor.contains("hfill"))
layBehave |= LAY_LEFT;
if(anchor.contains("top") || anchor.contains("vfill"))
layBehave |= LAY_TOP;
if(anchor.contains("right") || anchor.contains("hfill"))
layBehave |= LAY_RIGHT;
if(anchor.contains("bottom") || anchor.contains("vfill"))
layBehave |= LAY_BOTTOM;
}
}
if(StringRef(node->getStringAttr("flex-break", "")) == "before")
layBehave |= LAY_BREAK;
m_shadow.reset();
const char* shadow = node->getStringAttr("box-shadow");
if(shadow) {
m_shadow = std::make_unique<BoxShadow>();
StringRef shd(shadow);
m_shadow->dx = parseLength(shd, SvgLength(0)).value;
shd.advance(shd.find(" ")).trimL();
m_shadow->dy = parseLength(shd, SvgLength(0)).value;
shd.advance(shd.find(" ")).trimL();
if(!shd.isEmpty() && isDigit(shd[0])) {
m_shadow->blur = parseLength(shd, SvgLength(0)).value; // nanovg "feather"
shd.advance(shd.find(" ")).trimL();
}
if(!shd.isEmpty() && (isDigit(shd[0]) || shd[0] == '-')) { // spread can be negative
m_shadow->spread = parseLength(shd, SvgLength(0)).value; // padding for rect
shd.advance(shd.find(" ")).trimL();
}
if(shd.startsWith("inset"))
shd.advance(5).trimL();
m_shadow->color = parseColor(shd, Color::BLACK);
//if(std::isnan(dx) || std::isnan(dy) || std::isnan(blur) || std::isnan(spread))
}
StringRef radiusstr = node->getStringAttr("border-radius");
std::vector<real> radii;
parseNumbersList(radiusstr, radii);
if(node->type() == SvgNode::RECT) {
SvgRect* rnode = static_cast<SvgRect*>(node);
if(radii.size() == 1) // all equal
rnode->setCornerRadii(radii[0], radii[0], radii[0], radii[0]);
else if(radii.size() == 4) // top-left | top-right | bottom-right | bottom-left
rnode->setCornerRadii(radii[0], radii[1], radii[2], radii[3]);
else
rnode->setCornerRadii(0, 0, 0, 0);
}
if(m_shadow)
m_shadow->radius = radii.empty() ? 0 : radii[0];
layoutVarsValid = true;
}
void AbsPosWidget::updateLayoutVars()
{
offsetLeft = parseLength(node->getStringAttr("left"), NaN);
offsetTop = parseLength(node->getStringAttr("top"), NaN);
offsetRight = parseLength(node->getStringAttr("right"), NaN);
offsetBottom = parseLength(node->getStringAttr("bottom"), NaN);
Widget::updateLayoutVars();
}
const Rect& Widget::margins() const
{
// this is only used by prepareLayout; we want to preserve layoutVarsValid for needsLayout()
//if(!layoutVarsValid)
// const_cast<Widget*>(this)->updateLayoutVars(); // don't feel like marking all the vars mutable
return m_margins;
}
void Widget::setMargins(real t, real r, real b, real l)
{
// if margins change, we want layoutVarsValid to be cleared for needsLayout()
//bool wasvalid = layoutVarsValid;
m_margins = Rect::ltrb(l, t, r, b);
node->setAttribute("margin", fstring("%g %g %g %g", t, r, b, l).c_str());
//layoutVarsValid = wasvalid;
}
// all layout attributes are preserved on node, so they'll be written automatically
// for now, we assume serialization is only used for debugging, so write layout transform
void Widget::serializeAttr(SvgWriter* writer)
{
if(!m_layoutTransform.isIdentity()) {
char* buff = SvgWriter::serializeTransform(writer->xml.getTemp(), m_layoutTransform);
writer->xml.writeAttribute("layout:transform", buff);
}
}
void Widget::setLayoutTransform(const Transform2D& tf)
{
if(tf != m_layoutTransform) {
//const real* m0 = m_layoutTransform.asArray();
//const real* m1 = tf.asArray();
#if 0 //def NDEBUG
if(m1[0] == m0[0] && m1[1] == m0[1] && m1[2] == m0[2] && m1[3] == m0[3]) {
if(node->cachedBounds().isValid())
translateCachedBounds(node, Point(m1[4] - m0[4], m1[5] - m0[5]));
node->setDirty(SvgNode::BOUNDS_DIRTY);
}
else
#endif
node->invalidate(true);
m_layoutTransform = tf;
}
}
static void drawShadow(SvgPainter* svgp, const Widget* w)
{
Rect bounds = w->node->bounds();
//bounds.translate(w->window()->winBounds().origin());
if(!bounds.intersects(svgp->dirtyRect)) return;
Painter* p = svgp->p;
Widget::BoxShadow* shd = w->m_shadow.get();
bounds.pad(shd->spread);
Rect shdbnds = bounds.toSize().pad(0.5*shd->blur + 1).translate(shd->dx, shd->dy);
p->save();
p->setTransform(svgp->initialTransform);
p->translate(bounds.origin()); // - w->m_layoutTransform.map(Point(0,0)));
Gradient grad = Gradient::box(shd->dx, shd->dy, bounds.width(), bounds.height(), shd->radius, shd->blur);
grad.coordMode = Gradient::userSpaceOnUseMode;
//grad.setObjectBBox(Rect::ltwh(d, props.height, props.width, d));
grad.addStop(0, shd->color);
grad.addStop(1, Color(shd->color).setAlphaF(0));
p->setFillBrush(&grad);
p->drawRect(shdbnds);
p->restore();
}
void Widget::applyStyle(SvgPainter* svgp) const
{
// `svgp->p->transform(m_layoutTransform)` works w/ the corresponding code using totalTransform() in
// setLayoutBounds(), except for a few glitches (e.g. w/ first layout of menus)
// I think our original (and current) approach of applying layout translation before SVG transform and
// layout scaling after is probably better ... we should probably implement paintScale separately from
// Painter transform (just divide by scale in Painter::getTransform())
if(m_layoutTransform.isIdentity())
return;
Transform2D tf = svgp->p->getTransform();
// We should think of layout transform not as a proper transform, but as a way of keeping track of the
// translation and scaling to be applied in a special way to the node
// We apply layout transform directly and assume both it and initialTransform have only scale and translation
tf.m[0] *= m_layoutTransform.xscale();
tf.m[3] *= m_layoutTransform.yscale();
tf.m[4] += m_layoutTransform.xoffset() * svgp->initialTransform.xscale();
tf.m[5] += m_layoutTransform.yoffset() * svgp->initialTransform.yscale();
svgp->p->setTransform(tf);
// valid dirty rect indicates rendering (as opposed to bounds calculation)
if(svgp->dirtyRect.isValid() && m_shadow)
drawShadow(svgp, this);
}
// note we use rbegin/rend, so handlers added later have priority! Add flag to addHandler to control this?
bool Widget::sdlEvent(SvgGui* gui, SDL_Event* event)
{
if(sdlHandlers.empty() || !isEnabled())
return false;
gui->eventWidget = this;
gui->currSDLEvent = event;
for(auto it = sdlHandlers.rbegin(); it != sdlHandlers.rend(); ++it) {
if((*it)(gui, event))
return true;
}
return false;
}
// send a SDL event directly to a widget (without going through event queue)
bool Widget::sdlUserEvent(SvgGui* gui, Uint32 type, Sint32 code, void* data1, void* data2)
{
SDL_Event event = {0};
event.type = type;
event.user.code = code;
event.user.timestamp = SDL_GetTicks();
event.user.data1 = data1;
event.user.data2 = data2;
return sdlEvent(gui, &event);
}
void Window::setWinBounds(const Rect& r)
{
if(r != mBounds && mBounds.width() > 0 && mBounds.height() > 0 && svgGui && !sdlWindow)
svgGui->closedWindowBounds.rectUnion(mBounds); // handle change in window size
if(node->type() == SvgNode::DOC && r.toSize() != mBounds.toSize()) {
// width or height in % units w/o valid canvasRect will cause doc bounds() to return content bounds
documentNode()->setWidth(r.width() > 0 ? SvgLength(r.width()) : SvgLength(100, SvgLength::PERCENT));
documentNode()->setHeight(r.height() > 0 ? SvgLength(r.height()) : SvgLength(100, SvgLength::PERCENT));
}
mBounds = r;
}
Window* Window::rootWindow()
{
Window* win = this;
while(win->parentWindow)
win = win->parentWindow;
return win;
}
void Window::setTitle(const char* title)
{
winTitle = title;
Widget* titleWidget = selectFirst(".window-title");
if(titleWidget)
titleWidget->setText(title);
if(sdlWindow)
SDL_SetWindowTitle(sdlWindow, title);
}
void Window::setWindowXmlClass(const char* xmlcls)
{
if(xmlcls == windowXmlClass) return;
node->setXmlClass(addWord(removeWord(node->xmlClass(), windowXmlClass), xmlcls).c_str());
windowXmlClass = xmlcls;
}
bool SvgGui::debugLayout = false;
bool SvgGui::debugDirty = false;
SvgGui::SvgGui()
{
lay_context* ctx = &layoutCtx;
lay_init_context(ctx);
lay_reserve_items_capacity(ctx, 1024);
}
// user is now responsible for deleting Windows
SvgGui::~SvgGui()
{
ASSERT(windows.empty() && "All windows must be closed before deleting SvgGui object.");
lay_destroy_context(&layoutCtx);
if(timerThread) {
nextTimeout = 0;
timerSem.post();
timerThread->join();
}
}
// add an SDL event to the global event queue
void SvgGui::pushUserEvent(Uint32 type, Sint32 code, void* data1, void* data2)
{
SDL_Event event = {0};
event.type = type;
event.user.code = code;
event.user.timestamp = SDL_GetTicks();
event.user.data1 = data1;
event.user.data2 = data2;
SDL_PeepEvents(&event, 1, SDL_ADDEVENT, 0, 0); //SDL_PushEvent(&event);
PLATFORM_WakeEventLoop();
}
void SvgGui::delayDeleteWin(Window* win) { pushUserEvent(DELETE_WINDOW, 0, win); }
static int timerThreadFn(void* _self)
{
SvgGui* self = static_cast<SvgGui*>(_self);
while(1) {
while(self->nextTimeout == MAX_TIMESTAMP)
self->timerSem.wait();
if(self->nextTimeout <= 0)
break;
int64_t dt = self->nextTimeout - mSecSinceEpoch();
if(dt < 0 || !self->timerSem.waitForMsec(dt)) {
//SvgGui::pushUserEvent(SvgGui::TIMER, 0)
SDL_Event event = {0};
event.type = SvgGui::TIMER;
event.user.timestamp = SDL_GetTicks(); // self->nextTimeout???
SDL_PushEvent(&event);
PLATFORM_WakeEventLoop();
// main loop will update nextTimeout and signal the semaphore
self->timerSem.wait();
}
}
return 0;
}
// If widget associated with Timer is deleted (or parent window closed), timer will be removed
// Other options would be for Widget destructor to ensure timers are removed or to use class for timer
// handle which automatically removes timer on destruction
// Timer can have callback; if callback omitted, timer sends event to widget's sdlEvent instead - widget
// can only have one such default timer; we could consider Widget::setTimer(), removeTimer()
// Initially timers had no callback (just Widget + code), but things like long press are too messy w/o a
// callback
Timer* SvgGui::setTimer(int msec, Widget* widget, const std::function<int()>& callback)
{
ASSERT(msec > 0);
#if PLATFORM_EMSCRIPTEN
//#error "Don't forget to fix this"
#else
if(!timerThread)
timerThread.reset(new std::thread(timerThreadFn, (void*)this));
#endif
// remove default timer for widget if setting default timer
if(!callback)
removeTimer(widget);
Timer timer(msec, widget, callback);
timer.nextTick = mSecSinceEpoch() + msec;
if(timer.nextTick < nextTimeout) {
nextTimeout = timer.nextTick;
timerSem.post();
}
return &*timers.insert(std::lower_bound(timers.begin(), timers.end(), timer), std::move(timer));
}
Timer* SvgGui::setTimer(int msec, Widget* widget, Timer* oldtimer, const std::function<int()>& callback)
{
removeTimer(oldtimer);
return setTimer(msec, widget, callback);
}
// it is not necessary to update nextTimeout when removing timers - this will be handled in processTimers()
void SvgGui::removeTimer(Timer* toremove)
{
if(toremove)
timers.remove_if([toremove](const Timer& timer) { return &timer == toremove; });
}
// remove default timer for widget
void SvgGui::removeTimer(Widget* w)
{
timers.remove_if([w](const Timer& timer) { return timer.widget == w && !timer.callback; });
}
void SvgGui::removeTimers(Widget* w, bool children)
{
if(children)
timers.remove_if([w](const Timer& timer) { return isDescendant(timer.widget, w); });
else
timers.remove_if([w](const Timer& timer) { return timer.widget == w; });
}
// be wary of trying to refactor this: many branches + reentrant + used multiple places = very complex logic
static lay_id prepareLayout(lay_context* ctx, Widget* ext)
{
SvgNode* node = ext->node;
lay_id id = lay_item(ctx);
ext->setLayoutId(id);
//ext->setLayoutTransform(Transform2D());
if(!ext->layoutVarsValid)
ext->updateLayoutVars();
Rect bbox;
if(ext->onPrepareLayout && (bbox = ext->onPrepareLayout()).isValid()) {}
else if(node->asContainerNode() && (ext->layContain & Widget::LAYX_HASLAYOUT)) { //node->hasAttribute("layout")) {
for(SvgNode* child : node->asContainerNode()->children()) {
if(!child->isVisible() || child->displayMode() == SvgNode::AbsoluteMode)
continue;
Widget* w = child->hasExt() ? static_cast<Widget*>(child->ext()) : new Widget(child);
w->setLayoutId(-1); // reset layout id
// or should we iterate in reverse order?
if(ext->layContain & Widget::LAYX_REVERSE)
lay_push(ctx, id, prepareLayout(ctx, w)); // prepends
else
lay_insert(ctx, id, prepareLayout(ctx, w)); // appends
}
if(node->type() == SvgNode::DOC) {
SvgDocument* doc = static_cast<SvgDocument*>(node);
real w = doc->width().isPercent() ? 0 : doc->m_width.value;
real h = doc->height().isPercent() ? 0 : doc->m_height.value;
bbox = Rect::wh(w, h);
}
}
else {
bbox = node->bounds();
// moved here from setLayoutBounds
if(bbox.width() <= 0 || bbox.height() <= 0) {
ext->setLayoutTransform(Transform2D());
bbox = node->bounds();
}
}
const Rect& m = ext->margins(); //* ext->window()->gui()->globalScale;
lay_set_margins_ltrb(ctx, id, m.left, m.top, m.right, m.bottom);
lay_set_contain(ctx, id, ext->layContain & LAY_ITEM_BOX_MASK);
lay_set_behave(ctx, id, ext->layBehave & LAY_ITEM_LAYOUT_MASK);
if(bbox.isValid()) {
float w = (ext->layBehave & LAY_HFILL) != LAY_HFILL ? bbox.width() : 0; //int(bbox.width() + 0.5)
float h = (ext->layBehave & LAY_VFILL) != LAY_VFILL ? bbox.height() : 0; //int(bbox.height() + 0.5)
if(w > 0 || h > 0)
lay_set_size_xy(ctx, id, w, h); // this will fix size of node
}
return id;
}
static void applyLayout(lay_context* ctx, Widget* ext)
{
SvgNode* node = ext->node;
if(ext->layoutId() < 0)
return;
lay_vec4 r = lay_get_rect(ctx, ext->layoutId());
Rect dest = Rect::ltwh(r[0], r[1], r[2], r[3]);
if(SvgGui::debugLayout)
node->setAttribute("layout:ltwh", fstring("%.1f %.1f %.1f %.1f", dest.left, dest.top, dest.width(), dest.height()).c_str());
if(node->asContainerNode() && (ext->layContain & Widget::LAYX_HASLAYOUT)) { //node->hasAttribute("layout")) {
for(SvgNode* child : node->asContainerNode()->children()) {
if(!child->isVisible() || child->displayMode() == SvgNode::AbsoluteMode || !child->hasExt())
continue;
applyLayout(ctx, static_cast<Widget*>(child->ext()));
}
}
ext->setLayoutBounds(dest); // previously we did this before iterating over children
}
void Widget::setLayoutBounds(const Rect& dest)
{
static constexpr real thresh = 1E-3;
// Originally, we cleared layout transform at the beginning of prepareLayout, thus invalidating
// transformedBounds; now, we do not, so layout transform and bounds are not touched unless layout
// actually changes; this also allows us to scale rounded rect radii w/o storing original values
if(!dest.isValid())
return;
//if(node->hasClass("inputbox-bg")) PLATFORM_LOG("statusbar"); // set breakpoint here for debugging
// should we make an SDL_Event and use sdlEvent() instead of this separate onLayout callback?
Rect src = node->bounds();
if((onApplyLayout && onApplyLayout(src, dest)) || !src.isValid())
return;
if(node->asContainerNode() && (layContain & LAYX_HASLAYOUT))
return;
// with inputScale (assuming it can take non-integer values), we have to use floats instead of ints for
// layout (otherwise we get untouched pixels along right and/or bottom edges for some window sizes)
real sx = std::abs(dest.width() - src.width()) < thresh ? 1.0 : dest.width()/src.width();
real sy = std::abs(dest.height() - src.height()) < thresh ? 1.0 : dest.height()/src.height();
real dx = dest.left - src.left;
real dy = dest.top - src.top;
if(sx == 1 && sy == 1 && std::abs(dx) < thresh && std::abs(dy) < thresh)
return;
// this can happen if layout tries to squeeze stuff together - can happen with multiple flex containers
// with box-anchor=fill along flex direction!
if((sx != 1 && (layBehave & LAY_HFILL) != LAY_HFILL) || (sy != 1 && (layBehave & LAY_VFILL) != LAY_VFILL))
PLATFORM_LOG("Scaling non-scalable node: %s!\n", SvgNode::nodePath(node).c_str());
// can also just set w,h for image and use nodes instead of scaling?
// adjust rect size instead of scaling to handle strokes
if(node->type() == SvgNode::RECT && (sx != 1 || sy != 1)) {
SvgRect* rnode = static_cast<SvgRect*>(node);
Transform2D tf = rnode->totalTransform();
real sw = rnode->getFloatAttr("stroke-width", 0);
Rect r = rnode->getRect();
// assumes not non-scaling-stroke
real w = std::max(real(0), (dest.width() - sw)/tf.xscale());
real h = std::max(real(0), (dest.height() - sw)/tf.yscale());
rnode->setRect(Rect::ltwh(r.left, r.top, w, h), -1, -1); // preserve rounded rect radii
sx = 1;
sy = 1;
}
m_layoutTransform = Transform2D::translating(dx, dy) * m_layoutTransform * Transform2D::scaling(sx, sy);
// note that totalTransform() does not include any layout transforms
//Transform2D totaltf = node->totalTransform(); Dim tsx = totaltf.m11(); Dim tsy = totaltf.m22();
//m_layoutTransform = Transform2D().translate(-src.left/tsx, -src.top/tsy)
// .scale(sx, sy).translate(dest.left/tsx, dest.top/tsy) * m_layoutTransform;
// If clearing layout bounds every time, use this instead:
//m_layoutTransform = Transform2D().scale(sx, sy).translate(dx, dy);
node->invalidate(true);
}
// limiting layout: for now, layout can be limited to a subtree by setting the Widget.layoutIsolate flag
// - we may be able to infer this property in some cases (?), but there are definitely cases where we want
// it even though internal changes could theoretically affect global layout
// - use sparingly - only needed for a few complex widgets like TextEdit and Slider
static Widget* findLayoutDirtyRoot(Widget* w)
{
if(w->node->m_dirty == SvgNode::NOT_DIRTY)
return NULL;
if(!w->layoutVarsValid)
return w;
if(w->node->m_dirty == SvgNode::BOUNDS_DIRTY) // && (w->node->bounds() != w->node->m_renderedBounds || !w->node->isVisible()))
return w;
SvgContainerNode* container = w->node->asContainerNode();
if(container) {
if(container->m_removedBounds.isValid())
return w;
// don't descend if contents not subject to layout, but check if bounds have changed (note that this
// could be due to change of child, so node might only be CHILD_DIRTY, not BOUNDS dirty)
if(!(w->layContain & Widget::LAYX_HASLAYOUT) && !w->onPrepareLayout)
return w->node->bounds() != w->node->m_renderedBounds ? w : NULL;
Widget* dirtyRoot = NULL;
for(const SvgNode* child : container->children()) {
// abs pos nodes are laid out separately
if(child->m_dirty != SvgNode::NOT_DIRTY && child->isPaintable() && StringRef(child->getStringAttr("position")) != "absolute") {
// a newly shown child (which will be BOUNDS_DIRTY) may not have ext yet
if(!child->hasExt())
return w;
Widget* d = findLayoutDirtyRoot(static_cast<Widget*>(child->ext()));
// if two children are dirty, or dirty child is not isolated, w is the root
if(!d)
continue;
if(!d->layoutIsolate || dirtyRoot)
return w;
dirtyRoot = d;
}
}
return dirtyRoot;
}
return NULL;
}
// for sub-layout of a container; currently only used by ScrollWidget
void SvgGui::layoutWidget(Widget* contents, const Rect& bbox)
{
lay_context ctx;
lay_init_context(&ctx);
//lay_reserve_items_capacity(&ctx, 1024);
lay_id container_id = lay_item(&ctx);
lay_set_margins_ltrb(&ctx, container_id, bbox.left, bbox.top, 0, 0);
lay_set_size_xy(&ctx, container_id, bbox.width(), bbox.height());
lay_id contents_id = prepareLayout(&ctx, contents);
lay_insert(&ctx, container_id, contents_id);
// lay_run_context resets LAY_BREAK flags
lay_run_item(&ctx, 0); //lay_run_context(&ctx);
applyLayout(&ctx, contents);
lay_destroy_context(&ctx);
}
// for layout of a top-level window; handles position=absolute nodes
void SvgGui::layoutWindow(Window* win, const Rect& bbox)
{
//lay_reset_context(ctx);
// top-level layout
lay_context* ctx = &layoutCtx;
lay_id root = lay_item(ctx);
lay_set_size_xy(ctx, root, bbox.width(), bbox.height());
lay_id doc_id = prepareLayout(ctx, win);
lay_insert(ctx, root, doc_id);
lay_run_context(ctx);
applyLayout(ctx, win);
lay_reset_context(ctx);
}
void SvgGui::layoutAbsPosWidget(AbsPosWidget* ext)
{
lay_context* ctx = &layoutCtx;
Rect parentbbox = ext->node->parent()->bounds();
// we must clear existing layout transform on abs pos node, or it will "fight" with layout; although this
// does invalidate bounds, it means layout transform is not touched unless there is an actual change
// Can we do better? Can we use Widget::setLayoutBounds() for abs pos node transformation somehow?
ext->setLayoutTransform(Transform2D());
// Problem: w/o hfill on abs pos node, max-width is ignored; w/ hfill, width is always equal to max-width
// What we really wanted was a way to set max width or height of a flex wrap container
//Dim gs = win->gui()->globalScale;
//layoutWidget(ext, ext->maxWidth*gs, ext->maxHeight*gs);
prepareLayout(ctx, ext);
lay_run_item(ctx, 0); //lay_run_context(ctx); - this resets LAY_BREAK flags
applyLayout(ctx, ext);
// for now, we assume left and right are never both set, nor both top and bottom!
// note that nodes in absPosNodes always have a parent
Point offset = ext->calcOffset(parentbbox);
ext->setLayoutTransform(Transform2D::translating(offset) * ext->layoutTransform());
lay_reset_context(ctx);
}
// close all submenus up to parent_menu ... we may actually want closegroup = true to be default
void SvgGui::closeMenus(const Widget* parent_menu, bool closegroup)
{
if(menuStack.empty())
return;
if(parent_menu) {
if(closegroup)
parent_menu = getPressedGroupContainer(const_cast<Widget*>(parent_menu));
// can we do better than checking for class=menu?
while(parent_menu && !parent_menu->node->hasClass("menu"))
parent_menu = parent_menu->parent();
//if(!parent_menu) return; -- this breaks menubar behavior
}
while(!menuStack.empty() && menuStack.back() != parent_menu) {
Widget* menu = menuStack.back();
menuStack.pop_back();
menu->setVisible(false); // this now calls onHideWidget
menu->parent()->node->removeClass("pressed");
// with, e.g., ScrollWidget, open menu (modal) is closed on finger down, but next menu is not opened
// until finger up, so previous tricks with swallowing events, checking for menu reopen don't work
lastClosedMenu = menu;
}
Window* win = windows.back(); // menuStack.empty() ? windows.back() : menuStack.back()->window();
if(win->focusedWidget && (menuStack.empty() || win->focusedWidget->isDescendantOf(menuStack.back())))
win->focusedWidget->sdlUserEvent(this, FOCUS_GAINED, REASON_MENU);
}
void SvgGui::showMenu(Widget* menu)
{
Window* win = windows.back(); //menu->window();
if(win->focusedWidget)
win->focusedWidget->sdlUserEvent(this, FOCUS_LOST, REASON_MENU);
// I think this is the right thing to do, but hold off until we actually need it
//if(hoveredWidget) {
// Widget* modalWidget = !menuStack.empty() ? getPressedGroupContainer(menuStack.front()) : win->modalChild();
// hoveredLeave(commonParent(menu, hoveredWidget), modalWidget);
//}
//if(menuStack.empty()) onHideWidget(menu->window()); // clear pressed and hovered widgets
menu->setVisible(true);
menuStack.push_back(menu);
}
// don't return a callback as above, just provide this:
// parent_menu can be used to open context menu on menu item w/o closing menu
// make_pressed = false can be passed if opening menu on a release event (in which case pressedWidget will be
// immediately cleared anyway)
void SvgGui::showContextMenu(Widget* menu, const Point& p, const Widget* parent_menu, bool make_pressed)
{
Rect parentBounds = menu->node->parent()->bounds();
menu->node->setAttribute("left", fstring("%g", p.x - parentBounds.left).c_str());
menu->node->setAttribute("top", fstring("%g", p.y - parentBounds.top).c_str());
//menu->offsetLeft = SvgLength(p.x - parentBounds.left);
//menu->offsetTop = SvgLength(p.y - parentBounds.top);
if(!menu->isVisible()) {
closeMenus(parent_menu);
//openedContextMenu = menu;
//if(hoveredWidget) {
// Widget* modalWidget = !menuStack.empty() ? getPressedGroupContainer(menuStack.front()) : windows.back()->modalChild();
// hoveredLeave(commonParent(menu, hoveredWidget), modalWidget);
//}
menu->setVisible(true);
menuStack.push_back(menu);
// might we want to do this more generally whenever pressedWidget is replaced? don't seem to be any other
// cases right now (and we'd need to change Scroller a bit)
// we send pressedWidget for data2 to prevent closeMenus() in case pressedWidget is a menu
if(make_pressed) {
if(pressedWidget)
pressedWidget->sdlUserEvent(this, OUTSIDE_PRESSED, 0, currSDLEvent, pressedWidget); // menu);
setPressed(menu);
}
}
}
bool isLongPressOrRightClick(SDL_Event* event)
{
return (event->type == SvgGui::LONG_PRESS && event->tfinger.touchId == SvgGui::LONGPRESSID)
|| (event->type == SDL_FINGERDOWN && event->tfinger.fingerId != SDL_BUTTON_LMASK);
//event->tfinger.touchId == SDL_TOUCH_MOUSEID && event->tfinger.fingerId == SDL_BUTTON_RMASK);
}
// not sure if we should pass SvgGui (or Widget?) in light of its removal from other event handlers
void SvgGui::setupRightClick(Widget* itemext, const std::function<void(SvgGui*, Widget*, Point)>& callback)
{
// should have std::move somewhere here I think
itemext->addHandler([itemext, callback](SvgGui* gui, SDL_Event* event){
if(isLongPressOrRightClick(event)) {
callback(gui, itemext, Point(event->tfinger.x, event->tfinger.y));
return true;
}
return false;
});
}
// cache unused SDL_Windows?
// for now, use same context and painter for all windows - seems to work fine
// currently Painter only supports a single context because its nanovg context is static
// if win->bounds() is valid, window size will be fixed; should we accept Rect bounds as argument instead?
void SvgGui::showWindow(Window* win, Window* parent, bool showModal, Uint32 flags)
{
ASSERT((parent || !showModal) && "Modal windows must have parent");
ASSERT(parent != win && "parent cannot be equal to win!");
#ifndef SVGGUI_MULTIWINDOW
ASSERT((windows.empty() || (!win->sdlWindow && showModal && parent == windows.back())) &&
"Only modal windows supported on mobile; must be child of top-most modal!");
// this should probably be cleaned up; top level sdlWindow in SvgGui?
//if(!win->sdlWindow && windows.empty())
// win->sdlWindow = SDL_CreateWindow(win->winTitle.c_str(), 0, 0, 800, 800, flags | SDL_WINDOW_OPENGL);
ASSERT((!windows.empty() || win->sdlWindow) && "sdlWindow must be set for first window!");
// on mobile, we assume we can't change window size
if(win->sdlWindow) {
int w = 0, h = 0;
SDL_GetWindowSize(win->sdlWindow, &w, &h); // SDL_GL_GetDrawableSize?
win->setWinBounds(Rect::wh(w, h)*inputScale);
}
#else
int x = 0, y = 0, w = 800, h = 800;
if(win->bounds().isValid()) {
x = win->bounds().left;
y = win->bounds().top;
w = win->bounds().width();
h = win->bounds().height();
if(win->sdlWindow) {
SDL_SetWindowPosition(win->sdlWindow, x, y);
SDL_SetWindowSize(win->sdlWindow, w, h);
}
}
if(!win->sdlWindow)
win->sdlWindow = SDL_CreateWindow(win->winTitle.c_str(), x, y, w, h, flags | SDL_WINDOW_OPENGL);
else
SDL_ShowWindow(win->sdlWindow);
#endif
Rect bbox = win->winBounds();
// checking just this instead of isValid() allows user to specify size w/o specifying pos
if(bbox.right < 0 || bbox.bottom < 0)
win->setWinBounds(Rect::centerwh(parent ? parent->winBounds().center() : Point(0,0),
std::max(win->winBounds().width(), real(0)), std::max(win->winBounds().height(), real(0))));
win->parentWindow = parent;
win->svgGui = this;
windows.push_back(win);
win->setWindowXmlClass(windowXmlClass.c_str());
if(win->documentNode()->stylesheet() != windowStylesheet.get()) {
win->documentNode()->setStylesheet(windowStylesheet);
win->documentNode()->restyle();
}
if(showModal) {
// clear pressedWidget and hoveredWidget
onHideWidget(parent);
parent->node->setDirty(SvgNode::PIXELS_DIRTY);