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; 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 755 756 mixin(genMethod()); 757 758 759 private: 760 mixin(genField()); 761 762 static 763 { 764 string genField() 765 { 766 auto app = appender!string; 767 foreach(s; names) 768 app.formattedWrite("EventManager!(WeakRef!(const(JSArrayCpp))) _%sEvent;\n", s); 769 770 return app.data; 771 } 772 773 774 string genMethod() 775 { 776 auto app = appender!string; 777 foreach(s; names){ 778 app.formattedWrite("ref RestrictedSignal!(FiredContext, WeakRef!(const(JSArrayCpp))) %1$s() { return _%1$sEvent; }\n", s); 779 app.formattedWrite("override void %1$s(WeakRef!(const(JSArrayCpp)) arr) { _%1$sEvent.emit(this, arr); }\n", s); 780 } 781 782 return app.data; 783 } 784 } 785 } 786 787 788 /** 789 Example: 790 ------------------- 791 class MyButton : DeclareSignals!(HTMLElement, "onClick") 792 { 793 this(string id) 794 { 795 super(id, true); 796 } 797 798 799 override 800 void onClick(WeakRef!(JSArrayCpp) args) 801 { 802 writeln("OK"); 803 } 804 } 805 ------------------- 806 */ 807 abstract class DeclareSignals(Element, names...) : Element 808 if(is(Element : HTMLElement) && names.length >= 1) 809 { 810 this(T...)(auto ref T args) 811 { 812 super(forward!args); 813 } 814 815 816 final 817 void doJSInitialize(bool b) @property 818 { 819 _doInit = b; 820 } 821 822 823 final 824 void stopPropergation(bool b) @property 825 { 826 _stopProp = b; 827 } 828 829 830 mixin(genDeclMethods); 831 832 override 833 void onStart(HTMLPage page) 834 { 835 super.onStart(page); 836 this.activity.methodHandler.set(this); 837 } 838 839 840 override 841 void onLoad(bool init) 842 { 843 super.onLoad(init); 844 845 if(_doInit){ 846 this.activity.runJS(genSettingEventHandlers(this.id, this.domObject.jsExpr, _stopProp)); 847 } 848 } 849 850 851 private: 852 bool _doInit = true; 853 bool _stopProp = false; 854 855 static 856 { 857 string genDeclMethods() 858 { 859 auto app = appender!string(); 860 861 foreach(s; names) 862 app.formattedWrite(`@JSMethodTag("%1$s"w) `"void %1$s(WeakRef!(const(JSArrayCpp)));\n", s); 863 864 return app.data; 865 } 866 867 868 string genSettingEventHandlers(string id, string domExpr, bool stopProp) 869 { 870 import std.string : toLower; 871 872 auto app = appender!string(); 873 app.formattedWrite("var e = %s;", domExpr); 874 875 foreach(s; names){ 876 if(!stopProp) 877 app.formattedWrite(q{e.%3$s = function() { %1$s.%2$s(); };}, id, s, toLower(s)); 878 else 879 app.formattedWrite(q{e.%3$s = function(ev) { ev.stopPropergation(); %1$s.%2$s(); };}, id, s, toLower(s)); 880 } 881 882 return app.data; 883 } 884 } 885 } 886 887 888 alias DeclDefSignals(Element, names...) = DefineSignals!(DeclareSignals!(Element, names), names); 889 890 891 /** 892 Open context menu when user click right button. 893 */ 894 abstract class DeclareContextMenu(Element, setting...) : DeclareSignals!(Element, "onContextMenu", setting) 895 { 896 this(T...)(auto ref T args) 897 { 898 super(forward!args); 899 } 900 901 902 HTMLPage menuPage() @property; 903 904 905 override 906 void onContextMenu(WeakRef!(const(JSArrayCpp))) 907 { 908 this.activity.popup(this.menuPage); 909 } 910 } 911 912 913 /** 914 Mouse hover event 915 */ 916 abstract class DeclareHoverSignal(Element) : DeclareSignals!(Element, "onMouseOver", "onMouseOut") 917 { 918 this(T...)(auto ref T args) 919 { 920 super(forward!args); 921 } 922 923 924 /** 925 */ 926 final 927 bool hover() @property { return _hovered; } 928 929 930 final 931 void onHoverImpl() 932 { 933 if(_isStarted) 934 onHover(_hovered, Clock.currTime - _startTime); 935 } 936 937 938 /** 939 */ 940 void onHover(bool bOver, Duration dur); 941 942 943 override 944 void onUpdate() 945 { 946 super.onUpdate(); 947 948 onHoverImpl(); 949 } 950 951 952 override 953 void onMouseOver(WeakRef!(const(JSArrayCpp))) 954 { 955 _isStarted = true; 956 _hovered = true; 957 _startTime = Clock.currTime; 958 onHoverImpl(); 959 } 960 961 962 override 963 void onMouseOut(WeakRef!(const(JSArrayCpp))) 964 { 965 _hovered = false; 966 _startTime = Clock.currTime; 967 onHoverImpl(); 968 } 969 970 971 private: 972 bool _isStarted; 973 bool _hovered; 974 SysTime _startTime; 975 } 976 977 978 /** 979 EventPipe 980 */ 981 final class PageSignalHandler : HTMLElement 982 { 983 import carbon.event; 984 985 this(string id = "_event_signal_pipe") { super(id, false); } 986 987 @property 988 { 989 ref RestrictedSignal!(FiredContext, Activity) onStartHandler () { return _onStartEvent; } 990 ref RestrictedSignal!(FiredContext, bool) onAttachHandler () { return _onAttachEvent; } 991 ref RestrictedSignal!(FiredContext, bool) onLoadHandler () { return _onLoadEvent; } 992 ref RestrictedSignal!(FiredContext) onUpdateHandler () { return _onUpdateEvent; } 993 ref RestrictedSignal!(FiredContext) onDetachHandler () { return _onDetachEvent; } 994 ref RestrictedSignal!(FiredContext) onDestroyHandler() { return _onDestroyEvent; } 995 ref RestrictedSignal!(FiredContext, size_t, size_t) onResizeHandler () { return _onResizeEvent; } 996 } 997 998 override 999 void onStart(HTMLPage page) 1000 { 1001 super.onStart(page); 1002 _onStartEvent.emit(this, page.activity); 1003 } 1004 1005 1006 override 1007 void onAttach(bool bInit) 1008 { 1009 super.onAttach(bInit); 1010 _onAttachEvent.emit(this, bInit); 1011 } 1012 1013 1014 override 1015 void onLoad(bool bInit) 1016 { 1017 super.onLoad(bInit); 1018 _onLoadEvent.emit(this, bInit); 1019 } 1020 1021 1022 override 1023 void onUpdate() 1024 { 1025 super.onUpdate(); 1026 _onUpdateEvent.emit(this, ); 1027 } 1028 1029 1030 override 1031 void onDetach() 1032 { 1033 super.onDetach(); 1034 _onDetachEvent.emit(this, ); 1035 } 1036 1037 1038 override 1039 void onDestroy() 1040 { 1041 super.onDestroy(); 1042 _onDestroyEvent.emit(this, ); 1043 } 1044 1045 1046 override 1047 void onResize(size_t w, size_t h) 1048 { 1049 super.onResize(w, h); 1050 _onResizeEvent.emit(this, w, h); 1051 } 1052 1053 1054 private: 1055 EventManager!(Activity) _onStartEvent; 1056 EventManager!(bool) _onAttachEvent; 1057 EventManager!(bool) _onLoadEvent; 1058 EventManager!() _onUpdateEvent; 1059 EventManager!() _onDetachEvent; 1060 EventManager!() _onDestroyEvent; 1061 EventManager!(size_t, size_t) _onResizeEvent; 1062 } 1063 1064 1065 /** 1066 Selectors API 1067 */ 1068 alias querySelector = querySelectorImpl!false; 1069 1070 1071 /// ditto 1072 alias querySelectorAll = querySelectorImpl!true; 1073 1074 1075 auto querySelectorImpl(bool isAll)(Activity activity, string cssSelector) 1076 { 1077 import awebview.jsvariable; 1078 1079 static struct QuerySelectorResult 1080 { 1081 string jsExpr() const @property { return _jsExpr; } 1082 Activity activity() @property { return _activity; } 1083 1084 1085 mixin JSExprDOMEagerOperators!(); 1086 1087 private: 1088 string _jsExpr; 1089 Activity _activity; 1090 } 1091 1092 QuerySelectorResult res; 1093 res._jsExpr = mixin(Lstr!q{document.%[isAll ? "querySelectorAll" : "querySelector"%](%[toLiteral(cssSelector)%])}); 1094 res._activity = activity; 1095 1096 return res; 1097 }