1 module awebview.gui.html; 2 3 import carbon.utils; 4 import awebview.gui.activity; 5 import awebview.gui.methodhandler; 6 import awebview.gui.application; 7 8 import awebview.wrapper; 9 import awebview.cssgrammar; 10 11 import std.variant; 12 import std.typecons; 13 14 import std.array : appender; 15 import std.format : formattedWrite, format; 16 import std.functional : forward; 17 import std.conv : to; 18 import std.datetime; 19 public import core.time : Duration; 20 21 public import carbon.event : FiredContext; 22 import carbon.templates : Lstr; 23 import carbon.utils : toLiteral; 24 25 26 string buildHTMLTagAttr(in string[string] attrs) 27 { 28 auto app = appender!string(); 29 foreach(k, v; attrs) 30 app.formattedWrite("%s=%s ", k, toLiteral(v)); 31 32 return app.data; 33 } 34 35 36 string buildHTMLTagAttr(string tag, string value) 37 { 38 import std..string : format; 39 return format("%s=%s ", tag, toLiteral(value)); 40 } 41 42 unittest 43 { 44 assert(buildHTMLTagAttr("a", "b") == "a=b "); 45 assert(buildHTMLTagAttr(["a": "b"]) == "a=b "); 46 } 47 48 49 abstract class HTMLPage 50 { 51 this(string id) 52 { 53 _id = id; 54 } 55 56 57 final 58 @property 59 inout(Activity) activity() inout pure nothrow @safe @nogc 60 { 61 return _activity; 62 } 63 64 65 final 66 @property 67 inout(Application) application() inout pure nothrow @safe @nogc 68 { 69 if(_activity is null) 70 return null; 71 else 72 return _activity.application; 73 } 74 75 76 final 77 string id() const pure nothrow @safe @nogc @property { return _id; } 78 79 80 string html() @property; 81 82 83 inout(HTMLElement[string]) elements() inout @property; 84 85 86 final 87 inout(HTMLElement) opIndex(string id) inout 88 { 89 return this.elements[id]; 90 } 91 92 93 void onStart(Activity activity) 94 { 95 _activity = activity; 96 97 foreach(key, elem; elements.maybeModified){ 98 elem.onStart(this); 99 } 100 } 101 102 103 void onAttach(bool isInitPhase) 104 { 105 _activity = activity; 106 foreach(key, elem; elements.maybeModified) 107 elem.onAttach(isInitPhase); 108 } 109 110 111 void onLoad(bool isInit) 112 { 113 foreach(key, elem; elements.maybeModified) 114 elem.onLoad(isInit); 115 116 _clientWidth = activity.evalJS(`document.documentElement.clientWidth`).get!uint; 117 _clientHeight = activity.evalJS(`document.documentElement.clientHeight`).get!uint; 118 _resizeStatements = generateResizeStatements(this.activity); 119 onResize(activity.width, activity.height); 120 } 121 122 123 void onUpdate() 124 { 125 auto newW = activity.evalJS(`document.documentElement.clientWidth`).get!uint; 126 auto newH = activity.evalJS(`document.documentElement.clientHeight`).get!uint; 127 128 if(newW != _clientWidth || newH != _clientHeight) 129 onResize(activity.width, activity.height); 130 131 _clientWidth = newW; 132 _clientHeight = newH; 133 134 foreach(k, elem; elements.maybeModified) 135 elem.onUpdate(); 136 } 137 138 139 void onDetach() 140 { 141 foreach(key, elem; elements.maybeModified) 142 elem.onDetach(); 143 } 144 145 146 void onDestroy() 147 { 148 foreach(k, elem; elements.maybeModified) 149 elem.onDestroy(); 150 } 151 152 153 void onResize(size_t w, size_t h) 154 { 155 this.activity.runJS(_resizeStatements); 156 157 foreach(k, elem; elements.maybeModified) 158 elem.onResize(w, h); 159 } 160 161 162 void onReceiveImmediateMessage(ImmediateMessage msg) 163 { 164 foreach(k, e; elements.maybeModified){ 165 e.onReceiveImmediateMessage(msg); 166 } 167 } 168 169 170 private: 171 Activity _activity; 172 string _id; 173 174 size_t _clientWidth; 175 size_t _clientHeight; 176 177 string _resizeStatements; 178 } 179 180 181 class WebPage : HTMLPage 182 { 183 this(string id, string url) 184 { 185 super(id); 186 _url = url; 187 } 188 189 190 @property 191 void location(string str) 192 { 193 if(activity){ 194 _url = str; 195 activity.view.loadURL(WebURL(str)); 196 } 197 else 198 _url = str; 199 } 200 201 202 @property 203 string location() 204 { 205 if(activity) 206 return activity.evalJS(q{document.location}).to!string; 207 else 208 return _url; 209 } 210 211 212 override 213 void onAttach(bool isInitPhase) 214 { 215 _bLoaded = false; 216 } 217 218 219 override 220 void onUpdate() 221 { 222 if(!_bLoaded) 223 this.location = _url; 224 225 _bLoaded = true; 226 } 227 228 229 override 230 inout(HTMLElement[string]) elements() inout @property { return null; } 231 232 233 override 234 string html() @property 235 { 236 return `<html><head><title>Jump</title></head><body>Now loading...</body></html>`; 237 } 238 239 private: 240 string _url; 241 bool _bLoaded; 242 } 243 244 245 class TemplateHTMLPage(string form) : HTMLPage 246 { 247 this(string id, Variant[string] exts = null) 248 { 249 super(id); 250 _exts = exts; 251 } 252 253 254 final 255 auto js(this This)() @property pure nothrow @safe @nogc inout 256 { 257 static struct Result 258 { 259 string html() const 260 { 261 auto app = appender!string(); 262 foreach(_, e; _this._js) 263 app.formattedWrite(`<script src="%s"></script>`); 264 return app.data; 265 } 266 267 268 alias html this; 269 270 271 void opOpAssign(string op : "~")(string src) 272 { 273 import std.path : baseName; 274 275 auto bn = src.baseName; 276 _this._js[bn] = src; 277 } 278 279 280 private: 281 This _this; 282 } 283 284 285 return Result(this); 286 } 287 288 289 final 290 auto css(this This)() @property pure nothrow @safe @nogc inout 291 { 292 static struct Result 293 { 294 string html() const 295 { 296 auto app = appender!string(); 297 foreach(_, e; _this._css) 298 app.formattedWrite(`<link rel="stylesheet" href="%s">`); 299 return app.data; 300 } 301 302 303 alias html this; 304 305 306 void opOpAssign(string op : "~")(string src) 307 { 308 import std.path : baseName; 309 310 auto bn = src.baseName; 311 _this._css[bn] = src; 312 } 313 314 315 private: 316 This _this; 317 } 318 319 320 return Result(this); 321 } 322 323 324 override 325 @property 326 string html() 327 { 328 return mixin(Lstr!(form)); 329 } 330 331 332 override 333 @property 334 inout(HTMLElement[string]) elements() inout { return _elems; } 335 336 337 void opOpAssign(string op : "~")(HTMLElement element) 338 { 339 addElement(element); 340 } 341 342 343 void addElement(HTMLElement element) 344 { 345 _elems[element.id] = element; 346 if(this.activity !is null){ 347 element.onStart(this); 348 element.onAttach(true); 349 } 350 } 351 352 353 @property 354 inout(Variant[string]) exts() inout { return _exts; } 355 356 @property 357 inout(T) exts(T)(string str) inout { return *_exts[str].peek!T; } 358 359 360 private: 361 HTMLElement[string] _elems; 362 Variant[string] _exts; 363 string[string] _js; 364 string[string] _css; 365 } 366 367 368 class HTMLElement 369 { 370 import awebview.jsvariable; 371 372 373 this(string id, bool doCreateObject) 374 in{ 375 if(id is null) 376 assert(!doCreateObject); 377 } 378 body{ 379 _id = id; 380 _hasObj = doCreateObject; 381 } 382 383 384 final 385 @property 386 inout(HTMLPage) page() inout pure nothrow @safe @nogc { return _page; } 387 388 389 final 390 @property 391 inout(Activity) activity() inout pure nothrow @safe @nogc 392 { 393 if(_page is null) 394 return null; 395 else 396 return _page.activity; 397 } 398 399 400 final 401 @property 402 inout(Application) application() inout pure nothrow @safe @nogc 403 { 404 if(this.activity is null) 405 return null; 406 else 407 return this.activity.application; 408 } 409 410 411 final 412 @property 413 bool hasObject() const pure nothrow @safe @nogc 414 { 415 return _hasObj; 416 } 417 418 419 @property 420 WeakRef!JSObject jsobject() 421 in { 422 assert(_v.isObject); 423 } 424 body { 425 return _v.get!(WeakRef!JSObject); 426 } 427 428 429 @property 430 string jsExpr() 431 in { 432 assert(hasObject || hasId); 433 } 434 body { 435 if(this.hasObject) 436 return mixin(Lstr!q{_tmp_%[_id%].domObject}); 437 else 438 return `document.getElementById("` ~ id ~ `")`; 439 } 440 441 442 final 443 @property 444 auto domObject() 445 { 446 return jsExpression(this.activity, this.jsExpr); 447 } 448 449 450 final @property bool hasId() const pure nothrow @safe @nogc { return _id !is null; } 451 452 453 final @property string id() const pure nothrow @safe @nogc { return _id; } 454 455 456 @property string html() { return ""; } 457 @property string mime() { return "text/html"; } 458 @property const(void)[] rawResource() { return this.html; } 459 460 461 void onStart(HTMLPage page) 462 { 463 _page = page; 464 465 if(this.hasObject && !_v.isObject){ 466 _v = activity.createObject(_id); 467 } 468 } 469 470 471 void onDestroy() 472 { 473 _page = null; 474 _v = JSValue.null_; 475 } 476 477 478 void onAttach(bool isInit) 479 { 480 if(isInit && this.hasObject && !_v.isObject){ 481 _v = activity.createObject(_id); 482 } 483 } 484 485 486 void onUpdate() {} 487 488 489 void onDetach() {} 490 491 492 void onLoad(bool isInit) 493 { 494 if(this.hasObject){ 495 activity.runJS(mixin(Lstr!q{ 496 _tmp_%[_id%] = {}; 497 _tmp_%[_id%].domObject = document.getElementById("%[_id%]"); 498 })); 499 } 500 501 if(this.hasId || this.hasObject){ 502 foreach(key, ref v; _staticProperties) 503 this.opIndexAssign(v, key); 504 } 505 } 506 507 508 void onResize(size_t w, size_t h) {} 509 510 511 final 512 @property 513 auto staticProps() 514 { 515 static struct Result 516 { 517 void opIndexAssign(T)(T value, string name) 518 if(is(typeof(JSValue(value)) : JSValue)) 519 { 520 _elem.staticPropsSet(name, value); 521 } 522 523 524 void remove(string name) 525 { 526 _elem.staticPropsRemove(name); 527 } 528 529 530 bool opBinaryRight(string op : "in")(string name) inout 531 { 532 return _elem.inStaticProps(name); 533 } 534 535 private: 536 HTMLElement _elem; 537 } 538 539 return Result(this); 540 } 541 542 543 void staticPropsSet(T)(string name, T value) 544 if(is(typeof(JSValue(value)) : JSValue)) 545 in{ 546 assert(this.hasId); 547 } 548 body{ 549 JSValue jv = JSValue(value); 550 _staticProperties[name] = jv; 551 if(this.activity){ 552 this.opIndexAssign(jv, name); 553 } 554 } 555 556 557 final 558 void staticPropsRemove(string name) 559 { 560 _staticProperties.remove(name); 561 } 562 563 564 final 565 bool inStaticProps(string name) inout 566 { 567 return !(name !in _staticProperties); 568 } 569 570 571 final 572 { 573 mixin JSExprDOMEagerOperators!(); 574 } 575 576 577 final 578 Tuple!(uint, "x", uint, "y", uint, "width", uint, "height") 579 boundingClientRect() @property 580 { 581 this.activity.runJS(mixin(Lstr!q{ 582 var e = %[domObject.jsExpr%].getBoundingClientRect(); 583 _carrierObject_.x = e.left; 584 _carrierObject_.y = e.top; 585 _carrierObject_.w = e.width; 586 _carrierObject_.h = e.height; 587 })); 588 589 auto co = activity.carrierObject; 590 typeof(return) res; 591 res.x = co["x"].get!uint; 592 res.y = co["y"].get!uint; 593 res.width = co["w"].get!uint; 594 res.height = co["h"].get!uint; 595 596 return res; 597 } 598 599 600 final 601 uint posY() @property 602 { 603 return domObject.invoke("getBoundingClientRect")["top"].eval().get!uint; 604 } 605 606 607 final 608 uint posX() @property 609 { 610 return domObject.invoke("getBoundingClientRect")["left"].eval().get!uint; 611 } 612 613 614 final 615 uint[2] pos() @property 616 { 617 auto rec = boundingClientRect(); 618 return [rec.x, rec.y]; 619 } 620 621 622 final 623 uint width() @property 624 { 625 return domObject.invoke("getBoundingClientRect")["width"].eval().get!uint; 626 } 627 628 629 final 630 uint height() @property 631 { 632 return domObject.invoke("getBoundingClientRect")["width"].eval().get!uint; 633 } 634 635 636 637 void onReceiveImmediateMessage(ImmediateMessage msg){} 638 639 640 private: 641 string _id; 642 bool _hasObj; 643 JSValue _v; 644 HTMLPage _page; 645 JSValue[string] _staticProperties; 646 } 647 648 649 class IDOnlyElement : HTMLElement 650 { 651 this(string id) 652 in{ 653 assert(id !is null); 654 } 655 body{ 656 super(id, false); 657 } 658 } 659 660 661 class TagOnlyElement : HTMLElement 662 { 663 this(string id) 664 in { 665 assert(id !is null); 666 } 667 body { 668 super(id, true); 669 } 670 } 671 672 673 /** 674 Example: 675 ---------------------- 676 class MyButton : TemplateHTMLElement!(HTMLElement, 677 q{<input type="button" id="%[id%]" value="Click me!">}) 678 { 679 this(string id) 680 { 681 super(id, null, false); 682 } 683 } 684 685 auto btn1 = new MyButton("btn1"); 686 assert(btn1.html == q{<input type="button" id="btn1" value="Click me!">}); 687 ---------------------- 688 */ 689 abstract class TemplateHTMLElement(Element, string form) : Element 690 if(is(Element : HTMLElement)) 691 { 692 this(T...)(auto ref T args) 693 { 694 static if(is(typeof(super(forward!args[0 .. $-1]))) && 695 is(typeof(args[$-1]) : Variant[string])) 696 { 697 super(forward!args[0 .. $-1]); 698 _exts = args[$-1]; 699 } 700 else 701 super(forward!args); 702 } 703 704 705 @property 706 inout(Variant[string]) exts() inout { return _exts; } 707 708 709 override 710 @property 711 string html() 712 { 713 import carbon.templates : Lstr; 714 return mixin(Lstr!(form)); 715 } 716 717 718 private: 719 Variant[string] _exts; 720 } 721 722 723 /// ditto 724 alias TemplateHTMLElement(string form) = TemplateHTMLElement!(HTMLElement, form); 725 726 727 /** 728 Example: 729 ---------------- 730 class MyButton : DefineSignals!(DeclareSignals!(HTMLElement, "onClick"), "onClick") 731 { 732 this(string id) 733 { 734 super(id, true); 735 } 736 737 string html() const { ... } 738 } 739 740 741 MyButton btn1 = new MyButton("btn1"); 742 btn1.onClick.strongConnect(delegate(FiredContext ctx, WeakRef!(const(JSArrayCpp)) arr){ 743 assert(ctx.sender == btn1); 744 745 writeln("fired a signal by ", ctx); 746 }); 747 ---------------- 748 */ 749 abstract class DefineSignals(Element, names...) : Element 750 if(is(Element : HTMLElement) && names.length >= 1) 751 { 752 import carbon.event; 753 754 this(T...)(auto ref T args) 755 { 756 super(forward!args); 757 758 foreach(name; names) 759 mixin(format(`_%1$sEvent = new typeof(_%1$sEvent)();`, name)); 760 } 761 762 763 mixin(genMethod()); 764 765 766 private: 767 mixin(genField()); 768 769 static 770 { 771 string genField() 772 { 773 auto app = appender!string; 774 foreach(s; names) 775 app.formattedWrite("EventManager!(WeakRef!(const(JSArrayCpp))) _%sEvent;\n", s); 776 777 return app.data; 778 } 779 780 781 string genMethod() 782 { 783 auto app = appender!string; 784 foreach(s; names){ 785 app.formattedWrite("EventManager!(WeakRef!(const(JSArrayCpp))) %1$s() { return _%1$sEvent; }\n", s); 786 app.formattedWrite("override void %1$s(WeakRef!(const(JSArrayCpp)) arr) { _%1$sEvent.emit(this, arr); }\n", s); 787 } 788 789 return app.data; 790 } 791 } 792 } 793 794 795 /** 796 Example: 797 ------------------- 798 class MyButton : DeclareSignals!(HTMLElement, "onClick") 799 { 800 this(string id) 801 { 802 super(id, true); 803 } 804 805 806 override 807 void onClick(WeakRef!(JSArrayCpp) args) 808 { 809 writeln("OK"); 810 } 811 } 812 ------------------- 813 */ 814 abstract class DeclareSignals(Element, names...) : Element 815 if(is(Element : HTMLElement) && names.length >= 1) 816 { 817 this(T...)(auto ref T args) 818 { 819 super(forward!args); 820 } 821 822 823 final 824 void doJSInitialize(bool b) @property 825 { 826 _doInit = b; 827 } 828 829 830 final 831 void stopPropergation(bool b) @property 832 { 833 _stopProp = b; 834 } 835 836 837 mixin(genDeclMethods); 838 839 override 840 void onStart(HTMLPage page) 841 { 842 super.onStart(page); 843 this.activity.methodHandler.set(this); 844 } 845 846 847 override 848 void onLoad(bool init) 849 { 850 super.onLoad(init); 851 852 if(_doInit){ 853 this.activity.runJS(genSettingEventHandlers(this.id, this.domObject.jsExpr, _stopProp)); 854 } 855 } 856 857 858 private: 859 bool _doInit = true; 860 bool _stopProp = false; 861 862 static 863 { 864 string genDeclMethods() 865 { 866 auto app = appender!string(); 867 868 foreach(s; names) 869 app.formattedWrite(`@JSMethodTag("%1$s"w) `"void %1$s(WeakRef!(const(JSArrayCpp)));\n", s); 870 871 return app.data; 872 } 873 874 875 string genSettingEventHandlers(string id, string domExpr, bool stopProp) 876 { 877 import std..string : toLower; 878 879 auto app = appender!string(); 880 app.formattedWrite("var e = %s;", domExpr); 881 882 foreach(s; names){ 883 if(!stopProp) 884 app.formattedWrite(q{e.%3$s = function() { %1$s.%2$s(); };}, id, s, toLower(s)); 885 else 886 app.formattedWrite(q{e.%3$s = function(ev) { ev.stopPropergation(); %1$s.%2$s(); };}, id, s, toLower(s)); 887 } 888 889 return app.data; 890 } 891 } 892 } 893 894 895 alias DeclDefSignals(Element, names...) = DefineSignals!(DeclareSignals!(Element, names), names); 896 897 898 /** 899 Open context menu when user click right button. 900 */ 901 abstract class DeclareContextMenu(Element, setting...) : DeclareSignals!(Element, "onContextMenu", setting) 902 { 903 this(T...)(auto ref T args) 904 { 905 super(forward!args); 906 } 907 908 909 HTMLPage menuPage() @property; 910 911 912 override 913 void onContextMenu(WeakRef!(const(JSArrayCpp))) 914 { 915 this.activity.popup(this.menuPage); 916 } 917 } 918 919 920 /** 921 Mouse hover event 922 */ 923 abstract class DeclareHoverSignal(Element) : DeclareSignals!(Element, "onMouseOver", "onMouseOut") 924 { 925 this(T...)(auto ref T args) 926 { 927 super(forward!args); 928 } 929 930 931 /** 932 */ 933 final 934 bool hover() @property { return _hovered; } 935 936 937 final 938 void onHoverImpl() 939 { 940 if(_isStarted) 941 onHover(_hovered, Clock.currTime - _startTime); 942 } 943 944 945 /** 946 */ 947 void onHover(bool bOver, Duration dur); 948 949 950 override 951 void onUpdate() 952 { 953 super.onUpdate(); 954 955 onHoverImpl(); 956 } 957 958 959 override 960 void onMouseOver(WeakRef!(const(JSArrayCpp))) 961 { 962 _isStarted = true; 963 _hovered = true; 964 _startTime = Clock.currTime; 965 onHoverImpl(); 966 } 967 968 969 override 970 void onMouseOut(WeakRef!(const(JSArrayCpp))) 971 { 972 _hovered = false; 973 _startTime = Clock.currTime; 974 onHoverImpl(); 975 } 976 977 978 private: 979 bool _isStarted; 980 bool _hovered; 981 SysTime _startTime; 982 } 983 984 985 /** 986 EventPipe 987 */ 988 final class PageSignalHandler : HTMLElement 989 { 990 import carbon.event; 991 992 this(string id = "_event_signal_pipe") 993 { 994 super(id, false); 995 _onStartEvent = new EventManager!(Activity)(); 996 _onAttachEvent = new EventManager!bool(); 997 _onLoadEvent = new EventManager!bool(); 998 _onUpdateEvent = new EventManager!(); 999 _onDetachEvent = new EventManager!(); 1000 _onDestroyEvent = new EventManager!(); 1001 _onResizeEvent = new EventManager!(size_t, size_t)(); 1002 } 1003 1004 @property 1005 { 1006 EventManager!(Activity) onStartHandler () { return _onStartEvent; } 1007 EventManager!(bool) onAttachHandler () { return _onAttachEvent; } 1008 EventManager!(bool) onLoadHandler () { return _onLoadEvent; } 1009 EventManager!() onUpdateHandler () { return _onUpdateEvent; } 1010 EventManager!() onDetachHandler () { return _onDetachEvent; } 1011 EventManager!() onDestroyHandler() { return _onDestroyEvent; } 1012 EventManager!(size_t, size_t) onResizeHandler () { return _onResizeEvent; } 1013 } 1014 1015 override 1016 void onStart(HTMLPage page) 1017 { 1018 super.onStart(page); 1019 _onStartEvent.emit(this, page.activity); 1020 } 1021 1022 1023 override 1024 void onAttach(bool bInit) 1025 { 1026 super.onAttach(bInit); 1027 _onAttachEvent.emit(this, bInit); 1028 } 1029 1030 1031 override 1032 void onLoad(bool bInit) 1033 { 1034 super.onLoad(bInit); 1035 _onLoadEvent.emit(this, bInit); 1036 } 1037 1038 1039 override 1040 void onUpdate() 1041 { 1042 super.onUpdate(); 1043 _onUpdateEvent.emit(this); 1044 } 1045 1046 1047 override 1048 void onDetach() 1049 { 1050 super.onDetach(); 1051 _onDetachEvent.emit(this); 1052 } 1053 1054 1055 override 1056 void onDestroy() 1057 { 1058 super.onDestroy(); 1059 _onDestroyEvent.emit(this); 1060 } 1061 1062 1063 override 1064 void onResize(size_t w, size_t h) 1065 { 1066 super.onResize(w, h); 1067 _onResizeEvent.emit(this, w, h); 1068 } 1069 1070 1071 private: 1072 EventManager!(Activity) _onStartEvent; 1073 EventManager!(bool) _onAttachEvent; 1074 EventManager!(bool) _onLoadEvent; 1075 EventManager!() _onUpdateEvent; 1076 EventManager!() _onDetachEvent; 1077 EventManager!() _onDestroyEvent; 1078 EventManager!(size_t, size_t) _onResizeEvent; 1079 } 1080 1081 1082 /** 1083 Selectors API 1084 */ 1085 alias querySelector = querySelectorImpl!false; 1086 1087 1088 /// ditto 1089 alias querySelectorAll = querySelectorImpl!true; 1090 1091 1092 auto querySelectorImpl(bool isAll)(Activity activity, string cssSelector) 1093 { 1094 import awebview.jsvariable; 1095 1096 static struct QuerySelectorResult 1097 { 1098 string jsExpr() const @property { return _jsExpr; } 1099 Activity activity() @property { return _activity; } 1100 1101 1102 mixin JSExprDOMEagerOperators!(); 1103 1104 private: 1105 string _jsExpr; 1106 Activity _activity; 1107 } 1108 1109 QuerySelectorResult res; 1110 res._jsExpr = mixin(Lstr!q{document.%[isAll ? "querySelectorAll" : "querySelector"%](%[toLiteral(cssSelector)%])}); 1111 res._activity = activity; 1112 1113 return res; 1114 }