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 } 347 348 349 @property 350 inout(Variant[string]) exts() inout { return _exts; } 351 352 @property 353 inout(T) exts(T)(string str) inout { return *_exts[str].peek!T; } 354 355 356 private: 357 HTMLElement[string] _elems; 358 Variant[string] _exts; 359 string[string] _js; 360 string[string] _css; 361 } 362 363 364 class HTMLElement 365 { 366 import awebview.jsvariable; 367 368 369 this(string id, bool doCreateObject) 370 in{ 371 if(id is null) 372 assert(!doCreateObject); 373 } 374 body{ 375 _id = id; 376 _hasObj = doCreateObject; 377 } 378 379 380 final 381 @property 382 inout(HTMLPage) page() inout pure nothrow @safe @nogc { return _page; } 383 384 385 final 386 @property 387 inout(Activity) activity() inout pure nothrow @safe @nogc 388 { 389 if(_page is null) 390 return null; 391 else 392 return _page.activity; 393 } 394 395 396 final 397 @property 398 inout(Application) application() inout pure nothrow @safe @nogc 399 { 400 if(this.activity is null) 401 return null; 402 else 403 return this.activity.application; 404 } 405 406 407 final 408 @property 409 bool hasObject() const pure nothrow @safe @nogc 410 { 411 return _hasObj; 412 } 413 414 415 @property 416 WeakRef!JSObject jsobject() 417 in { 418 assert(_v.isObject); 419 } 420 body { 421 return _v.get!(WeakRef!JSObject); 422 } 423 424 425 @property 426 string jsExpr() 427 in { 428 assert(hasObject || hasId); 429 } 430 body { 431 if(this.hasObject) 432 return mixin(Lstr!q{_tmp_%[_id%].domObject}); 433 else 434 return `document.getElementById("` ~ id ~ `")`; 435 } 436 437 438 final 439 @property 440 auto domObject() 441 { 442 return jsExpression(this.activity, this.jsExpr); 443 } 444 445 446 final @property bool hasId() const pure nothrow @safe @nogc { return _id !is null; } 447 448 449 final @property string id() const pure nothrow @safe @nogc { return _id; } 450 451 452 @property string html() { return ""; } 453 @property string mime() { return "text/html"; } 454 @property const(void)[] rawResource() { return this.html; } 455 456 457 void onStart(HTMLPage page) 458 { 459 _page = page; 460 461 if(this.hasObject && !_v.isObject){ 462 _v = activity.createObject(_id); 463 } 464 } 465 466 467 void onDestroy() 468 { 469 _page = null; 470 _v = JSValue.null_; 471 } 472 473 474 void onAttach(bool isInit) 475 { 476 if(isInit && this.hasObject && !_v.isObject){ 477 _v = activity.createObject(_id); 478 } 479 } 480 481 482 void onUpdate() {} 483 484 485 void onDetach() {} 486 487 488 void onLoad(bool isInit) 489 { 490 if(this.hasObject){ 491 activity.runJS(mixin(Lstr!q{ 492 _tmp_%[_id%] = {}; 493 _tmp_%[_id%].domObject = document.getElementById("%[_id%]"); 494 })); 495 } 496 497 if(this.hasId || this.hasObject){ 498 foreach(key, ref v; _staticProperties) 499 this.opIndexAssign(v, key); 500 } 501 } 502 503 504 void onResize(size_t w, size_t h) {} 505 506 507 final 508 @property 509 auto staticProps() 510 { 511 static struct Result 512 { 513 void opIndexAssign(T)(T value, string name) 514 if(is(typeof(JSValue(value)) : JSValue)) 515 { 516 _elem.staticPropsSet(name, value); 517 } 518 519 520 void remove(string name) 521 { 522 _elem.staticPropsRemove(name); 523 } 524 525 526 bool opBinaryRight(string op : "in")(string name) inout 527 { 528 return _elem.inStaticProps(name); 529 } 530 531 private: 532 HTMLElement _elem; 533 } 534 535 return Result(this); 536 } 537 538 539 void staticPropsSet(T)(string name, T value) 540 if(is(typeof(JSValue(value)) : JSValue)) 541 in{ 542 assert(this.hasId); 543 } 544 body{ 545 JSValue jv = JSValue(value); 546 _staticProperties[name] = jv; 547 if(this.activity){ 548 this.opIndexAssign(jv, name); 549 } 550 } 551 552 553 final 554 void staticPropsRemove(string name) 555 { 556 _staticProperties.remove(name); 557 } 558 559 560 final 561 bool inStaticProps(string name) inout 562 { 563 return !(name !in _staticProperties); 564 } 565 566 567 final 568 { 569 mixin JSExprDOMEagerOperators!(); 570 } 571 572 573 final 574 Tuple!(uint, "x", uint, "y", uint, "width", uint, "height") 575 boundingClientRect() @property 576 { 577 this.activity.runJS(mixin(Lstr!q{ 578 var e = %[domObject.jsExpr%].getBoundingClientRect(); 579 _carrierObject_.x = e.left; 580 _carrierObject_.y = e.top; 581 _carrierObject_.w = e.width; 582 _carrierObject_.h = e.height; 583 })); 584 585 auto co = activity.carrierObject; 586 typeof(return) res; 587 res.x = co["x"].get!uint; 588 res.y = co["y"].get!uint; 589 res.width = co["w"].get!uint; 590 res.height = co["h"].get!uint; 591 592 return res; 593 } 594 595 596 final 597 uint posY() @property 598 { 599 return domObject.invoke("getBoundingClientRect")["top"].eval().get!uint; 600 } 601 602 603 final 604 uint posX() @property 605 { 606 return domObject.invoke("getBoundingClientRect")["left"].eval().get!uint; 607 } 608 609 610 final 611 uint[2] pos() @property 612 { 613 auto rec = boundingClientRect(); 614 return [rec.x, rec.y]; 615 } 616 617 618 final 619 uint width() @property 620 { 621 return domObject.invoke("getBoundingClientRect")["width"].eval().get!uint; 622 } 623 624 625 final 626 uint height() @property 627 { 628 return domObject.invoke("getBoundingClientRect")["width"].eval().get!uint; 629 } 630 631 632 633 void onReceiveImmediateMessage(ImmediateMessage msg){} 634 635 636 private: 637 string _id; 638 bool _hasObj; 639 JSValue _v; 640 HTMLPage _page; 641 JSValue[string] _staticProperties; 642 } 643 644 645 class IDOnlyElement : HTMLElement 646 { 647 this(string id) 648 in{ 649 assert(id !is null); 650 } 651 body{ 652 super(id, false); 653 } 654 } 655 656 657 class TagOnlyElement : HTMLElement 658 { 659 this(string id) 660 in { 661 assert(id !is null); 662 } 663 body { 664 super(id, true); 665 } 666 } 667 668 669 /** 670 Example: 671 ---------------------- 672 class MyButton : TemplateHTMLElement!(HTMLElement, 673 q{<input type="button" id="%[id%]" value="Click me!">}) 674 { 675 this(string id) 676 { 677 super(id, null, false); 678 } 679 } 680 681 auto btn1 = new MyButton("btn1"); 682 assert(btn1.html == q{<input type="button" id="btn1" value="Click me!">}); 683 ---------------------- 684 */ 685 abstract class TemplateHTMLElement(Element, string form) : Element 686 if(is(Element : HTMLElement)) 687 { 688 this(T...)(auto ref T args) 689 { 690 static if(is(typeof(super(forward!args[0 .. $-1]))) && 691 is(typeof(args[$-1]) : Variant[string])) 692 { 693 super(forward!args[0 .. $-1]); 694 _exts = args[$-1]; 695 } 696 else 697 super(forward!args); 698 } 699 700 701 @property 702 inout(Variant[string]) exts() inout { return _exts; } 703 704 705 override 706 @property 707 string html() 708 { 709 import carbon.templates : Lstr; 710 return mixin(Lstr!(form)); 711 } 712 713 714 private: 715 Variant[string] _exts; 716 } 717 718 719 /// ditto 720 alias TemplateHTMLElement(string form) = TemplateHTMLElement!(HTMLElement, form); 721 722 723 /** 724 Example: 725 ---------------- 726 class MyButton : DefineSignals!(DeclareSignals!(HTMLElement, "onClick"), "onClick") 727 { 728 this(string id) 729 { 730 super(id, true); 731 } 732 733 string html() const { ... } 734 } 735 736 737 MyButton btn1 = new MyButton("btn1"); 738 btn1.onClick.strongConnect(delegate(FiredContext ctx, WeakRef!(const(JSArrayCpp)) arr){ 739 assert(ctx.sender == btn1); 740 741 writeln("fired a signal by ", ctx); 742 }); 743 ---------------- 744 */ 745 abstract class DefineSignals(Element, names...) : Element 746 if(is(Element : HTMLElement) && names.length >= 1) 747 { 748 import carbon.event; 749 750 this(T...)(auto ref T args) 751 { 752 super(forward!args); 753 754 foreach(name; names) 755 mixin(format(`_%1$sEvent = new typeof(_%1$sEvent)();`, name)); 756 } 757 758 759 mixin(genMethod()); 760 761 762 private: 763 mixin(genField()); 764 765 static 766 { 767 string genField() 768 { 769 auto app = appender!string; 770 foreach(s; names) 771 app.formattedWrite("EventManager!(WeakRef!(const(JSArrayCpp))) _%sEvent;\n", s); 772 773 return app.data; 774 } 775 776 777 string genMethod() 778 { 779 auto app = appender!string; 780 foreach(s; names){ 781 app.formattedWrite("EventManager!(WeakRef!(const(JSArrayCpp))) %1$s() { return _%1$sEvent; }\n", s); 782 app.formattedWrite("override void %1$s(WeakRef!(const(JSArrayCpp)) arr) { _%1$sEvent.emit(this, arr); }\n", s); 783 } 784 785 return app.data; 786 } 787 } 788 } 789 790 791 /** 792 Example: 793 ------------------- 794 class MyButton : DeclareSignals!(HTMLElement, "onClick") 795 { 796 this(string id) 797 { 798 super(id, true); 799 } 800 801 802 override 803 void onClick(WeakRef!(JSArrayCpp) args) 804 { 805 writeln("OK"); 806 } 807 } 808 ------------------- 809 */ 810 abstract class DeclareSignals(Element, names...) : Element 811 if(is(Element : HTMLElement) && names.length >= 1) 812 { 813 this(T...)(auto ref T args) 814 { 815 super(forward!args); 816 } 817 818 819 final 820 void doJSInitialize(bool b) @property 821 { 822 _doInit = b; 823 } 824 825 826 final 827 void stopPropergation(bool b) @property 828 { 829 _stopProp = b; 830 } 831 832 833 mixin(genDeclMethods); 834 835 override 836 void onStart(HTMLPage page) 837 { 838 super.onStart(page); 839 this.activity.methodHandler.set(this); 840 } 841 842 843 override 844 void onLoad(bool init) 845 { 846 super.onLoad(init); 847 848 if(_doInit){ 849 this.activity.runJS(genSettingEventHandlers(this.id, this.domObject.jsExpr, _stopProp)); 850 } 851 } 852 853 854 private: 855 bool _doInit = true; 856 bool _stopProp = false; 857 858 static 859 { 860 string genDeclMethods() 861 { 862 auto app = appender!string(); 863 864 foreach(s; names) 865 app.formattedWrite(`@JSMethodTag("%1$s"w) `"void %1$s(WeakRef!(const(JSArrayCpp)));\n", s); 866 867 return app.data; 868 } 869 870 871 string genSettingEventHandlers(string id, string domExpr, bool stopProp) 872 { 873 import std.string : toLower; 874 875 auto app = appender!string(); 876 app.formattedWrite("var e = %s;", domExpr); 877 878 foreach(s; names){ 879 if(!stopProp) 880 app.formattedWrite(q{e.%3$s = function() { %1$s.%2$s(); };}, id, s, toLower(s)); 881 else 882 app.formattedWrite(q{e.%3$s = function(ev) { ev.stopPropergation(); %1$s.%2$s(); };}, id, s, toLower(s)); 883 } 884 885 return app.data; 886 } 887 } 888 } 889 890 891 alias DeclDefSignals(Element, names...) = DefineSignals!(DeclareSignals!(Element, names), names); 892 893 894 /** 895 Open context menu when user click right button. 896 */ 897 abstract class DeclareContextMenu(Element, setting...) : DeclareSignals!(Element, "onContextMenu", setting) 898 { 899 this(T...)(auto ref T args) 900 { 901 super(forward!args); 902 } 903 904 905 HTMLPage menuPage() @property; 906 907 908 override 909 void onContextMenu(WeakRef!(const(JSArrayCpp))) 910 { 911 this.activity.popup(this.menuPage); 912 } 913 } 914 915 916 /** 917 Mouse hover event 918 */ 919 abstract class DeclareHoverSignal(Element) : DeclareSignals!(Element, "onMouseOver", "onMouseOut") 920 { 921 this(T...)(auto ref T args) 922 { 923 super(forward!args); 924 } 925 926 927 /** 928 */ 929 final 930 bool hover() @property { return _hovered; } 931 932 933 final 934 void onHoverImpl() 935 { 936 if(_isStarted) 937 onHover(_hovered, Clock.currTime - _startTime); 938 } 939 940 941 /** 942 */ 943 void onHover(bool bOver, Duration dur); 944 945 946 override 947 void onUpdate() 948 { 949 super.onUpdate(); 950 951 onHoverImpl(); 952 } 953 954 955 override 956 void onMouseOver(WeakRef!(const(JSArrayCpp))) 957 { 958 _isStarted = true; 959 _hovered = true; 960 _startTime = Clock.currTime; 961 onHoverImpl(); 962 } 963 964 965 override 966 void onMouseOut(WeakRef!(const(JSArrayCpp))) 967 { 968 _hovered = false; 969 _startTime = Clock.currTime; 970 onHoverImpl(); 971 } 972 973 974 private: 975 bool _isStarted; 976 bool _hovered; 977 SysTime _startTime; 978 } 979 980 981 /** 982 EventPipe 983 */ 984 final class PageSignalHandler : HTMLElement 985 { 986 import carbon.event; 987 988 this(string id = "_event_signal_pipe") 989 { 990 super(id, false); 991 _onStartEvent = new EventManager!(Activity)(); 992 _onAttachEvent = new EventManager!bool(); 993 _onLoadEvent = new EventManager!bool(); 994 _onUpdateEvent = new EventManager!(); 995 _onDetachEvent = new EventManager!(); 996 _onDestroyEvent = new EventManager!(); 997 _onResizeEvent = new EventManager!(size_t, size_t)(); 998 } 999 1000 @property 1001 { 1002 EventManager!(Activity) onStartHandler () { return _onStartEvent; } 1003 EventManager!(bool) onAttachHandler () { return _onAttachEvent; } 1004 EventManager!(bool) onLoadHandler () { return _onLoadEvent; } 1005 EventManager!() onUpdateHandler () { return _onUpdateEvent; } 1006 EventManager!() onDetachHandler () { return _onDetachEvent; } 1007 EventManager!() onDestroyHandler() { return _onDestroyEvent; } 1008 EventManager!(size_t, size_t) onResizeHandler () { return _onResizeEvent; } 1009 } 1010 1011 override 1012 void onStart(HTMLPage page) 1013 { 1014 super.onStart(page); 1015 _onStartEvent.emit(this, page.activity); 1016 } 1017 1018 1019 override 1020 void onAttach(bool bInit) 1021 { 1022 super.onAttach(bInit); 1023 _onAttachEvent.emit(this, bInit); 1024 } 1025 1026 1027 override 1028 void onLoad(bool bInit) 1029 { 1030 super.onLoad(bInit); 1031 _onLoadEvent.emit(this, bInit); 1032 } 1033 1034 1035 override 1036 void onUpdate() 1037 { 1038 super.onUpdate(); 1039 _onUpdateEvent.emit(this); 1040 } 1041 1042 1043 override 1044 void onDetach() 1045 { 1046 super.onDetach(); 1047 _onDetachEvent.emit(this); 1048 } 1049 1050 1051 override 1052 void onDestroy() 1053 { 1054 super.onDestroy(); 1055 _onDestroyEvent.emit(this); 1056 } 1057 1058 1059 override 1060 void onResize(size_t w, size_t h) 1061 { 1062 super.onResize(w, h); 1063 _onResizeEvent.emit(this, w, h); 1064 } 1065 1066 1067 private: 1068 EventManager!(Activity) _onStartEvent; 1069 EventManager!(bool) _onAttachEvent; 1070 EventManager!(bool) _onLoadEvent; 1071 EventManager!() _onUpdateEvent; 1072 EventManager!() _onDetachEvent; 1073 EventManager!() _onDestroyEvent; 1074 EventManager!(size_t, size_t) _onResizeEvent; 1075 } 1076 1077 1078 /** 1079 Selectors API 1080 */ 1081 alias querySelector = querySelectorImpl!false; 1082 1083 1084 /// ditto 1085 alias querySelectorAll = querySelectorImpl!true; 1086 1087 1088 auto querySelectorImpl(bool isAll)(Activity activity, string cssSelector) 1089 { 1090 import awebview.jsvariable; 1091 1092 static struct QuerySelectorResult 1093 { 1094 string jsExpr() const @property { return _jsExpr; } 1095 Activity activity() @property { return _activity; } 1096 1097 1098 mixin JSExprDOMEagerOperators!(); 1099 1100 private: 1101 string _jsExpr; 1102 Activity _activity; 1103 } 1104 1105 QuerySelectorResult res; 1106 res._jsExpr = mixin(Lstr!q{document.%[isAll ? "querySelectorAll" : "querySelector"%](%[toLiteral(cssSelector)%])}); 1107 res._activity = activity; 1108 1109 return res; 1110 }