Przeglądaj źródła

[Hide] File browser alpha, with previews and fast search (#269)

* [fileBrowser] First draft of the filebrowser
Valden 3 miesięcy temu
rodzic
commit
499c970222

+ 45 - 18
bin/app.html

@@ -24,10 +24,29 @@
 
 
 <script>
-	var win = nw.Window.get();
-	win.on('close', function() {
-		nw.App.quit();
-	});
+	function getQueryVariable(variable)
+	{
+			var query = window.location.search.substring(1);
+			var vars = query.split("&");
+			for (var i=0;i<vars.length;i++) {
+						var pair = vars[i].split("=");
+						if(pair[0] == variable){return pair[1];}
+			}
+			return(false);
+	}
+
+	var thumbnail = getQueryVariable("thumbnail");
+	if (!thumbnail) {
+		var win = nw.Window.get();
+		win.on('close', function() {
+			chrome.app.window.getAll().forEach(win => {
+				win.close(true);
+			});
+			nw.App.closeAllWindows();
+			nw.App.quit();
+		});
+	}
+
 </script>
 
 <script>
@@ -68,7 +87,9 @@
 	</menu>
 	<menu label="View" class="view">
 		<menu label="Resources" component="hide.view.FileTree" state='{"path":""}'></menu>
-		<menu label="Directory" component="hide.view.FileTree"></menu>
+			<menu label="Directory" component="hide.view.FileTree"></menu>
+		<menu label="File Browser" component="hide.view.FileBrowser" state='{}'></menu>
+		<menu label="Inspector" component="hide.view.Inspector" state='{}'></menu>
 		<separator></separator>
 		<menu label="About" component="hide.view.About"></menu>
 		<menu label="Debug" class="debug"></menu>
@@ -124,22 +145,24 @@
 </xml>
 <script src="hide.js"></script>
 <script>
-	// fix for monaco
-	var _R = Reflect;
-	Reflect = global.Reflect;
-	for( f in _R )
-		Reflect[f] = _R[f];
+	if (!thumbnail) {
+		// fix for monaco
+		var _R = Reflect;
+		Reflect = global.Reflect;
+		for( f in _R )
+			Reflect[f] = _R[f];
 
-	// tmp fix jsonWorker in monaco 0.52, see https://github.com/microsoft/monaco-editor/issues/4778
-	var appPath = hide.tools.IdeData.getAppPath();
+		// tmp fix jsonWorker in monaco 0.52, see https://github.com/microsoft/monaco-editor/issues/4778
+		var appPath = hide.tools.IdeData.getAppPath();
 
-	if (appPath != null && !appPath.endsWith("/")) {
-		appPath += "/";
-	}
+		if (appPath != null && !appPath.endsWith("/")) {
+			appPath += "/";
+		}
 
-	var libPath = (appPath == null ? '' : appPath) + 'libs/monaco/min';
-	amdRequire.config({ baseUrl: libPath});
-	amdRequire(['vs/editor/editor.main'], function() { });
+		var libPath = (appPath == null ? '' : appPath) + 'libs/monaco/min';
+		amdRequire.config({ baseUrl: libPath});
+		amdRequire(['vs/editor/editor.main'], function() { });
+	}
 </script>
 
 
@@ -153,6 +176,10 @@
 	var ext = file.split(".").pop().toLowerCase();
 	if( ext == "js")
 	{
+		// destroy self if we are in thumbnail generation mode
+		if (thumbnail)
+			nw.Window.get().close(true);
+
 		if (timer != 0)
 			clearTimeout(timer);
 		timer = setTimeout(function() {

+ 429 - 0
bin/res/icons/svg/LICENCE.txt

@@ -0,0 +1,429 @@
+Icons from Blender https://ui.blender.org/icons
+
+Attribution-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+     Considerations for licensors: Our public licenses are
+     intended for use by those authorized to give the public
+     permission to use material in ways otherwise restricted by
+     copyright and certain other rights. Our licenses are
+     irrevocable. Licensors should read and understand the terms
+     and conditions of the license they choose before applying it.
+     Licensors should also secure all rights necessary before
+     applying our licenses so that the public can reuse the
+     material as expected. Licensors should clearly mark any
+     material not subject to the license. This includes other CC-
+     licensed material, or material used under an exception or
+     limitation to copyright. More considerations for licensors:
+	wiki.creativecommons.org/Considerations_for_licensors
+
+     Considerations for the public: By using one of our public
+     licenses, a licensor grants the public permission to use the
+     licensed material under specified terms and conditions. If
+     the licensor's permission is not necessary for any reason--for
+     example, because of any applicable exception or limitation to
+     copyright--then that use is not regulated by the license. Our
+     licenses grant only permissions under copyright and certain
+     other rights that a licensor has authority to grant. Use of
+     the licensed material may still be restricted for other
+     reasons, including because others have copyright or other
+     rights in the material. A licensor may make special requests,
+     such as asking that all changes be marked or described.
+     Although not required by our licenses, you are encouraged to
+     respect those requests where reasonable. More_considerations
+     for the public:
+	wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+  a. Adapted Material means material subject to Copyright and Similar
+     Rights that is derived from or based upon the Licensed Material
+     and in which the Licensed Material is translated, altered,
+     arranged, transformed, or otherwise modified in a manner requiring
+     permission under the Copyright and Similar Rights held by the
+     Licensor. For purposes of this Public License, where the Licensed
+     Material is a musical work, performance, or sound recording,
+     Adapted Material is always produced where the Licensed Material is
+     synched in timed relation with a moving image.
+
+  b. Adapter's License means the license You apply to Your Copyright
+     and Similar Rights in Your contributions to Adapted Material in
+     accordance with the terms and conditions of this Public License.
+
+  c. BY-SA Compatible License means a license listed at
+     creativecommons.org/compatiblelicenses, approved by Creative
+     Commons as essentially the equivalent of this Public License.
+
+  d. Copyright and Similar Rights means copyright and/or similar rights
+     closely related to copyright including, without limitation,
+     performance, broadcast, sound recording, and Sui Generis Database
+     Rights, without regard to how the rights are labeled or
+     categorized. For purposes of this Public License, the rights
+     specified in Section 2(b)(1)-(2) are not Copyright and Similar
+     Rights.
+
+  e. Effective Technological Measures means those measures that, in the
+     absence of proper authority, may not be circumvented under laws
+     fulfilling obligations under Article 11 of the WIPO Copyright
+     Treaty adopted on December 20, 1996, and/or similar international
+     agreements.
+
+  f. Exceptions and Limitations means fair use, fair dealing, and/or
+     any other exception or limitation to Copyright and Similar Rights
+     that applies to Your use of the Licensed Material.
+
+  g. License Elements means the license attributes listed in the name
+     of a Creative Commons Public License. The License Elements of this
+     Public License are Attribution and ShareAlike.
+
+  h. Licensed Material means the artistic or literary work, database,
+     or other material to which the Licensor applied this Public
+     License.
+
+  i. Licensed Rights means the rights granted to You subject to the
+     terms and conditions of this Public License, which are limited to
+     all Copyright and Similar Rights that apply to Your use of the
+     Licensed Material and that the Licensor has authority to license.
+
+  j. Licensor means the individual(s) or entity(ies) granting rights
+     under this Public License.
+
+  k. Share means to provide material to the public by any means or
+     process that requires permission under the Licensed Rights, such
+     as reproduction, public display, public performance, distribution,
+     dissemination, communication, or importation, and to make material
+     available to the public including in ways that members of the
+     public may access the material from a place and at a time
+     individually chosen by them.
+
+  l. Sui Generis Database Rights means rights other than copyright
+     resulting from Directive 96/9/EC of the European Parliament and of
+     the Council of 11 March 1996 on the legal protection of databases,
+     as amended and/or succeeded, as well as other essentially
+     equivalent rights anywhere in the world.
+
+  m. You means the individual or entity exercising the Licensed Rights
+     under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+  a. License grant.
+
+       1. Subject to the terms and conditions of this Public License,
+          the Licensor hereby grants You a worldwide, royalty-free,
+          non-sublicensable, non-exclusive, irrevocable license to
+          exercise the Licensed Rights in the Licensed Material to:
+
+            a. reproduce and Share the Licensed Material, in whole or
+               in part; and
+
+            b. produce, reproduce, and Share Adapted Material.
+
+       2. Exceptions and Limitations. For the avoidance of doubt, where
+          Exceptions and Limitations apply to Your use, this Public
+          License does not apply, and You do not need to comply with
+          its terms and conditions.
+
+       3. Term. The term of this Public License is specified in Section
+          6(a).
+
+       4. Media and formats; technical modifications allowed. The
+          Licensor authorizes You to exercise the Licensed Rights in
+          all media and formats whether now known or hereafter created,
+          and to make technical modifications necessary to do so. The
+          Licensor waives and/or agrees not to assert any right or
+          authority to forbid You from making technical modifications
+          necessary to exercise the Licensed Rights, including
+          technical modifications necessary to circumvent Effective
+          Technological Measures. For purposes of this Public License,
+          simply making modifications authorized by this Section 2(a)
+          (4) never produces Adapted Material.
+
+       5. Downstream recipients.
+
+            a. Offer from the Licensor -- Licensed Material. Every
+               recipient of the Licensed Material automatically
+               receives an offer from the Licensor to exercise the
+               Licensed Rights under the terms and conditions of this
+               Public License.
+
+            b. Additional offer from the Licensor -- Adapted Material.
+               Every recipient of Adapted Material from You
+               automatically receives an offer from the Licensor to
+               exercise the Licensed Rights in the Adapted Material
+               under the conditions of the Adapter's License You apply.
+
+            c. No downstream restrictions. You may not offer or impose
+               any additional or different terms or conditions on, or
+               apply any Effective Technological Measures to, the
+               Licensed Material if doing so restricts exercise of the
+               Licensed Rights by any recipient of the Licensed
+               Material.
+
+       6. No endorsement. Nothing in this Public License constitutes or
+          may be construed as permission to assert or imply that You
+          are, or that Your use of the Licensed Material is, connected
+          with, or sponsored, endorsed, or granted official status by,
+          the Licensor or others designated to receive attribution as
+          provided in Section 3(a)(1)(A)(i).
+
+  b. Other rights.
+
+       1. Moral rights, such as the right of integrity, are not
+          licensed under this Public License, nor are publicity,
+          privacy, and/or other similar personality rights; however, to
+          the extent possible, the Licensor waives and/or agrees not to
+          assert any such rights held by the Licensor to the limited
+          extent necessary to allow You to exercise the Licensed
+          Rights, but not otherwise.
+
+       2. Patent and trademark rights are not licensed under this
+          Public License.
+
+       3. To the extent possible, the Licensor waives any right to
+          collect royalties from You for the exercise of the Licensed
+          Rights, whether directly or through a collecting society
+          under any voluntary or waivable statutory or compulsory
+          licensing scheme. In all other cases the Licensor expressly
+          reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+  a. Attribution.
+
+       1. If You Share the Licensed Material (including in modified
+          form), You must:
+
+            a. retain the following if it is supplied by the Licensor
+               with the Licensed Material:
+
+                 i. identification of the creator(s) of the Licensed
+                    Material and any others designated to receive
+                    attribution, in any reasonable manner requested by
+                    the Licensor (including by pseudonym if
+                    designated);
+
+                ii. a copyright notice;
+
+               iii. a notice that refers to this Public License;
+
+                iv. a notice that refers to the disclaimer of
+                    warranties;
+
+                 v. a URI or hyperlink to the Licensed Material to the
+                    extent reasonably practicable;
+
+            b. indicate if You modified the Licensed Material and
+               retain an indication of any previous modifications; and
+
+            c. indicate the Licensed Material is licensed under this
+               Public License, and include the text of, or the URI or
+               hyperlink to, this Public License.
+
+       2. You may satisfy the conditions in Section 3(a)(1) in any
+          reasonable manner based on the medium, means, and context in
+          which You Share the Licensed Material. For example, it may be
+          reasonable to satisfy the conditions by providing a URI or
+          hyperlink to a resource that includes the required
+          information.
+
+       3. If requested by the Licensor, You must remove any of the
+          information required by Section 3(a)(1)(A) to the extent
+          reasonably practicable.
+
+  b. ShareAlike.
+
+     In addition to the conditions in Section 3(a), if You Share
+     Adapted Material You produce, the following conditions also apply.
+
+       1. The Adapter's License You apply must be a Creative Commons
+          license with the same License Elements, this version or
+          later, or a BY-SA Compatible License.
+
+       2. You must include the text of, or the URI or hyperlink to, the
+          Adapter's License You apply. You may satisfy this condition
+          in any reasonable manner based on the medium, means, and
+          context in which You Share Adapted Material.
+
+       3. You may not offer or impose any additional or different terms
+          or conditions on, or apply any Effective Technological
+          Measures to, Adapted Material that restrict exercise of the
+          rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+     to extract, reuse, reproduce, and Share all or a substantial
+     portion of the contents of the database;
+
+  b. if You include all or a substantial portion of the database
+     contents in a database in which You have Sui Generis Database
+     Rights, then the database in which You have Sui Generis Database
+     Rights (but not its individual contents) is Adapted Material,
+
+     including for purposes of Section 3(b); and
+  c. You must comply with the conditions in Section 3(a) if You Share
+     all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+  c. The disclaimer of warranties and limitation of liability provided
+     above shall be interpreted in a manner that, to the extent
+     possible, most closely approximates an absolute disclaimer and
+     waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+  a. This Public License applies for the term of the Copyright and
+     Similar Rights licensed here. However, if You fail to comply with
+     this Public License, then Your rights under this Public License
+     terminate automatically.
+
+  b. Where Your right to use the Licensed Material has terminated under
+     Section 6(a), it reinstates:
+
+       1. automatically as of the date the violation is cured, provided
+          it is cured within 30 days of Your discovery of the
+          violation; or
+
+       2. upon express reinstatement by the Licensor.
+
+     For the avoidance of doubt, this Section 6(b) does not affect any
+     right the Licensor may have to seek remedies for Your violations
+     of this Public License.
+
+  c. For the avoidance of doubt, the Licensor may also offer the
+     Licensed Material under separate terms or conditions or stop
+     distributing the Licensed Material at any time; however, doing so
+     will not terminate this Public License.
+
+  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+     License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+  a. The Licensor shall not be bound by any additional or different
+     terms or conditions communicated by You unless expressly agreed.
+
+  b. Any arrangements, understandings, or agreements regarding the
+     Licensed Material not stated herein are separate from and
+     independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+  a. For the avoidance of doubt, this Public License does not, and
+     shall not be interpreted to, reduce, limit, restrict, or impose
+     conditions on any use of the Licensed Material that could lawfully
+     be made without permission under this Public License.
+
+  b. To the extent possible, if any provision of this Public License is
+     deemed unenforceable, it shall be automatically reformed to the
+     minimum extent necessary to make it enforceable. If the provision
+     cannot be reformed, it shall be severed from this Public License
+     without affecting the enforceability of the remaining terms and
+     conditions.
+
+  c. No term or condition of this Public License will be waived and no
+     failure to comply consented to unless expressly agreed to by the
+     Licensor.
+
+  d. Nothing in this Public License constitutes or may be interpreted
+     as a limitation upon, or waiver of, any privileges and immunities
+     that apply to the Licensor or You, including from the legal
+     processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.

+ 1 - 0
bin/res/icons/svg/big_folder.svg

@@ -0,0 +1 @@
+<svg id="svg4185" height="1300" viewBox="0 0 1600 1300" width="1600" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"><sodipodi:namedview pagecolor="#303030" showgrid="true"><inkscape:grid id="grid5" units="px" spacingx="100" spacingy="100" color="#4772b3" opacity="0.2" visible="true" /></sodipodi:namedview><g transform="matrix(3.40716 0 0 3.4049685 6.883458 -2342.962)"><path id="path3350" d="m46.165476 717.44637h93.219344c15.7258.001 18.61638 5.95025 22.81669 10.88994l14.74763 16.42645c1.19974 1.50075 2.35846 2.05667 4.90331 2.04456h237.11359c11.80105 0 19.25432 7.94778 19.25432 18.963l-.00003 264.87648c0 5.5144-4.70131 9.9062-10.21572 9.9068l-390.848314.043c-5.51443.0006-9.850844-4.4137-9.850844-9.9281v-294.25912c0-10.22939 7.58285-18.96301 18.860024-18.96301z" fill="#b2b2b2"/><path id="rect3347" d="m46.165476 776.17182h372.800564c11.80105 0 19.17809 7.94779 19.1815 18.96301l.0728 235.53337c.002 5.5144-4.73072 9.8744-10.24513 9.8748l-390.818934.028c-5.51443.0004-9.850844-4.388-9.850844-9.9024v-235.53377c0-10.22939 7.58285-18.96301 18.860024-18.96301z" fill="#fff"/><g id="g2" fill-opacity=".15"><path id="rect4358" d="m45.643295 1008.0261h374.05457v3.143315h-374.05457z"/><path id="rect4362" d="m45.46962 993.42914h374.05457v3.143315h-374.05457z"/><path id="rect4360" d="m45.538475 978.53735h374.05457v3.143315h-374.05457z"/></g></g></svg>

+ 1 - 0
bin/res/icons/svg/close.svg

@@ -0,0 +1 @@
+<svg height="1000" viewBox="0 0 1000 1000" width="1000" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"><sodipodi:namedview pagecolor="#303030" showgrid="true"><inkscape:grid id="grid5" units="px" spacingx="100" spacingy="100" color="#4772b3" opacity="0.2" visible="true" /></sodipodi:namedview><g fill="#fff"><path d="m306.99023 241.72461a.66673335.66673335 0 0 0 -.65625.67578v5.93359h-5.93359a.66673335.66673335 0 1 0 0 1.33204h5.93359v5.93359a.66673335.66673335 0 1 0 1.33204 0v-5.93359h5.93359a.66673335.66673335 0 1 0 0-1.33204h-5.93359v-5.93359a.66673335.66673335 0 0 0 -.67579-.67578z" transform="matrix(-53.033 -53.033 -53.033 53.033 29986.348 3575.914)"/></g></svg>

+ 1 - 0
bin/res/icons/svg/file.svg

@@ -0,0 +1 @@
+<svg height="1600" viewBox="0 0 1400 1600" width="1400" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"><sodipodi:namedview pagecolor="#303030" showgrid="true"><inkscape:grid id="grid5" units="px" spacingx="100" spacingy="100" color="#4772b3" opacity="0.2" visible="true" /></sodipodi:namedview><g fill="#fff"><path d="m158.48048 492.99995c-.15153.004-.29304.0766-.38477.19727l-4.94922 4.94921c-.31479.315-.0918.85335.35352.85352h5c.27613-.00003.49997-.22387.5-.5v-4.5h5v12h-10v-6h-1v6.5c.00003.27613.22387.49997.5.5h11c.27613-.00003.49997-.22387.5-.5v-13c-.00003-.27613-.22387-.49997-.5-.5h-6c-.005-.00006-.009-.00006-.0137 0-.00067.00002-.001-.00002-.002 0-.001.00004-.003-.00005-.004 0z" fill-rule="evenodd" opacity=".6" transform="matrix(100 0 0 100 -15199.958 -49199.9928)"/></g></svg>

+ 1 - 0
bin/res/icons/svg/file_parent.svg

@@ -0,0 +1 @@
+<svg height="1400" viewBox="0 0 1300 1400" width="1300" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"><sodipodi:namedview pagecolor="#303030" showgrid="true"><inkscape:grid id="grid5" units="px" spacingx="100" spacingy="100" color="#4772b3" opacity="0.2" visible="true" /></sodipodi:namedview><g fill="#fff"><path d="m261.44603 53.01256c-.12976.0036-.25303.05754-.34375.15039l-2.98353 2.983534c-.19518.195265-.19518.511767 0 .707032l2.98353 2.983534c.47126.490506 1.19754-.235768.70704-.707032l-2.13002-2.130018h7.78214c.83435 0 1.5.665651 1.5 1.5v5c-.01.676161 1.00956.676161 1 0v-5c0-1.374789-1.12521-2.5-2.5-2.5h-7.78214l2.13002-2.130018c.32528-.318006.0914-.869901-.36329-.857422z" transform="matrix(0 100 100 0 -5200.9736 -25696.694)"/></g></svg>

+ 2 - 0
bin/res/icons/svg/generate.hx

@@ -0,0 +1,2 @@
+
+

+ 0 - 0
bin/res/icons/svg/icons.css


+ 0 - 0
bin/res/icons/svg/icons.less


Plik diff jest za duży
+ 9 - 0
bin/res/icons/svg/loading.svg


+ 1 - 0
bin/res/icons/svg/right_caret.svg

@@ -0,0 +1 @@
+<svg height="1100" viewBox="0 0 700 1100" width="700" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"><sodipodi:namedview pagecolor="#303030" showgrid="true"><inkscape:grid id="grid5" units="px" spacingx="100" spacingy="100" color="#4772b3" opacity="0.2" visible="true" /></sodipodi:namedview><g fill="#fff"><path d="m410.49414 601.99414a.50005.50005 0 0 0 -.34766.85938l3.64649 3.64648-3.64649 3.64648a.50005.50005 0 1 0 .70704.70704l4-4a.50005.50005 0 0 0 0-.70704l-4-4a.50005.50005 0 0 0 -.35938-.15234z" transform="matrix(100 0 0 100 -40899.645 -60100.058)"/></g></svg>

+ 1 - 0
bin/res/icons/svg/search.svg

@@ -0,0 +1 @@
+<svg height="1600" viewBox="0 0 1600 1600" width="1600" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"><sodipodi:namedview pagecolor="#303030" showgrid="true"><inkscape:grid id="grid5" units="px" spacingx="100" spacingy="100" color="#4772b3" opacity="0.2" visible="true" /></sodipodi:namedview><g fill="#fff"><path d="m99 599c-2.74773 0-5 2.25226-5 5 0 1.1945.441488 2.28163 1.148438 3.14453l-5.001954 5.00195a.50005.50005 0 1 0 .707032.70704l5.001953-5.00196c.8629.70695 1.950034 1.14844 3.144531 1.14844 2.74773 0 5-2.25227 5-5 0-2.74774-2.25227-5-5-5zm0 1c2.20227 0 4 1.79773 4 4 0 2.20226-1.79773 4-4 4s-4-1.79774-4-4c0-2.20227 1.79773-4 4-4z" transform="matrix(100 0 0 100 -8899.6439 -59800.356)"/></g></svg>

+ 1 - 0
bin/res/icons/svg/unknown_file.svg

@@ -0,0 +1 @@
+<svg id="svg4" height="1600" viewBox="0 0 1600 1599.9999" width="1600" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"><sodipodi:namedview pagecolor="#303030" showgrid="true"><inkscape:grid id="grid5" units="px" spacingx="100" spacingy="100" color="#4772b3" opacity="0.2" visible="true" /></sodipodi:namedview><g id="g1"><g id="blender_info" fill="none"><path id="path1" d="m799.78455 179.90086c342.43575 0 620.55515 277.73924 620.55515 620.0115 0 342.27234-278.1194 620.50664-620.55515 620.50664-342.43441 0-620.16037-278.2343-620.16037-620.50664 0-342.27226 277.72596-620.0115 620.16037-620.0115z" stroke-width="1.00181"/></g><g id="blender_text" fill="#fff" fill-rule="evenodd" transform="matrix(26.924199 0 0 26.917795 -1784.6771 -61.216)"><path id="path3" d="m97.000019 18.000014c-4.683996-.023-7.966055 2.174981-7.964055 2.185981l1.513001 3.293012s1.513005-.691021 2.961004-1.166021c1.122999-.367999 1.907021-.525979 2.99002-.525979.896999 0 2.460047.162975 3.257046.864974.868995.766 1.035965 1.712023 1.035965 2.482022s-.09699 1.589015-.63699 2.399014c-.565995.846999-1.637017 1.811979-2.841016 2.813978-1.227998 1.020999-1.890022 1.693985-2.599021 2.905984-.683999 1.171998-.715986 3.242051-.715986 3.511051v1.235968h3.499988v-.855034c0-.741999.119993-1.784997.599992-2.484996.680999-.992999 1.934063-1.958968 2.411063-2.340967.478-.383 1.94199-1.614019 2.82499-2.656018.52-.613999.93395-1.355011 1.22595-2.142011.329-.883999.42401-1.825988.42401-2.566988 0-2.390997-.94598-4.165953-2.13098-5.104952-.642-.509-2.29798-1.831018-5.854981-1.849018zm-.999987 23.000003a3 3 0 0 0 -3.000031 2.999947 3 3 0 0 0 3.000031 3.00002 3 3 0 0 0 2.999959-3.00002 3 3 0 0 0 -2.999959-2.999947z"/><path id="path2" d="m96 6c14.35 0 26 11.65 26 26s-11.65 26-26 26-26-11.65-26-26 11.65-26 26-26zm0 3c12.694 0 23 10.306 23 23s-10.306 23-23 23-23-10.306-23-23 10.306-23 23-23z"/></g></g></svg>

+ 496 - 1
bin/style.css

@@ -4312,11 +4312,13 @@ hide-popover hide-content {
 }
 :root {
   --hover-highlight: rgba(114, 180, 255, 0.5);
-  --selection: rgba(114, 180, 255);
+  --selection: #3185ce;
   --hover: #444;
   --basic-shadow: 2px 2px 3px rgba(0, 0, 0, 0.75);
   --sublte-shadow: 1px 1px 3px rgba(0, 0, 0, 0.33);
   --basic-border: 1px solid #353535;
+  --basic-border-hover: 1px solid #555;
+  --basic-border-focused: 1px solid var(--selection);
   --basic-border-radius: 5px;
   --basic-padding: 0.2em;
   --bg-0: #000;
@@ -4471,6 +4473,14 @@ fancy-toolbar {
   display: flex;
   flex-direction: row;
   align-items: center;
+  padding-left: 0.25em;
+  padding-right: 0.25em;
+}
+fancy-toolbar.shadow {
+  position: relative;
+  background-color: #272727;
+  box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2);
+  z-index: 999;
 }
 fancy-button {
   position: relative;
@@ -4918,3 +4928,488 @@ blend-space-2d-root properties-container .hide-properties dl > div .hide-range i
 .fancy-hide {
   display: none;
 }
+file-browser {
+  display: flex;
+  flex-direction: row;
+  height: 100%;
+}
+file-browser .left {
+  width: 300px;
+  min-width: 100px;
+  background-color: mediumaquamarine;
+}
+file-browser .right {
+  flex: 1 1;
+  display: flex;
+  flex-direction: column;
+}
+fancy-flex-fill {
+  flex: 1 1;
+}
+fancy-icon {
+  height: 1em;
+  display: block;
+  mask-size: contain;
+  background-color: currentColor;
+  mask-mode: luminance;
+  mask-repeat: no-repeat;
+  aspect-ratio: 1/1;
+  mask-position: center;
+}
+fancy-icon.small {
+  height: 12px;
+}
+fancy-icon.big {
+  height: 22px;
+}
+fancy-icon.fi-right-caret {
+  mask-image: url("res/icons/svg/right_caret.svg");
+}
+fancy-icon.fi-close {
+  mask-image: url("res/icons/svg/close.svg");
+}
+fancy-icon.fi-search {
+  mask-image: url("res/icons/svg/search.svg");
+}
+fancy-closable {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #333;
+  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.33);
+  margin-bottom: 2px;
+  overflow: hidden;
+  height: 0px;
+}
+fancy-closable * {
+  box-sizing: border-box;
+}
+fancy-search {
+  position: relative;
+  flex: 1 1;
+  padding: 2px;
+  max-width: 400px;
+}
+fancy-search,
+fancy-search * {
+  box-sizing: border-box;
+}
+fancy-search > input {
+  border: var(--basic-border);
+  border-radius: var(--basic-border-radius);
+  min-width: 0;
+  width: 100%;
+  height: 100%;
+}
+fancy-search > input:hover {
+  border: var(--basic-border-hover);
+}
+fancy-search > input:focus {
+  border: var(--basic-border-focused);
+  outline: none;
+}
+fancy-search .search-icon {
+  position: absolute;
+  right: 0.5em;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #999;
+  z-index: 1;
+}
+fancy-tree {
+  --anim-speed: 0.1s;
+  background-color: #222;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+fancy-tree:focus {
+  outline: none;
+}
+fancy-tree fancy-wrapper {
+  display: block;
+  width: 100%;
+  flex: 1 1;
+  min-width: 100px;
+  overflow-x: clip;
+  overflow-y: auto;
+}
+fancy-tree fancy-wrapper fancy-tree-item {
+  --depth: 0;
+  display: flex;
+  flex-direction: column;
+  --highlight: 0;
+  box-sizing: border-box;
+}
+fancy-tree fancy-wrapper fancy-tree-item.hide-search {
+  overflow: hidden;
+  height: 0px;
+  display: none;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-children {
+  height: 0px;
+  overflow: hidden;
+  display: none;
+}
+fancy-tree fancy-wrapper fancy-tree-item.open > fancy-tree-children {
+  display: block;
+  height: auto;
+}
+fancy-tree fancy-wrapper fancy-tree-item.open > fancy-tree-children:has(fancy-tree-name:focus) {
+  overflow: visible;
+}
+fancy-tree fancy-wrapper fancy-tree-item.open > fancy-tree-header .caret::before {
+  transform: rotate(90deg);
+}
+fancy-tree fancy-wrapper fancy-tree-item:has(fancy-tree-children .selected) > fancy-tree-header fancy-tree-name {
+  text-decoration: underline var(--selection);
+}
+fancy-tree fancy-wrapper fancy-tree-item.current:not(.selected) {
+  --highlight: 10;
+}
+fancy-tree fancy-wrapper fancy-tree-item.selected > fancy-tree-header {
+  color: white;
+  --background: var(--selection);
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header {
+  padding-left: calc((var(--depth) - 1) * 1em + 0.25em);
+  box-sizing: border-box;
+  height: 20px;
+  max-height: 20px;
+  min-height: 20px;
+  --background: #222;
+  background-color: hsl(from var(--background) h s calc(var(--highlight) + l));
+  display: flex;
+  position: relative;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header:has(>.caret:hover, >fancy-tree-name:hover) {
+  --highlight: 10;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header fancy-tree-icon {
+  align-self: center;
+  width: 16px;
+  height: 16px;
+  display: block;
+  flex-shrink: 0;
+  filter: drop-shadow(1px 1px rgba(0, 0, 0, 0.5));
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header fancy-tree-icon.caret {
+  width: 16px;
+  height: 16px;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header fancy-tree-icon.caret::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  margin: 2px;
+  width: 12px;
+  height: 12px;
+  transition: transform var(--anim-speed);
+  background-color: currentColor;
+  mask-image: url("res/icons/svg/right_caret.svg");
+  mask-position: center;
+  mask-repeat: no-repeat;
+  mask-size: contain;
+  pointer-events: all;
+  cursor: pointer;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header fancy-tree-name {
+  display: block;
+  text-overflow: ellipsis;
+  align-self: stretch;
+  overflow: clip;
+  text-wrap: nowrap;
+  flex-shrink: 1;
+  cursor: pointer;
+  filter: drop-shadow(1px 1px rgba(0, 0, 0, 0.5));
+  min-width: 2em;
+  padding: 1px;
+  padding-left: 0;
+  align-content: center;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header fancy-tree-name:focus {
+  z-index: 100;
+  background-color: #222;
+  outline-offset: 1px;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header.feedback-drop-top::after {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 0;
+  right: 0;
+  top: 0;
+  border-top: solid 1px var(--selection);
+  z-index: 10;
+  pointer-events: none;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header.feedback-drop-bot::after {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border-top: solid 1px var(--selection);
+  z-index: 10;
+  pointer-events: none;
+}
+fancy-tree fancy-wrapper fancy-tree-item fancy-tree-header.feedback-drop-in::after {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+  border: solid 1px var(--selection);
+  z-index: 10;
+  pointer-events: none;
+}
+fancy-tree2 {
+  --anim-speed: 0.1s;
+  background-color: #222;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+fancy-tree2:focus {
+  outline: none;
+}
+fancy-tree2 fancy-scroll {
+  display: block;
+  width: 100%;
+  flex: 1 1;
+  min-width: 100px;
+  overflow-x: clip;
+  overflow-y: auto;
+}
+fancy-tree2 fancy-scroll fancy-item-container {
+  position: relative;
+  display: block;
+  overflow: hidden;
+  contain: layout paint size style;
+}
+fancy-tree2 fancy-scroll fancy-tree-item {
+  position: absolute;
+  --depth: 0;
+  --highlight: 0;
+  width: 100%;
+  padding-left: calc((var(--depth) - 1) * 1em + 0.25em);
+  box-sizing: border-box;
+  height: 20px;
+  max-height: 20px;
+  min-height: 20px;
+  --background: #222;
+  background-color: hsl(from var(--background) h s calc(var(--highlight) + l));
+  display: flex;
+}
+fancy-tree2 fancy-scroll fancy-tree-item.hide-search {
+  overflow: hidden;
+  height: 0px;
+  display: none;
+}
+fancy-tree2 fancy-scroll fancy-tree-item.open > .caret::before {
+  transform: rotate(90deg);
+}
+fancy-tree2 fancy-scroll fancy-tree-item:has(fancy-tree-children .selected) > fancy-tree-header fancy-tree-name {
+  text-decoration: underline var(--selection);
+}
+fancy-tree2 fancy-scroll fancy-tree-item.current {
+  outline: dashed 1px #AAA;
+  z-index: 10;
+  outline-offset: -1px;
+}
+fancy-tree2 fancy-scroll fancy-tree-item.selected {
+  color: white;
+  --background: var(--selection);
+}
+fancy-tree2 fancy-scroll fancy-tree-item:has(>:hover) {
+  --highlight: 10;
+  cursor: pointer;
+}
+fancy-tree2 fancy-scroll fancy-tree-item .hidden {
+  visibility: hidden;
+  pointer-events: none;
+}
+fancy-tree2 fancy-scroll fancy-tree-item fancy-tree-icon {
+  align-self: center;
+  width: 16px;
+  height: 16px;
+  display: block;
+  flex-shrink: 0;
+  filter: drop-shadow(1px 1px rgba(0, 0, 0, 0.5));
+}
+fancy-tree2 fancy-scroll fancy-tree-item fancy-tree-icon.caret {
+  width: 16px;
+  height: 16px;
+}
+fancy-tree2 fancy-scroll fancy-tree-item fancy-tree-icon.caret::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  margin: 2px;
+  width: 12px;
+  height: 12px;
+  transition: transform var(--anim-speed);
+  background-color: currentColor;
+  mask-image: url("res/icons/svg/right_caret.svg");
+  mask-position: center;
+  mask-repeat: no-repeat;
+  mask-size: contain;
+  pointer-events: all;
+}
+fancy-tree2 fancy-scroll fancy-tree-item fancy-tree-name {
+  display: block;
+  text-overflow: ellipsis;
+  align-self: stretch;
+  overflow: clip;
+  text-wrap: nowrap;
+  flex-shrink: 1;
+  filter: drop-shadow(1px 1px rgba(0, 0, 0, 0.5));
+  min-width: 2em;
+  padding: 1px;
+  padding-left: 0;
+  align-content: center;
+}
+fancy-tree2 fancy-scroll fancy-tree-item fancy-tree-name:focus {
+  z-index: 100;
+  background-color: #222;
+  outline-offset: 1px;
+}
+fancy-tree2 fancy-scroll fancy-tree-item fancy-tree-name > span.search-hl {
+  background-color: var(--selection);
+  color: white;
+}
+fancy-tree2 fancy-scroll fancy-tree-item.feedback-drop-top::after {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 0;
+  right: 0;
+  top: 0;
+  border-top: solid 1px var(--selection);
+  z-index: 10;
+  pointer-events: none;
+}
+fancy-tree2 fancy-scroll fancy-tree-item.feedback-drop-bot::after {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border-top: solid 1px var(--selection);
+  z-index: 10;
+  pointer-events: none;
+}
+fancy-tree2 fancy-scroll fancy-tree-item.feedback-drop-in::after {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+  border: solid 1px var(--selection);
+  z-index: 10;
+  pointer-events: none;
+}
+fancy-gallery {
+  display: flex;
+  flex-direction: column;
+  overflow: none;
+  flex-grow: 0;
+  flex-shrink: 1;
+  align-self: stretch;
+  min-width: 0;
+  min-height: 0;
+  width: 100%;
+}
+fancy-gallery,
+fancy-gallery * {
+  box-sizing: border-box;
+}
+fancy-gallery fancy-scroll {
+  flex: 1 1;
+  overflow-y: scroll;
+}
+fancy-gallery fancy-scroll fancy-item-container {
+  position: relative;
+  display: block;
+  overflow: hidden;
+  width: 100%;
+  min-height: 100%;
+  contain: layout paint size style;
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item {
+  padding: 4px;
+  position: absolute;
+  align-items: stretch;
+  overflow: hidden;
+  contain: layout paint size style;
+  background-color: #282828;
+  border-radius: 6px;
+  box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.4);
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item:hover {
+  background-color: var(--hover);
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item.details {
+  padding: 0;
+  display: flex;
+  align-items: center;
+  box-shadow: unset;
+  border-radius: unset;
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item fancy-thumbnail {
+  flex: 0 1;
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item .thumb {
+  transform: unset;
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item .loading {
+  animation: spinner 5s linear infinite;
+  transform: rotate(0deg);
+}
+@keyframes spinner {
+  to {
+    transform: rotate(360deg);
+  }
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item.details fancy-image {
+  height: 100%;
+  width: auto;
+  aspect-ratio: 1 / 1;
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item fancy-name {
+  position: absolute;
+  bottom: 0px;
+  left: 0px;
+  right: 0px;
+  height: 32px;
+  display: block;
+  text-align: center;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+fancy-gallery fancy-scroll fancy-item-container fancy-item.details fancy-name {
+  position: relative;
+  height: auto;
+  flex: 1 1;
+  text-align: left;
+}
+fancy-image {
+  display: block;
+  width: 100%;
+  aspect-ratio: 1 / 1;
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: cover;
+  transform: scale(0.75, 0.75);
+}

+ 633 - 2
bin/style.less

@@ -5110,13 +5110,16 @@ hide-popover {
 
 :root {
 	--hover-highlight: rgba(114, 180, 255, 0.5);
-	--selection: rgba(114, 180, 255);
+	--selection: #3185ce;
 	--hover: #444;
 
 	--basic-shadow: 2px 2px 3px rgba(0,0,0,.75);
 	--sublte-shadow: 1px 1px 3px rgba(0,0,0,.33);
 
 	--basic-border: 1px solid #353535;
+	--basic-border-hover: 1px solid #555;
+	--basic-border-focused: 1px solid var(--selection);
+
 	--basic-border-radius: 5px;
 	--basic-padding: 0.2em;
 
@@ -5316,6 +5319,16 @@ fancy-toolbar {
 	display: flex;
 	flex-direction: row;
 	align-items: center;
+
+	&.shadow {
+		position: relative;
+		background-color: #272727;
+		box-shadow: 2px 2px 2px rgba(0,0,0,0.2);
+		z-index: 999;
+	}
+
+	padding-left: 0.25em;
+	padding-right: 0.25em;
 }
 
 fancy-button {
@@ -5383,7 +5396,6 @@ fancy-button {
 		&:hover {
 			background: none;
 			color: var(--fancy-main-text-color);
-
 		}
 	}
 
@@ -5879,4 +5891,623 @@ blend-space-2d-root {
 
 .fancy-hide {
 	display: none;
+}
+
+file-browser {
+	display: flex;
+	flex-direction: row;
+	height: 100%;
+
+	.left {
+		width: 300px;
+		min-width: 100px;
+		background-color: mediumaquamarine;
+	}
+
+	.right {
+		flex: 1 1;
+		display: flex;
+		flex-direction: column;
+	}
+}
+
+fancy-flex-fill {
+	flex: 1 1;
+}
+
+fancy-icon {
+	aspect-ratio: 1/1;
+	height: 1em;
+	display: block;
+	mask-size: contain;
+	background-color: currentColor;
+	mask-mode: luminance;
+	mask-repeat: no-repeat;
+	aspect-ratio: 1/1;
+	mask-position: center;
+
+	&.small {
+		height: 12px;
+	}
+
+
+	&.big {
+		height: 22px;
+	}
+
+	&.fi-right-caret 		{ mask-image: url("res/icons/svg/right_caret.svg"); }
+	&.fi-close 				{ mask-image: url("res/icons/svg/close.svg"); }
+	&.fi-search 			{ mask-image: url("res/icons/svg/search.svg"); }
+}
+
+fancy-closable {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+
+	* {
+		box-sizing: border-box;
+	}
+
+	background-color: #333;
+	box-shadow: 1px 1px 1px rgba(0,0,0,0.33);
+	margin-bottom: 2px;
+
+	overflow: hidden;
+	height: 0px;
+}
+
+fancy-search {
+	position: relative;
+	flex: 1 1;
+	padding: 2px;
+	max-width: 400px;
+
+	&, * {
+		box-sizing: border-box;
+	}
+
+	> input {
+		border: var(--basic-border);
+		border-radius: var(--basic-border-radius);
+
+		&:hover {
+			border: var(--basic-border-hover);
+		}
+
+		&:focus {
+			border: var(--basic-border-focused);
+
+			outline: none;
+		}
+
+		min-width: 0;
+		width: 100%;
+		height: 100%;
+	}
+
+	.search-icon {
+		position: absolute;
+		right: 0.5em;
+		top: 50%;
+		transform: translateY(-50%);
+		color: #999;
+		z-index: 1;
+	}
+}
+
+fancy-tree {
+	--anim-speed: 0.1s;
+
+	background-color: #222;
+
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+
+	&:focus {
+		outline: none;
+	}
+
+	fancy-wrapper {
+		display: block;
+		width: 100%;
+		flex: 1 1;
+		min-width: 100px;
+		overflow-x: clip;
+		overflow-y: auto;
+
+		fancy-tree-item {
+			--depth: 0;
+
+			display: flex;
+
+			flex-direction: column;
+
+			&.hide-search {
+				overflow: hidden;
+				height: 0px;
+				display: none;
+			}
+
+			fancy-tree-children {
+				height: 0px;
+				overflow: hidden;
+				display: none;
+			}
+
+			&.open > fancy-tree-children {
+				display: block;
+				height: auto;
+
+				&:has(fancy-tree-name:focus) {
+					overflow: visible;
+				}
+			}
+
+			&.open>fancy-tree-header .caret::before {
+				transform: rotate(90deg);
+			}
+
+			// Select if we have a children that is selected
+			&:has(fancy-tree-children .selected) > fancy-tree-header {
+				fancy-tree-name {
+					text-decoration: underline var(--selection);
+				}
+			}
+
+			--highlight: 0;
+			&.current:not(.selected) {
+				--highlight: 10;
+			}
+
+			&.selected > fancy-tree-header {
+				color: white;
+				--background: var(--selection);
+			}
+
+			box-sizing: border-box;
+
+
+			fancy-tree-header {
+				padding-left: calc((var(--depth) - 1) * 1.0em + 0.25em);
+				position: relative;
+				box-sizing: border-box;
+
+				height: 20px;
+				max-height: 20px;
+				min-height: 20px;
+
+				--background: #222;
+				background-color: e("hsl(from var(--background) h s calc(var(--highlight) + l))");
+
+
+				display: flex;
+
+				position: relative;
+
+				&:has(>.caret:hover, >fancy-tree-name:hover) {
+					--highlight: 10;
+				}
+
+				fancy-tree-icon {
+					align-self: center;
+					width: 16px;
+					height: 16px;
+
+					display: block;
+
+					flex-shrink: 0;
+
+					filter: drop-shadow(1px 1px rgba(0,0,0,0.5));
+
+					&.caret {
+						width: 16px;
+						height: 16px;
+						&::before {
+							content: "";
+							position: absolute;
+							top: 0; left: 0;
+							margin: 2px;
+							width: 12px; height: 12px;
+
+							transition: transform var(--anim-speed);
+							background-color: currentColor;
+							mask-image: url("res/icons/svg/right_caret.svg");
+
+							mask-position: center;
+							mask-repeat: no-repeat;
+							mask-size: contain;
+
+							pointer-events: all;
+							cursor: pointer;
+						}
+					}
+				}
+
+				fancy-tree-name {
+					display: block;
+					text-overflow: ellipsis;
+					align-self: stretch;
+					overflow: clip;
+					text-wrap: nowrap;
+					flex-shrink: 1;
+					cursor: pointer;
+					filter: drop-shadow(1px 1px rgba(0,0,0,0.5));
+					min-width: 2em;
+					padding: 1px;
+					padding-left: 0;
+					align-content: center;
+
+					&:focus {
+						z-index: 100;
+						background-color: #222;
+						outline-offset: 1px;
+					}
+
+				}
+
+				&.feedback-drop-top::after {
+					position: absolute;
+					content: "";
+					display: block;
+					left: 0;
+					right: 0;
+					top: 0;
+					border-top: solid 1px var(--selection);
+					z-index: 10;
+					pointer-events: none;
+				}
+
+				&.feedback-drop-bot::after {
+					position: absolute;
+					content: "";
+					display: block;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-top: solid 1px var(--selection);
+					z-index: 10;
+					pointer-events: none;
+				}
+
+				&.feedback-drop-in::after {
+					position: absolute;
+					content: "";
+					display: block;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					top: 0;
+					border: solid 1px var(--selection);
+					z-index: 10;
+					pointer-events: none;
+				}
+			}
+
+
+
+		}
+	}
+
+
+}
+
+fancy-tree2 {
+	--anim-speed: 0.1s;
+
+	background-color: #222;
+
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+
+	&:focus {
+		outline: none;
+	}
+
+	fancy-scroll {
+		display: block;
+		width: 100%;
+		flex: 1 1;
+		min-width: 100px;
+		overflow-x: clip;
+		overflow-y: auto;
+
+		fancy-item-container {
+			position: relative;
+			display: block;
+			overflow: hidden;
+
+			contain: layout paint size style;
+		}
+
+		fancy-tree-item {
+			position: absolute;
+			--depth: 0;
+			--highlight: 0;
+			width: 100%;
+
+			display: flex;
+
+			&.hide-search {
+				overflow: hidden;
+				height: 0px;
+				display: none;
+			}
+
+			&.open> .caret::before {
+				transform: rotate(90deg);
+			}
+
+			// Select if we have a children that is selected
+			&:has(fancy-tree-children .selected) > fancy-tree-header {
+				fancy-tree-name {
+					text-decoration: underline var(--selection);
+				}
+			}
+
+			// &.current:not(.selected) {
+			// 	//--highlight: 10;
+			// }
+
+			&.current {
+				outline: dashed 1px #AAA;
+				z-index: 10;
+				outline-offset: -1px;
+			}
+
+			&.selected {
+				color: white;
+				--background: var(--selection);
+			}
+
+
+
+			box-sizing: border-box;
+
+			padding-left: calc((var(--depth) - 1) * 1.0em + 0.25em);
+			box-sizing: border-box;
+
+			height: 20px;
+			max-height: 20px;
+			min-height: 20px;
+
+			--background: #222;
+			background-color: e("hsl(from var(--background) h s calc(var(--highlight) + l))");
+
+			display: flex;
+
+			&:has(>:hover) {
+				--highlight: 10;
+				cursor: pointer;
+			}
+
+			.hidden {
+				visibility: hidden;
+				pointer-events: none;
+			}
+
+			fancy-tree-icon {
+				align-self: center;
+				width: 16px;
+				height: 16px;
+
+				display: block;
+
+				flex-shrink: 0;
+
+				filter: drop-shadow(1px 1px rgba(0,0,0,0.5));
+
+				&.caret {
+					width: 16px;
+					height: 16px;
+					&::before {
+						content: "";
+						position: absolute;
+						top: 0; left: 0;
+						margin: 2px;
+						width: 12px; height: 12px;
+
+						transition: transform var(--anim-speed);
+						background-color: currentColor;
+						mask-image: url("res/icons/svg/right_caret.svg");
+
+						mask-position: center;
+						mask-repeat: no-repeat;
+						mask-size: contain;
+
+						pointer-events: all;
+					}
+				}
+			}
+
+			fancy-tree-name {
+				display: block;
+				text-overflow: ellipsis;
+				align-self: stretch;
+				overflow: clip;
+				text-wrap: nowrap;
+				flex-shrink: 1;
+				filter: drop-shadow(1px 1px rgba(0,0,0,0.5));
+				min-width: 2em;
+				padding: 1px;
+				padding-left: 0;
+				align-content: center;
+
+				&:focus {
+					z-index: 100;
+					background-color: #222;
+					outline-offset: 1px;
+				}
+
+				>span.search-hl {
+					background-color: var(--selection);
+					color: white;
+				}
+
+			}
+
+			&.feedback-drop-top::after {
+				position: absolute;
+				content: "";
+				display: block;
+				left: 0;
+				right: 0;
+				top: 0;
+				border-top: solid 1px var(--selection);
+				z-index: 10;
+				pointer-events: none;
+			}
+
+			&.feedback-drop-bot::after {
+				position: absolute;
+				content: "";
+				display: block;
+				left: 0;
+				right: 0;
+				bottom: 0;
+				border-top: solid 1px var(--selection);
+				z-index: 10;
+				pointer-events: none;
+			}
+
+			&.feedback-drop-in::after {
+				position: absolute;
+				content: "";
+				display: block;
+				left: 0;
+				right: 0;
+				bottom: 0;
+				top: 0;
+				border: solid 1px var(--selection);
+				z-index: 10;
+				pointer-events: none;
+			}
+
+
+
+		}
+	}
+}
+
+fancy-gallery {
+	display: flex;
+	flex-direction: column;
+	overflow: none;
+
+	flex-grow: 0;
+	flex-shrink: 1;
+	align-self: stretch;
+
+	min-width: 0;
+	min-height: 0;
+
+	width: 100%;
+
+	&,* {
+		box-sizing: border-box;
+	}
+
+	fancy-scroll {
+		flex: 1 1;
+		overflow-y: scroll;
+
+		fancy-item-container {
+			position: relative;
+			display: block;
+			overflow: hidden;
+
+			width: 100%;
+			min-height: 100%;
+
+			contain: layout paint size style;
+
+			fancy-item {
+				padding: 4px;
+				position: absolute;
+				align-items: stretch;
+				overflow: hidden;
+
+				contain: layout paint size style;
+
+				background-color: #282828;
+				border-radius: 6px;
+
+				&:hover {
+					background-color: var(--hover);
+				}
+
+				box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.4);
+
+				&.details {
+					padding: 0;
+					display: flex;
+					align-items: center;
+
+					box-shadow: unset;
+					border-radius: unset;
+				}
+
+				fancy-thumbnail {
+					flex: 0 1;
+					display: block;
+					width: 100%;
+					height: 100%;
+				}
+
+				.thumb {
+					transform: unset;
+
+				}
+
+				.loading {
+					animation: spinner 5.0s linear infinite;
+					transform: rotate(0deg);
+					@keyframes spinner {
+						to { transform: rotate(360deg); }
+					}
+				}
+
+				&.details fancy-image {
+					height: 100%;
+					width: auto;
+					aspect-ratio: 1 / 1;
+				}
+
+				fancy-name {
+					position: absolute;
+					bottom: 0px;
+					left: 0px;
+					right: 0px;
+					height: 32px;
+					display: block;
+					text-align: center;
+					//text-wrap: wrap;
+					//word-break: break-all;
+					text-overflow: ellipsis;
+					overflow: hidden;
+
+				}
+
+				&.details fancy-name {
+					position: relative;
+					height: auto;
+					flex: 1 1;
+					text-align: left;
+
+				}
+			}
+		}
+	}
+}
+
+fancy-image {
+	display: block;
+	width: 100%;
+	aspect-ratio: 1 / 1;
+	background-position: center;
+	background-repeat: no-repeat;
+	background-size: cover;
+	transform: scale(0.75,0.75);
 }

+ 1 - 0
common.hxml

@@ -5,6 +5,7 @@
 -lib hx3compat
 -lib domkit
 -lib hashlink
+-lib format
 -D js-classic
 -D js-unflatten
 -D hscriptPos

+ 152 - 60
hide/Ide.hx

@@ -28,7 +28,6 @@ class Ide extends hide.tools.IdeData {
 	var layout : golden.Layout;
 
 	var currentLayout : { name : String, state : Config.LayoutState };
-	var defaultLayout : { name : String, state : Config.LayoutState };
 	var currentFullScreen(default,set) : hide.ui.View<Dynamic>;
 	var maximized : Bool;
 	var fullscreen : Bool;
@@ -40,6 +39,7 @@ class Ide extends hide.tools.IdeData {
 	var subView : { component : String, state : Dynamic, events : {} };
 	var scripts : Map<String,Array<Void->Void>> = new Map();
 	var hasReloaded = false;
+	public var thumbnailMode : Bool = false;
 
 	var hideRoot : hide.Element;
 	var statusBar : hide.Element;
@@ -60,8 +60,14 @@ class Ide extends hide.tools.IdeData {
 		initPad();
 		isCDB = Sys.getEnv("HIDE_START_CDB") == "1" || nw.App.manifest.name == "CDB";
 		isDebugger = Sys.getEnv("HIDE_DEBUG") == "1";
+
+		var thumb = StringTools.contains(js.Browser.window.location.href, "thumbnail");
+		if (thumb) {
+			thumbnailMode = true;
+		}
+
 		function wait() {
-			if( monaco.ScriptEditor == null ) {
+			if( monaco.ScriptEditor == null && !thumbnailMode ) {
 				haxe.Timer.delay(wait, 10);
 				return;
 			}
@@ -83,6 +89,10 @@ class Ide extends hide.tools.IdeData {
 		hxd.Pad.wait((p) -> gamePad = p);
 	}
 
+	function thumbnailInit() {
+		var generator = @:privateAccess new hide.tools.ThumbnailGenerator();
+	}
+
 	function startup() {
 		inst = this;
 		window = nw.Window.get();
@@ -130,7 +140,9 @@ class Ide extends hide.tools.IdeData {
 				}
 			}
 		}
-		window.show(true);
+
+		if (!thumbnailMode)
+			window.show(true);
 
 		if( config.global.get("hide") == null )
 			error("Failed to load defaultProps.json");
@@ -142,6 +154,13 @@ class Ide extends hide.tools.IdeData {
 
 		setProject(current);
 		loadProject();
+
+		if (thumbnailMode) {
+			thumbnailInit();
+			hxd.System.setLoop(mainLoop);
+			return;
+		}
+
 		window.window.document.addEventListener("mousedown", function(e) {
 			mouseX = e.x;
 			mouseY = e.y;
@@ -162,14 +181,17 @@ class Ide extends hide.tools.IdeData {
 		});
 		window.on('move', function() haxe.Timer.delay(onWindowChange,100));
 		window.on('resize', function() haxe.Timer.delay(onWindowChange,100));
-		window.on('close', function() {
-			if( hasReloaded ) return;
-			if( !isDebugger )
-				for( v in views )
-					if( !v.onBeforeClose() )
-						return;
-			window.close(true);
-		});
+		if (!thumbnailMode) {
+			window.on('close', function() {
+				if( hasReloaded ) return;
+				if( !isDebugger )
+					for( v in views )
+						if( !v.onBeforeClose() )
+							return;
+				window.close(true);
+			});
+		}
+
 		window.on("blur", function() { if( h3d.Engine.getCurrent() != null && !hasReloaded ) hxd.Key.initialize(); });
 
 		// handle commandline parameters
@@ -214,7 +236,6 @@ class Ide extends hide.tools.IdeData {
 		}
 		body.ondragover = function(e:js.html.DragEvent) {
 			dragFunc(false, e);
-			return false;
 		};
 		body.ondrop = function(e:js.html.DragEvent) {
 			if(!dragFunc(true, e)) {
@@ -222,7 +243,6 @@ class Ide extends hide.tools.IdeData {
 					openFile(Reflect.field(f,"path"));
 				e.preventDefault();
 			}
-			return false;
 		}
 
 		if( subView != null ) body.className +=" hide-subview";
@@ -336,6 +356,61 @@ class Ide extends hide.tools.IdeData {
 			v.onResize();
 	}
 
+	function getOrInitTarget(position: hide.ui.View.DisplayPosition) : golden.ContentItem {
+		if (layout.root == null)
+			return null;
+		var target = layout.root.getItemsById(position)[0];
+		if (target != null)
+			return target;
+
+		var parent : golden.ContentItem = null;
+		var config : golden.Config.ItemConfig;
+		var index : Int = null;
+		var rootRow = layout.root.contentItems[0];
+		switch(position) {
+			case Left:
+				config = {
+					type: Stack,
+				};
+				parent = rootRow;
+				index = 0;
+			case Center:
+				config = {
+					type: Stack,
+					isClosable: false,
+					width: 1500,
+					height: 800,
+				};
+				parent = getOrInitTarget(MiddleColumnInternal);
+				index = 0;
+			case Bottom:
+				config = {
+					type: Stack,
+				};
+				parent = getOrInitTarget(MiddleColumnInternal);
+				index = parent.contentItems.length;
+			case Right:
+				config = {
+					type: Stack,
+				};
+				parent = rootRow;
+				index = parent.contentItems.length;
+			case MiddleColumnInternal:
+				config = {
+					type: Column,
+					isClosable: false,
+				}
+				parent = rootRow;
+				index = hxd.Math.iclamp(1, 0, parent.contentItems.length);
+		}
+
+		config.id = position;
+		parent.addChild(config, index);
+		var target = layout.root.getItemsById(position)[0];
+
+		return target;
+	}
+
 	function initLayout( ?state : { name : String, state : Config.LayoutState } ) {
 		initializing = true;
 
@@ -344,30 +419,37 @@ class Ide extends hide.tools.IdeData {
 			layout = null;
 		}
 
-		defaultLayout = null;
-		var layoutName = isCDB ? "CDB" : "Default";
 		var emptyLayout : Config.LayoutState = { content : [], fullScreen : null };
-		for( p in projectConfig.layouts )
-			if( p.name == layoutName ) {
-				if( p.state.content == null ) continue; // old version
-				defaultLayout = p;
-				break;
+
+		if( state == null ) {
+			var emptyLayout : Config.LayoutState = {
+				content: [{type: golden.Config.ItemType.Row, isClosable: false, id: "content_root"}], fullScreen : null,
+			};
+
+
+			var layoutName = isCDB ? "CDB" : "Default";
+			for( i => p in projectConfig.layouts.copy() ) {
+				if( p.name == layoutName ) {
+					if( p.state.content == null || (p.state.content:Array<Dynamic>)[0]?.id != "content_root") {
+						projectConfig.layouts.splice(i, 1);
+						continue;
+					};
+					state = p;
+				}
+			}
+
+			if( state == null ) {
+				state = { name : layoutName, state : emptyLayout };
+				projectConfig.layouts.push(state);
 			}
-		if( defaultLayout == null ) {
-			defaultLayout = { name : layoutName, state : emptyLayout };
-			projectConfig.layouts.push(defaultLayout);
-			config.current.sync();
-			config.user.save();
 		}
-		if( state == null )
-			state = defaultLayout;
 
 		if( subView != null )
 			state = { name : "SubView", state : emptyLayout };
 
-		this.currentLayout = state;
+		currentLayout = state;
 
-		var config : golden.Config = {
+		var goldenConfig : golden.Config = {
 			content: state.state.content,
 			settings: {
 				// Default to false
@@ -377,17 +459,18 @@ class Ide extends hide.tools.IdeData {
 				showMaximiseIcon : config.user.get('layout.showMaximiseIcon') == true
 			}
 		};
+
 		var comps = new Map();
 		for( vcl in hide.ui.View.viewClasses )
 			comps.set(vcl.name, true);
 		function checkRec(i:golden.Config.ItemConfig) {
-			if( i.componentName != null && !comps.exists(i.componentName) ) {
+			if( i.componentName != null && i.componentState != null && !comps.exists(i.componentName) ) {
 				i.componentState.deletedComponent = i.componentName;
 				i.componentName = "hide.view.Unknown";
 			}
 			if( i.content != null ) for( i in i.content ) checkRec(i);
 		}
-		for( i in config.content ) checkRec(i);
+		for( i in goldenConfig.content ) checkRec(i);
 
 		if (hideRoot != null) {
 			hideRoot.remove();
@@ -405,7 +488,10 @@ class Ide extends hide.tools.IdeData {
 			new Element('<span class="build">hide $commitHash</span>').appendTo(statusBar);
 		}
 
-		layout = new golden.Layout(config, goldenContainer.get(0));
+		layout = new golden.Layout(goldenConfig, goldenContainer.get(0));
+
+
+
 		var resizeTimer : haxe.Timer = null;
 		var observer = new hide.comp.ResizeObserver((elts, observer) -> {
 			if (resizeTimer != null) {
@@ -439,6 +525,8 @@ class Ide extends hide.tools.IdeData {
 		layout.init();
 		layout.on('stateChanged', onLayoutChanged);
 
+		getOrInitTarget(Center);
+
 		var waitCount = 0;
 		function waitInit() {
 			waitCount++;
@@ -528,7 +616,7 @@ class Ide extends hide.tools.IdeData {
 	function onLayoutChanged() {
 		if( initializing || !ideConfig.autoSaveLayout || isCDB )
 			return;
-		defaultLayout.state = saveLayout();
+		currentLayout.state = saveLayout();
 		if( subView == null ) this.config.user.save();
 	}
 
@@ -711,6 +799,9 @@ class Ide extends hide.tools.IdeData {
 			}
 			h3d.mat.MaterialSetup.current = render;
 
+			if (thumbnailMode) {
+				return;
+			}
 			initMenu();
 			initLayout();
 		});
@@ -798,6 +889,7 @@ class Ide extends hide.tools.IdeData {
 	public function reload() {
 		hasReloaded = true;
 		fileWatcher.dispose();
+		hide.tools.FileManager.onBeforeReload();
 		hide.view.RemoteConsoleView.onBeforeReload();
 		js.Browser.location.reload();
 	}
@@ -1523,11 +1615,29 @@ class Ide extends hide.tools.IdeData {
 		if( subView != null ) Reflect.callMethod(subView.events,Reflect.field(subView.events,name),[param]);
 	}
 
+	public function getOrOpenInspector() {
+		var inspector = layout.root.getItemsById("inspector")[0];
+		if (inspector != null)
+			return;
+
+		open("hide.view.Inspector", {});
+		return;
+	}
+
+	public function closeInspector() {
+		var inspector = layout.root.getItemsById("inspector")[0];
+		if (inspector != null) {
+			inspector.remove();
+		}
+	}
+
 	public function open( component : String, state : Dynamic, ?onCreate : hide.ui.View<Dynamic> -> Void, ?onOpen : hide.ui.View<Dynamic> -> Void ) {
+		if (layout.root == null)
+			return;
 		if( state == null ) state = {};
 
-		var c = hide.ui.View.viewClasses.get(component);
-		if( c == null )
+		var viewConfig = hide.ui.View.viewClasses.get(component);
+		if( viewConfig == null )
 			throw "Unknown component " + component;
 
 		state.componentName = component;
@@ -1540,27 +1650,10 @@ class Ide extends hide.tools.IdeData {
 			}
 		}
 
-		var options = c.options;
+		var options = viewConfig.options;
 
-		var bestTarget : golden.Container = null;
-		for( v in views )
-			if( v.defaultOptions.position == options.position ) {
-				if( bestTarget == null || bestTarget.width * bestTarget.height < v.container.width * v.container.height )
-					bestTarget = v.container;
-			}
+		var target = getOrInitTarget(options.position ?? Center);
 
-		var index : Null<Int> = null;
-		var target;
-		if( bestTarget != null )
-			target = bestTarget.parent.parent;
-		else {
-			target = layout.root.contentItems[0];
-			if( target == null ) {
-				layout.root.addChild({ type : Row, isClosable: false });
-				target = layout.root.contentItems[0];
-			}
-			target.config.isClosable = false;
-		}
 		var needResize = options.width != null;
 		target.on("componentCreated", function(c) {
 			target.off("componentCreated");
@@ -1584,15 +1677,14 @@ class Ide extends hide.tools.IdeData {
 		var config : golden.Config.ItemConfig = {
 			type : Component,
 			componentName : component,
-			componentState : state
+			componentState : state,
 		};
 
-		if( options.position == Left ) index = 0;
+		if (options.id != null) {
+			config.id = options.id;
+		}
 
-		if( index == null )
-			target.addChild(config);
-		else
-			target.addChild(config, index);
+		target.addChild(config);
 	}
 
 	public function reopenLastClosedTab() {

+ 12 - 1
hide/comp/ContentEditable.hx

@@ -5,6 +5,7 @@ class ContentEditable extends Component {
 	public var spellcheck(never, set) : Bool;
 
 	var html : js.html.Element = null;
+	var initialValue: String;
 
 	public function new(?parent : Element, ?element : Element) {
 		if (element == null) {
@@ -22,11 +23,16 @@ class ContentEditable extends Component {
 			var sel = js.Browser.window.getSelection();
 			sel.removeAllRanges();
 			sel.addRange(range);
+			initialValue = value;
 		}
 		html.onkeydown = function(e: js.html.KeyboardEvent) {
 			if (e.keyCode == 13) {
 				html.blur();
 			}
+			if (e.key == "Escape") {
+				value = initialValue;
+				html.blur();
+			}
 			e.stopPropagation();
 		}
 		html.oninput = function(e) {
@@ -49,10 +55,13 @@ class ContentEditable extends Component {
 
 		html.onblur = function() {
 			if (js.Browser.window.getSelection != null) {js.Browser.window.getSelection().removeAllRanges();}
-			if (wasEdited) {
+			if (get_value() != initialValue) {
 				onChange(get_value());
 				wasEdited = false;
 			}
+			else {
+				onCancel();
+			}
 		}
 	}
 
@@ -69,5 +78,7 @@ class ContentEditable extends Component {
 		return html.innerText;
 	}
 
+	public dynamic function onCancel() {};
+
 	public dynamic function onChange(v: String) {};
 }

+ 47 - 0
hide/comp/FancyClosable.hx

@@ -0,0 +1,47 @@
+package hide.comp;
+
+/**
+	A container that is hidden by default and that can appear scrolling down from the top,
+	like for a search bar
+**/
+class FancyClosable extends hide.comp.Component {
+	var open = false;
+
+	public override function new(parent: Element = null, target: Element = null) {
+		var el = new hide.Element('
+			<fancy-closable>
+				<fancy-button class="quieter close-btn"><fancy-icon class="medium fi-close"></fancy-icon></fancy-button>
+			</fancy-closable>
+		');
+		if (target != null) {
+			var children = target.children();
+			target.replaceWith(el);
+			children.insertBefore(el.children().first());
+		}
+		super(parent, el);
+
+		var close = element.get(0).querySelector(".close-btn");
+		close.onclick = (e) -> toggleOpen(false);
+	}
+
+	public dynamic function onOpen() : Void {};
+	public dynamic function onClose() : Void {};
+
+	public function toggleOpen(?force: Bool) : Void {
+		var want = force != null ? force : !open;
+		if (open != want) {
+			open = want;
+			FancyTree.animateReveal(element.get(0), open);
+		}
+
+		if (open) {
+			onOpen();
+		} else {
+			onClose();
+		}
+	}
+
+	public function isOpen() : Bool {
+		return open;
+	}
+}

+ 303 - 0
hide/comp/FancyGallery.hx

@@ -0,0 +1,303 @@
+package hide.comp;
+
+enum GalleryRefreshFlag {
+	Search;
+	Items;
+	RegenHeader;
+}
+
+typedef GalleryRefreshFlags = haxe.EnumFlags<GalleryRefreshFlag>;
+
+typedef GalleryItemData<GalleryItem> = {item: GalleryItem, name: String, element: js.html.Element, iconStringCache: String};
+
+class FancyGallery<GalleryItem> extends hide.comp.Component {
+
+	var currentData : Array<GalleryItemData<GalleryItem>> = [];
+	var itemMap : Map<{}, GalleryItemData<GalleryItem>> = [];
+	var itemContainer : js.html.Element;
+	var scroll : js.html.Element;
+
+	var lastHeight : Float = 0;
+
+	var itemHeightPx = 128;
+	var itemWidthPx = 128;
+	static final itemTitleHeight = 32;
+
+	var details = false;
+
+	public function new(parent: Element, el: Element) {
+		if (el != null) {
+			if (el.get(0).tagName != "FANCY-GALLERY") {
+				throw "el must be a fancy-gallery node";
+			}
+		}
+		else {
+			el = new Element('<fancy-gallery></fancy-gallery>');
+		}
+
+		super(parent, el);
+		element.html("
+			<fancy-scroll>
+				<fancy-item-container>
+				</fancy-item-container>
+			</fancy-scroll>
+		");
+
+		var resizeObserver = new hide.comp.ResizeObserver((_, _) -> {
+			queueRefresh();
+		});
+		resizeObserver.observe(element.get(0));
+
+		itemContainer = el.find("fancy-item-container").get(0);
+
+		itemContainer.onwheel = (e: js.html.WheelEvent) -> {
+			if (e.ctrlKey) {
+				e.preventDefault();
+				e.stopPropagation();
+
+
+				if (e.deltaY < 0) {
+					if (details) {
+						itemWidthPx = 32;
+						details = false;
+					} else {
+						itemWidthPx = hxd.Math.floor(itemWidthPx * 1.25);
+					}
+				} else if (e.deltaY > 0) {
+					itemWidthPx = hxd.Math.floor(itemWidthPx / 1.25);
+				}
+
+				if (itemWidthPx < 32) {
+					details = true;
+				}
+				queueRefresh();
+			}
+		}
+		scroll = el.find("fancy-scroll").get(0);
+
+		scroll.onscroll = (e) -> queueRefresh();
+	}
+
+	var refreshQueued : Bool = false;
+	var currentRefreshFlags : GalleryRefreshFlags = GalleryRefreshFlags.ofInt(0);
+
+	public function queueRefresh(flag: GalleryRefreshFlag = null) {
+		if (flag != null) {
+			currentRefreshFlags.set(flag);
+		}
+		if (!refreshQueued) {
+			refreshQueued = true;
+			js.Browser.window.requestAnimationFrame((_) -> onRefreshInternal());
+		}
+	}
+
+	public dynamic function getItems() : Array<GalleryItem> {
+		return [];
+	}
+
+	public dynamic function getName(item: GalleryItem) : String {
+		return "";
+	}
+
+	public dynamic function getIcon(item: GalleryItem) : String {
+		return null;
+	}
+
+	public dynamic function onDoubleClick(item: GalleryItem) : Void {
+	}
+
+	/**
+		Called when an item becomes visible on screen due to scrolling or other things.
+	**/
+	public dynamic function visibilityChanged(item: GalleryItem, isVisible: Bool) : Void {
+
+	}
+
+	/**
+		Drag and drop interface.
+		Set this struct with all of it's function callback to handle drag and drop inside your tree.
+	**/
+	public var dragAndDropInterface :
+	{
+		/**
+			Called when the user starts a drag and drop operation on `item`.
+			Fill dataTransfer with the information you want to transfer, you can use getSelectedItems to handle dragging more than
+			one item at a time.
+			Return `true` if the drag operation is allowed, and `false` to cancel it
+		**/
+		onDragStart: (item: GalleryItem, dataTransfer: js.html.DataTransfer) -> Bool,
+
+		// /**
+		// 	Called when the user hovers on `target` with a drag and drop operation. You need to return what drop orperation is allowed
+		// 	on the given object
+		// **/
+		// getItemDropFlags: (target: TreeItem, dataTransfer: js.html.DataTransfer) -> DropFlags,
+
+		// /**
+		// 	Called when the user drops an item on `target` and getItemDropFlags returned at least one valid flag.
+		// 	`where` tells you where the item was dropped, and you can use `dataTransfer` to know what was dropped
+		// **/
+		// onDrop: (target: TreeItem, where: DropOperation, dataTransfer: js.html.DataTransfer) -> Void
+	} = null;
+
+
+
+	public function rebuild() {
+		queueRefresh(Items);
+		queueRefresh(Search);
+		queueRefresh(RegenHeader);
+	}
+
+	/**
+		Never call this directly
+	**/
+	function onRefreshInternal() {
+		refreshQueued = false;
+
+		if (currentRefreshFlags.has(Items)) {
+			rebuildItems();
+		}
+
+		var oldChildren = [for (node in itemContainer.childNodes) node];
+
+		var margin = 8;
+
+		var bounds = scroll.getBoundingClientRect();
+
+
+		if (details) {
+			margin = 0;
+			itemHeightPx = 16;
+			itemWidthPx = hxd.Math.floor(bounds.width);
+		} else {
+			itemHeightPx = itemWidthPx + itemTitleHeight;
+		}
+
+
+		var numData = currentData.length;
+
+
+		var itemsPerRow = hxd.Math.imax(hxd.Math.floor((bounds.width - margin) / (itemWidthPx + margin)), 1);
+
+		var height = hxd.Math.ceil(numData / itemsPerRow) * (itemHeightPx + margin) + margin;
+		if (height != lastHeight) {
+			itemContainer.style.height = '${height}px';
+			lastHeight = height;
+		}
+
+		// We might need to recompute the scroll height so we call getBoundingClientRect again
+		var scrollHeight = scroll.getBoundingClientRect().height;
+
+
+
+		var clipStart = scroll.scrollTop;
+		var clipEnd = scrollHeight + clipStart;
+		var itemStart = hxd.Math.floor((clipStart-margin) / (itemHeightPx+margin)) * itemsPerRow;
+		var itemEnd = hxd.Math.ceil((clipEnd-margin) / (itemHeightPx+margin)) * itemsPerRow;
+
+
+		for (index in hxd.Math.imax(itemStart, 0) ... hxd.Math.imin(currentData.length, itemEnd)) {
+			var data = currentData[index];
+			var element = getElement(data);
+
+			element.style.left = '${((index % itemsPerRow)) * (itemWidthPx + margin) + margin}px';
+			element.style.top = '${hxd.Math.floor(index / itemsPerRow) * (itemHeightPx + margin) + margin}px';
+
+			if (!oldChildren.remove(element)) {
+				itemContainer.appendChild(element);
+				visibilityChanged(data.item, true);
+			}
+		}
+
+		for (oldChild in oldChildren) {
+			if (itemContainer.contains(oldChild)) {
+				itemContainer.removeChild(oldChild);
+				var data : GalleryItemData<GalleryItem> = untyped oldChild.__data;
+				if (data != null) {
+					visibilityChanged(data.item, false);
+				}
+			}
+		}
+
+		currentRefreshFlags = GalleryRefreshFlags.ofInt(0);
+	}
+
+	function setupDragAndDrop(data : GalleryItemData<GalleryItem>) {
+		if (dragAndDropInterface == null)
+			return;
+
+		data.element.draggable = true;
+		data.element.ondragstart = (e:js.html.DragEvent) -> {
+			if (dragAndDropInterface.onDragStart(data.item, e.dataTransfer)) {
+				e.dataTransfer.effectAllowed = "move";
+				e.dataTransfer.setDragImage(data.element, 0,0);
+			} else {
+				e.preventDefault();
+			}
+		}
+	}
+
+	function getElement(data : GalleryItemData<GalleryItem>) : js.html.Element {
+		if (currentRefreshFlags.has(RegenHeader) && data.element != null) {
+			data.element.remove();
+			data.element = null;
+			data.iconStringCache = null;
+		}
+
+		if (data.element == null) {
+			data.element = js.Browser.document.createElement("fancy-item");
+			untyped data.element.__data = data;
+
+			data.element.innerHTML = '
+				<fancy-thumbnail></fancy-thumbnail>
+				<fancy-name><fancy-name>
+			';
+
+			data.element.ondblclick = (e) -> {
+				onDoubleClick(data.item);
+			}
+
+			setupDragAndDrop(data);
+		}
+
+		if (!details) {
+			data.element.style.width = '${itemWidthPx}px';
+			data.element.style.height = '${itemHeightPx}px';
+		} else {
+			data.element.style.width = '100%';
+		}
+
+		data.element.style.height = '${itemHeightPx}px';
+		data.element.classList.toggle("details", details);
+
+		var name = data.element.querySelector("fancy-name");
+		if (name.title != data.name) {
+			name.innerHTML = '<span class="bg">${data.name}</span>';
+			name.title = data.name;
+		}
+
+		var img = data.element.querySelector("fancy-thumbnail");
+		var imgString = getIcon(data.item) ?? '<fancy-image style="background-image:url(\'res/icons/svg/unknown_file.svg\')"></fancy-image>';
+		if (imgString != data.iconStringCache) {
+			img.innerHTML = imgString;
+			data.iconStringCache = imgString;
+		}
+
+		return data.element;
+	}
+
+	function rebuildItems() {
+		currentData.resize(0);
+		var items = getItems();
+		for (item in items) {
+			var data = hrt.tools.MapUtils.getOrPut(itemMap, cast item, {
+				item: item,
+				name: getName(item),
+				element: null,
+				iconStringCache: null,
+			});
+
+			currentData.push(data);
+		}
+	}
+}

+ 51 - 0
hide/comp/FancySearch.hx

@@ -0,0 +1,51 @@
+package hide.comp;
+
+typedef SearchRanges = Array<Int>;
+class FancySearch extends hide.comp.Component {
+	var open = false;
+	var input : js.html.InputElement;
+
+	public override function new(parent: Element = null, target: Element = null) {
+		var el = new hide.Element('
+			<fancy-search>
+				<input type="text" class="search-field">
+				<fancy-icon class="search-icon fi-search"></fancy-icon>
+			</fancy-search>
+		');
+		if (target != null) {
+			target.replaceWith(el);
+		}
+		super(parent, el);
+
+		input = cast element.get(0).querySelector(".search-field");
+
+		input.oninput = (e) -> {
+			onSearch(e.target.value, false);
+		}
+
+		input.onchange = (e) -> {
+			onSearch(e.target.value, true);
+		}
+	}
+
+	public dynamic function onSearch(search: String, enter: Bool) : Void {};
+
+	public function hasFocus() : Bool {
+		return js.Browser.document.activeElement ==  input;
+	}
+
+	public function blur() : Void {
+		input.blur();
+	}
+
+	public function focus() : Void {
+		input.focus();
+	}
+
+	public static function computeSearchRanges(haystack: String, needle: String) : SearchRanges {
+		var pos = haystack.toLowerCase().indexOf(needle);
+		if (pos < 0)
+			return null;
+		return [pos, pos + needle.length];
+	}
+}

+ 802 - 0
hide/comp/FancyTree.hx

@@ -0,0 +1,802 @@
+package hide.comp;
+
+enum DropFlag {
+	Reorder;
+	Reparent;
+}
+
+typedef DropFlags = haxe.EnumFlags<DropFlag>;
+
+enum RefreshFlag {
+	Flat;
+	Search;
+	FocusCurrent;
+	RegenHeader;
+}
+
+typedef RefreshFlags = haxe.EnumFlags<RefreshFlag>;
+
+enum FilterFlag {
+	Visible;
+	MatchSearch;
+	Open;
+}
+
+typedef FilterFlags = haxe.EnumFlags<FilterFlag>;
+
+typedef TreeItemData<TreeItem> = {element: js.html.Element, ?searchRanges: FancySearch.SearchRanges, item: TreeItem, name: String, ?iconCache: String, open: Bool, filterState: FilterFlags, children: Array<TreeItemData<TreeItem>>, parent: TreeItemData<TreeItem>, depth: Int, identifier: String};
+
+enum DropOperation {
+	Before;
+	After;
+	Inside;
+}
+
+class FancyTree<TreeItem> extends hide.comp.Component {
+	var rootData : Array<TreeItemData<TreeItem>> = [];
+	var itemMap : Map<{}, TreeItemData<TreeItem>> = [];
+	var selection : Map<{}, Bool> = [];
+	var openState: Map<String, Bool> = [];
+	var currentItem(default, set) : TreeItemData<TreeItem>;
+	var currentVisible(default, set) : Bool = false;
+
+	static final overDragOpenDelaySec = 0.5;
+
+	function set_currentVisible(v) {
+		currentVisible = v;
+		if (currentVisible)
+			queueRefresh(FocusCurrent);
+		else
+			queueRefresh();
+		return currentVisible;
+	}
+
+	function set_currentItem(v) {
+		currentItem = v;
+		queueRefresh(FocusCurrent);
+		return currentItem;
+	}
+
+	var searchBarClosable: hide.comp.FancyClosable = null;
+	var searchBar : hide.comp.FancySearch = null;
+
+	var flatData : Array<TreeItemData<TreeItem>>;
+
+	var itemContainer : js.html.Element;
+	var scroll : js.html.Element;
+	var currentSearch : String = "";
+
+	var moveLastDragOver: TreeItemData<TreeItem>;
+	var moveLastDragOverStart: Float = 0;
+
+
+	public function new(parent: Element) {
+		var el = new Element('
+			<fancy-tree2 tabindex="-1">
+				<fancy-closable><fancy-search></fancy-search></fancy-closable>
+				<fancy-scroll>
+				<fancy-item-container>
+				</fancy-item-container>
+				</fancy-scroll>
+			</fancy-tree2>'
+		);
+		super(parent, el);
+
+		searchBarClosable = new FancyClosable(null, element.find("fancy-closable"));
+
+		searchBar = new FancySearch(null, element.find("fancy-search"));
+		searchBar.onSearch = (search, _) -> {
+			currentSearch = search.toLowerCase();
+			queueRefresh(Search);
+		}
+
+		searchBarClosable.onClose = () -> {
+			currentSearch = "";
+			queueRefresh(Search);
+		}
+
+		scroll = el.find("fancy-scroll").get(0);
+		itemContainer = el.find("fancy-item-container").get(0);
+		lastHeight = null;
+
+		var fancyTree = el.get(0);
+		fancyTree.onkeydown = inputHandler;
+
+		scroll.onscroll = (e) -> queueRefresh();
+
+		fancyTree.onblur = (e) -> {
+			currentVisible = false;
+			currentItem = null;
+		}
+
+		fancyTree.onclick = (e) -> {
+			currentVisible = false;
+		}
+ 	}
+
+
+	/**
+		To customise the icon of an element
+	**/
+	public dynamic function getIcon(item: TreeItem) : String {return null;}
+
+	/**
+		The display name of an item
+	**/
+	public dynamic function getName(item: TreeItem) : String {return "undefined";}
+
+	/**
+		If items in the tree can have the same name, this function should return a unique name for each of them.
+		Used to save the state of the open folders in the tree
+	**/
+	public dynamic function getUniqueName(item: TreeItem) : String {return getName(item);}
+
+	/**
+		Called when the selected items in the tree changed
+	**/
+	public dynamic function onSelectionChanged() {
+	}
+
+	/**
+		Drag and drop interface.
+		Set this struct with all of it's function callback to handle drag and drop inside your tree.
+	**/
+	public var dragAndDropInterface :
+	{
+		/**
+			Called when the user starts a drag and drop operation on `item`.
+			Fill dataTransfer with the information you want to transfer, you can use getSelectedItems to handle dragging more than
+			one item at a time.
+			Return `true` if the drag operation is allowed, and `false` to cancel it
+		**/
+		onDragStart: (item: TreeItem, dataTransfer: js.html.DataTransfer) -> Bool,
+
+		/**
+			Called when the user hovers on `target` with a drag and drop operation. You need to return what drop orperation is allowed
+			on the given object
+		**/
+		getItemDropFlags: (target: TreeItem, dataTransfer: js.html.DataTransfer) -> DropFlags,
+
+		/**
+			Called when the user drops an item on `target` and getItemDropFlags returned at least one valid flag.
+			`where` tells you where the item was dropped, and you can use `dataTransfer` to know what was dropped
+		**/
+		onDrop: (target: TreeItem, where: DropOperation, dataTransfer: js.html.DataTransfer) -> Void
+	} = null;
+
+	/**
+		Called when the user renamed the item via F2 / Context menu
+	**/
+	public dynamic function onNameChange(item: TreeItem, newName: String) : Void {
+	}
+
+	/**
+		Called for each of your items in the tree. for the root elements, get called with null as a parameter
+	**/
+	public dynamic function getChildren(item: TreeItem) : Array<TreeItem> {return null;}
+
+	/**
+		Returns a string that allow an item in the tree to be uniquely identified.
+		Default to a path/of/the/item/name
+		Customize this if you have items that can share names
+	**/
+	public dynamic function getIdentifier(item: TreeItem) : String {
+		var data = itemMap.get(cast item);
+		if (data == null)
+			return null;
+		function rec(data) {
+			if (data.parent != null)
+				return getIdentifier(data.parent) + "/" + data.name;
+			return data.name;
+		}
+
+		return rec(data);
+	}
+
+	/**
+		Called to know if an item in the tree can be opened or has children. Default to calling getChildren and seeing if it returns false.
+		Set this function to optimise the initial loading of the tree if getChildren is expensive
+	**/
+	public dynamic function hasChildren(item : TreeItem) : Bool {
+		var children = getChildren(item);
+		if (children == null)
+			return false;
+		return children.length > 0;
+	}
+
+	public function getSelectedItems() : Array<TreeItem> {
+		return [for (item => _ in selection) (cast item:TreeItemData<TreeItem>).item];
+	}
+
+	public function generateChildren(parentData: TreeItemData<TreeItem>) : Array<TreeItemData<TreeItem>> {
+		var childrenTreeItem = getChildren(parentData?.item);
+
+		var childrenData : Array<TreeItemData<TreeItem>> = [];
+		if (childrenTreeItem != null) {
+			for (childItem in childrenTreeItem) {
+				var childData : TreeItemData<TreeItem>;
+				childData = {
+					item: childItem,
+					parent: parentData,
+					children: null,
+					open: false,
+					filterState: Visible,
+					depth: parentData?.depth + 1 ?? 0,
+					element: null,
+					name: StringTools.htmlEscape(getName(childItem)),
+					identifier: getIdentifier(childItem),
+				};
+				itemMap.set(cast childItem, childData);
+				childrenData.push(childData);
+			}
+		}
+		if (parentData != null) {
+			parentData.children = childrenData;
+		}
+		return childrenData;
+	}
+
+	public function ensureVisible(data) {
+
+	}
+
+	public function rebuildTree() {
+		rootData = generateChildren(null);
+
+		queueRefresh(Search);
+	}
+
+	function regenerateFlatData() {
+		flatData = [];
+		flattenRec(rootData, flatData);
+	}
+
+	public function selectItem(item: TreeItem, openSelf: Bool = false) {
+		clearSelection();
+		var data = itemMap.get(cast item);
+		if (data == null) {
+			return;
+		}
+		setSelection(data, true);
+		currentItem = data;
+		var cur = openSelf ? data : data.parent;
+		while (cur != null) {
+			toggleDataOpen(cur, true);
+			cur = cur.parent;
+		}
+	}
+
+	function inputHandler(e: js.html.KeyboardEvent) {
+		if (hide.ui.Keys.matchJsEvent("search", e, ide.currentConfig)) {
+			e.stopPropagation();
+			e.preventDefault();
+
+			searchBarClosable.toggleOpen(true);
+			searchBar.focus();
+		}
+
+		// if (hide.ui.Keys.matchJsEvent("rename", e, ide.currentConfig) && selection.iterator().hasNext()) {
+		// 	e.stopPropagation();
+		// 	e.preventDefault();
+
+		// 	beginRename(cast selection.keyValueIterator().next().key);
+		// }
+
+		if (e.key == "Escape") {
+			if (searchBarClosable.isOpen()) {
+				e.stopPropagation();
+				e.preventDefault();
+
+				searchBarClosable.toggleOpen(false);
+				searchBar.blur();
+				element.get(0).focus();
+				currentSearch = "";
+				queueRefresh(Search);
+			}
+		}
+
+		var delta = 0;
+		switch (e.key) {
+			case "ArrowUp":
+				delta -= 1;
+			case "ArrowDown":
+				delta += 1;
+			case "PageUp":
+				delta -= 10;
+			case "PageDown":
+				delta += 10;
+		}
+
+		if (delta != 0) {
+			e.stopPropagation();
+			e.preventDefault();
+			moveCurrent(delta);
+			return;
+		}
+
+		if (currentItem == null)
+			return;
+
+		if (e.key == "ArrowRight" && hasChildren(currentItem.item)) {
+			e.stopPropagation();
+			e.preventDefault();
+
+			if (currentItem == null || isOpen(currentItem)) {
+				moveCurrent(1);
+			}
+			else if (currentItem != null && !isOpen(currentItem)) {
+				toggleDataOpen(currentItem, true);
+				//saveState();
+			}
+			return;
+		}
+		if (e.key == "ArrowLeft") {
+			e.stopPropagation();
+			e.preventDefault();
+
+			var anyChildren = hasChildren(currentItem.item);
+			var goToParent = !anyChildren && currentItem.parent != null;
+			goToParent = goToParent || anyChildren && !isOpen(currentItem);
+
+			if (goToParent && currentItem.parent != null) {
+				currentItem = currentItem.parent;
+			} else if(anyChildren && currentItem != null) {
+				toggleDataOpen(currentItem, false);
+				//saveState();
+			}
+			return;
+		}
+
+		if (e.key == "Enter") {
+			e.stopPropagation();
+			e.preventDefault();
+
+			clearSelection();
+			if (currentItem != null) {
+				setSelection(currentItem, true);
+			}
+			onSelectionChanged();
+			return;
+		}
+	}
+
+	public function moveCurrent(delta: Int) {
+		if (delta == 0)
+			return;
+		if (flatData.length <= 0)
+			return;
+
+		currentVisible = true;
+
+		var currentIndex = flatData.indexOf(currentItem);
+		if (currentIndex < 0) {
+			currentItem = flatData[0];
+
+			if (searchBarClosable.isOpen() && searchBar.hasFocus()) {
+				searchBar.blur();
+				element.focus();
+			}
+			return;
+		}
+
+		var nextIndex = currentIndex + delta;
+		if (nextIndex < 0) {
+			if (searchBarClosable.isOpen()) {
+				searchBar.focus();
+				return;
+			}
+			else {
+				nextIndex = 0;
+			}
+		}
+		else {
+			if (searchBarClosable.isOpen() && searchBar.hasFocus()) {
+				searchBar.blur();
+				element.focus();
+			}
+		}
+
+		if (nextIndex > flatData.length-1)
+			nextIndex = flatData.length-1;
+
+		if (nextIndex != currentIndex) {
+			currentItem = flatData[nextIndex];
+		}
+	}
+
+	var lastHeight = null;
+
+	// Never call this function directly, instead call queueRefresh();
+	function onRefreshInternal() {
+		if (currentRefreshFlags.has(Search)) {
+			filterRec(rootData);
+			currentRefreshFlags.set(Flat);
+		}
+
+		if (currentRefreshFlags.has(Flat) || flatData == null) {
+			regenerateFlatData();
+		}
+
+		//itemContainer.innerHTML = "";
+		var oldChildren = [for (node in itemContainer.childNodes) node];
+
+		var itemHeightPx = 20;
+
+		var height = itemHeightPx * flatData.length;
+		if (height != lastHeight) {
+			itemContainer.style.height = '${height}px';
+			lastHeight = height;
+		}
+
+		var scrollHeight = scroll.getBoundingClientRect().height;
+
+		if (currentRefreshFlags.has(FocusCurrent)) {
+			var currentIndex = flatData.indexOf(currentItem);
+
+			if (currentIndex >= 0) {
+				var currentHeight = currentIndex * itemHeightPx;
+				if (currentHeight < scroll.scrollTop) {
+					scroll.scrollTo(scroll.scrollLeft, currentHeight);
+				}
+
+				if (currentHeight + itemHeightPx - scrollHeight > scroll.scrollTop) {
+					scroll.scrollTo(scroll.scrollLeft, currentHeight + itemHeightPx - scrollHeight);
+				}
+			}
+		}
+
+		var clipStart = scroll.scrollTop;
+		var clipEnd = scrollHeight + clipStart;
+		var itemStart = hxd.Math.floor(clipStart / itemHeightPx);
+		var itemEnd = hxd.Math.ceil(clipEnd / itemHeightPx);
+
+		for (index in hxd.Math.imax(itemStart, 0) ... hxd.Math.imin(flatData.length, itemEnd + 1)) {
+			var data = flatData[index];
+			var element = genElement(data);
+			element.style.top = '${index * itemHeightPx}px';
+			if (!oldChildren.remove(element))
+				itemContainer.appendChild(element);
+		}
+
+		for (oldChild in oldChildren) {
+			itemContainer.removeChild(oldChild);
+		}
+
+		currentRefreshFlags = RefreshFlags.ofInt(0);
+		refreshQueued = false;
+	}
+
+	public function filterRec(children: Array<TreeItemData<TreeItem>>) : Bool {
+		var anyVisible = false;
+		for (child in children) {
+			child.filterState = FilterFlags.ofInt(0);
+			child.searchRanges = null;
+
+			if (currentSearch.length == 0) {
+				child.filterState |= Visible;
+			} else {
+				child.searchRanges = FancySearch.computeSearchRanges(child.name, currentSearch);
+				if (child.searchRanges != null) {
+					child.filterState |= MatchSearch;
+					child.filterState |= Visible;
+				}
+			}
+			if (child.children == null) {
+				generateChildren(child);
+			}
+
+			if(filterRec(child.children) && currentSearch.length > 0) {
+				child.filterState |= Visible;
+				child.filterState |= Open;
+			}
+
+			anyVisible = anyVisible || child.filterState.has(Visible);
+		}
+
+		return anyVisible;
+	}
+
+	function genElement(data: TreeItemData<TreeItem>) : js.html.Element {
+		var element : js.html.Element = data.element;
+
+		if (currentRefreshFlags.has(RegenHeader) && data.element != null) {
+			data.element.remove();
+			data.element = null;
+		}
+
+		if (data.element == null) {
+			element = js.Browser.document.createElement("fancy-tree-item");
+			element.style.setProperty("--depth", Std.string(data.depth));
+
+			element.innerHTML =
+			'
+				<fancy-tree-icon class="caret"></fancy-tree-icon>
+				<fancy-tree-icon class="header-icon"></fancy-tree-icon>
+				<fancy-tree-name></fancy-tree-name>
+			';
+
+			var fold = element.querySelector(".caret");
+			fold.addEventListener("click", (e) -> {
+				toggleDataOpen(data);
+				//saveState();
+			});
+
+			var closure = dataClickHandler.bind(data);
+
+			var icon = element.querySelector(".header-icon");
+			icon.onclick = closure;
+
+			var name = element.querySelector("fancy-tree-name");
+			name.onclick = closure;
+
+			data.element = element;
+
+			setupDragAndDrop(data);
+		}
+
+		var fold = element.querySelector(".caret");
+		fold.classList.toggle("hidden", !hasChildren(data.item));
+		element.classList.toggle("open", isOpen(data));
+		element.classList.toggle("selected", selection.exists(cast data));
+		element.classList.toggle("current", currentVisible && currentItem == data);
+
+		var icon = element.querySelector(".header-icon");
+		var iconContent = getIcon(data.item);
+		icon.classList.toggle("hidden", iconContent == null);
+		if (iconContent != null && iconContent != data.iconCache) {
+			icon.innerHTML = iconContent;
+			data.iconCache = iconContent;
+		}
+
+		var nameElement = element.querySelector("fancy-tree-name");
+		element.title = data.name;
+
+		if (data.searchRanges != null) {
+			var name = data.name;
+			var lastPos = 0;
+			var finalName = "";
+			for (index in 0...(data.searchRanges.length>>1)) {
+				var first = name.substr(lastPos, data.searchRanges[index]);
+				var match = name.substr(data.searchRanges[index], data.searchRanges[index+1] - data.searchRanges[index]);
+				finalName += first + '<span class="search-hl">' + match + "</span>";
+				lastPos = data.searchRanges[index+1];
+			}
+			finalName += name.substr(lastPos);
+			nameElement.innerHTML = finalName;
+		} else {
+			nameElement.innerHTML = data.name;
+		}
+
+		return data.element;
+	}
+
+	function setupDragAndDrop(data: TreeItemData<TreeItem>) {
+		if (dragAndDropInterface != null) {
+			var ondragstart = (e: js.html.DragEvent) -> {
+				if (!selection.get(cast data)) {
+					clearSelection();
+					setSelection(data, true);
+				}
+
+				moveLastDragOver = null;
+
+				if (dragAndDropInterface.onDragStart(data.item, e.dataTransfer)) {
+					e.dataTransfer.effectAllowed = "move";
+					e.dataTransfer.setDragImage(data.element, 0, 0);
+				} else {
+					e.preventDefault();
+				}
+			};
+
+			var elements = [data.element.querySelector("fancy-tree-name"), data.element.querySelector(".header-icon")];
+
+			// drag from the interactible elements of the item
+			for (element in elements) {
+				element.draggable = true;
+				element.ondragstart = ondragstart;
+			}
+
+
+			// drop on the full item element
+			data.element.ondragover = (e: js.html.DragEvent) -> {
+				var operation = getDragOperation(data,e);
+				if (operation != null) {
+					// Auto open item if the user hover for enough time
+					if (operation == Inside) {
+						var time = haxe.Timer.stamp();
+
+						if (moveLastDragOver != data) {
+							moveLastDragOver = data;
+							moveLastDragOverStart = haxe.Timer.stamp();
+						}
+
+						if (time - moveLastDragOverStart > overDragOpenDelaySec && !isOpen(data)) {
+							toggleDataOpen(data, true);
+							//saveState();
+						}
+					}
+
+					e.preventDefault();
+					setDragStyle(data.element, operation);
+				} else {
+					setDragStyle(data.element, null);
+				}
+			}
+
+			data.element.ondragenter = (e: js.html.DragEvent) -> {
+				var operation = getDragOperation(data,e);
+				if (operation != null) {
+					setDragStyle(data.element, operation);
+					e.preventDefault();
+				}
+			}
+
+			data.element.ondragleave = (e: js.html.DragEvent) -> {
+				setDragStyle(data.element, null);
+				e.preventDefault();
+			}
+
+			data.element.ondrop = (e: js.html.DragEvent) -> {
+				setDragStyle(data.element, null);
+				e.preventDefault();
+
+				var operation = getDragOperation(data,e);
+				if (operation != null) {
+					dragAndDropInterface.onDrop(data.item, operation, e.dataTransfer);
+				}
+				e.preventDefault();
+				e.stopPropagation();
+			}
+		}
+	}
+
+	function setDragStyle(element: js.html.Element, target: Null<DropOperation>) {
+		element.classList.toggle("feedback-drop-top", target == Before);
+		element.classList.toggle("feedback-drop-bot", target == After);
+		element.classList.toggle("feedback-drop-in", target == Inside);
+	}
+
+	function getDragOperation(data: TreeItemData<TreeItem>, event: js.html.DragEvent) : DropOperation {
+		var element = data.element;
+		var flags = dragAndDropInterface.getItemDropFlags(data.item, event.dataTransfer);
+		if (flags == DropFlags.ofInt(0)) {
+			return null;
+		}
+
+		if (!flags.has(Reorder)) {
+			return Inside;
+		}
+
+		var rect = element.getBoundingClientRect();
+		var nameRect = element.getBoundingClientRect();
+
+		if (flags.has(Reparent) && event.clientX > nameRect.left + 100) {
+			return Inside;
+		}
+
+		if (event.clientY > rect.top + rect.height / 2) {
+			return After;
+		}
+		return Before;
+	}
+
+	public function clearSelection() {
+		selection.clear();
+		queueRefresh();
+	}
+
+
+
+	function setSelection(data: TreeItemData<TreeItem>, select: Bool) {
+		if (select) {
+			selection.set(cast data, true);
+		} else {
+			selection.remove(cast data);
+		}
+	}
+
+	function dataClickHandler(data: TreeItemData<TreeItem>, event: js.html.MouseEvent) : Void {
+		if (!event.ctrlKey) {
+			clearSelection();
+		}
+
+		var currentIndex = flatData.indexOf(currentItem);
+		if (event.shiftKey && currentIndex >= 0) {
+			var newIndex = flatData.indexOf(data);
+
+			var min = hxd.Math.imin(currentIndex, newIndex);
+			var max = hxd.Math.imax(currentIndex, newIndex);
+
+			for (i in min...max + 1) {
+				setSelection(flatData[i], true);
+			}
+		} else {
+			setSelection(data, !selection.exists(cast data));
+		}
+
+		if (!(event.shiftKey && !event.ctrlKey) || currentItem == null)
+			currentItem = data;
+		onSelectionChanged();
+
+		queueRefresh();
+	}
+
+	public function openItem(item: TreeItem, ?force: Bool) {
+		var data = itemMap.get(cast item);
+		if (data != null) {
+			toggleDataOpen(data, force);
+		}
+	}
+
+	function toggleDataOpen(data: TreeItemData<TreeItem>, ?force: Bool) {
+		var want = force ?? !isOpen(data);
+		if (currentSearch.length > 0) {
+			data.filterState.setTo(Open, want);
+		}
+		data.open = want;
+		queueRefresh(Flat);
+	}
+
+	var refreshQueued : Bool = false;
+	var currentRefreshFlags : RefreshFlags = RefreshFlags.ofInt(0);
+
+
+	// TODO(ces) : The main release of haxe doesn't support type inference with `|` which make using
+	// queueRefresh with an EnumFlag as an argument cumbersome. Untill then, make multiple queueRefresh calls
+	// with each of the flags you want to set
+	function queueRefresh(?flag: RefreshFlag = null) {
+		if (flag != null) {
+			currentRefreshFlags.set(flag);
+		}
+		if (!refreshQueued) {
+			refreshQueued = true;
+			js.Browser.window.requestAnimationFrame((_) -> onRefreshInternal());
+		}
+	}
+
+	public static function animateReveal(element: js.html.Element, reveal: Bool, durationMs: Int = 75) {
+		function finish() {
+			if (reveal) {
+				element.style.height = "auto";
+			} else {
+				element.style.height = null;
+			}
+		};
+		for (anim in element.getAnimations()) {
+			anim.cancel();
+		}
+
+		if (durationMs > 0) {
+			var anim = element.animate([
+				{height: "0px"},
+				{height: '${element.scrollHeight}px'},
+			], {
+				duration: durationMs,
+				iterations: 1,
+				direction: reveal ? js.html.PlaybackDirection.NORMAL : js.html.PlaybackDirection.REVERSE,
+				easing: "ease-in",
+			});
+
+			anim.onfinish = (e) -> finish();
+		}
+		else {
+			finish();
+		}
+	}
+	function flattenRec(currentArray: Array<TreeItemData<TreeItem>>, targetArray: Array<TreeItemData<TreeItem>>) {
+		for (child in currentArray) {
+			if (!child.filterState.has(Visible)) continue;
+			targetArray.push(child);
+			if (isOpen(child)) {
+				if (child.children == null)
+					generateChildren(child);
+				flattenRec(child.children, targetArray);
+			}
+		}
+	}
+
+	function isOpen(data: TreeItemData<TreeItem>) {
+		return data.open || data.filterState.has(Open);
+	}
+
+}

+ 787 - 0
hide/comp/FancyTreeOld.hx

@@ -0,0 +1,787 @@
+// package hide.comp;
+
+// /**
+// 	TODO :
+// 		[X] Search
+// 		[X] Rename item
+// 		[ ] Move items
+// 		[X] Save fold state
+// 		[ ] Customisable context menu
+// 		[ ] Customisable end of list icons
+// 		[ ] Custom "pills" info near the title
+// 		[X] Fold animation
+// 		[X] General styling
+
+// **/
+
+// enum MoveAllowedOperations {
+// 	Reorder;
+// 	Reparent;
+// }
+
+// typedef MoveFlags = haxe.EnumFlags<MoveAllowedOperations>;
+
+// enum GetDragTarget {
+// 	None;
+// 	Top;
+// 	Bot;
+// 	In;
+// }
+
+
+// typedef TreeItemData<TreeItem> = {?element: js.html.Element, ?header: js.html.Element, ?children: Array<TreeItem>, ?parent: TreeItem, ?depth: Int, ?item: TreeItem, ?temporaryOpen: Bool, ?passSearch: Bool, ?name: String, ?path: String};
+
+// class FancyTree<TreeItem : Dynamic> extends hide.comp.Component {
+// 	public var itemMap : Map<{}, TreeItemData<TreeItem>> = [];
+
+// 	// Forced to use another object because itemMap can't use null as an index for some reason
+// 	var rootData : TreeItemData<TreeItem> = {};
+
+// 	var openState: Map<String, Bool> = [];
+
+// 	public var moveFlags : MoveFlags = MoveFlags.ofInt(0);
+
+// 	var selection : Map<{}, Bool> = [];
+// 	var currentItem : TreeItem;
+
+// 	var searchBar : hide.comp.FancySearch = null;
+
+// 	var moveLastDragOver : TreeItem;
+// 	var moveLastDragTime : Int = 0;
+
+// 	public function new(parent: Element) {
+// 		var el = new Element('<fancy-tree tabindex="-1">
+// 			<fancy-search></fancy-search>
+// 			<fancy-wrapper></fancy-wrapper></fancy-tree>');
+// 		super(parent, el);
+
+// 		searchBar = new FancySearch(null, element.find("fancy-search"));
+// 		searchBar.onSearch = (search, _) -> {
+// 			filterRec(null, search);
+
+// 			ensureVisible(getDataOrRoot(currentItem));
+// 		}
+
+// 		var fancyTree = el.get(0);
+// 		fancyTree.onkeydown = (e: js.html.KeyboardEvent) -> {
+
+// 			if (hide.ui.Keys.matchJsEvent("search", e, ide.currentConfig)) {
+// 				e.stopPropagation();
+// 				e.preventDefault();
+
+// 				searchBar.toggleSearch(true, true);
+// 			}
+
+// 			if (hide.ui.Keys.matchJsEvent("rename", e, ide.currentConfig) && selection.iterator().hasNext()) {
+// 				e.stopPropagation();
+// 				e.preventDefault();
+
+// 				beginRename(cast selection.keyValueIterator().next().key);
+// 			}
+
+// 			if (e.key == "Escape") {
+// 				if (searchBar.isOpen()) {
+// 					e.stopPropagation();
+// 					e.preventDefault();
+
+// 					searchBar.toggleSearch(false);
+// 					fancyTree.focus();
+// 					resetSearch(null);
+// 				}
+// 			}
+
+// 			var delta = 0;
+// 			switch (e.key) {
+// 				case "ArrowUp":
+// 					delta -= 1;
+// 				case "ArrowDown":
+// 					delta += 1;
+// 				case "PageUp":
+// 					delta -= 10;
+// 				case "PageDown":
+// 					delta += 10;
+// 			}
+
+// 			if (delta != 0) {
+// 				e.stopPropagation();
+// 				e.preventDefault();
+// 				moveCurrent(delta);
+// 				return;
+// 			}
+
+// 			if (currentItem == null)
+// 				return;
+
+// 			if (e.key == "ArrowRight" && hasChildren(currentItem)) {
+// 				e.stopPropagation();
+// 				e.preventDefault();
+
+// 				var currentData = getDataOrRoot(currentItem);
+// 				if (currentData == null || isDataVisuallyOpen(currentData)) {
+// 					moveCurrent(1);
+// 				}
+// 				else if (currentData != null && !isDataVisuallyOpen(currentData)) {
+// 					toggleItemOpen(currentItem, true);
+// 					saveState();
+// 				}
+// 				return;
+// 			}
+// 			if (e.key == "ArrowLeft") {
+// 				e.stopPropagation();
+// 				e.preventDefault();
+
+// 				var currentData = getDataOrRoot(currentItem);
+// 				if (currentData != null) {
+// 					var anyChildren = hasChildren(currentItem);
+// 					var goToParent = !anyChildren && currentData.parent != null;
+// 					goToParent = goToParent || anyChildren && !isDataVisuallyOpen(currentData);
+
+// 					if (goToParent && currentData.parent != null) {
+// 						setCurrent(currentData.parent);
+// 					} else if(anyChildren && currentItem != null) {
+// 						toggleItemOpen(currentItem, false);
+// 						saveState();
+// 					}
+// 				}
+// 				return;
+// 			}
+
+// 			if (e.key == "Enter") {
+// 				e.stopPropagation();
+// 				e.preventDefault();
+
+// 				clearSelection();
+// 				if (currentItem != null) {
+// 					setSelection(currentItem, true);
+// 				}
+// 				onSelectionChanged();
+// 				return;
+// 			}
+// 		};
+
+// 		resetItemCache();
+// 	}
+
+// 	public function beginRename(item: TreeItem) {
+// 		var data = getDataOrRoot(item);
+
+// 		var name = data.header.querySelector("fancy-tree-name");
+// 		name.contentEditable = "plaintext-only";
+// 		var edit = new ContentEditable(null, new Element(name));
+
+// 		edit.onChange = (newValue) -> {
+// 			onNameChange(item, name.textContent);
+// 			refreshHeader(data);
+// 			element.focus();
+// 		}
+
+// 		edit.onCancel = () -> {
+// 			refreshHeader(data);
+// 			element.focus();
+// 		}
+
+// 		edit.element.focus();
+// 	}
+
+// 	function saveState() {
+// 		saveDisplayState("openState", openState);
+// 	}
+
+// 	inline function isDataVisuallyOpen(data: TreeItemData<TreeItem>) {
+// 		return data.item == null || openState.get(data.path) || data.temporaryOpen;
+// 	}
+
+// 	/**
+// 		Called for each of your items in the tree. for the root elements, get called with null as a parameter
+// 	**/
+// 	public dynamic function getChildren(item: TreeItem) : Array<TreeItem> {return null;}
+
+// 	/**
+// 		Called to know if an item in the tree can be opened or has children. Default to calling getChildren and seeing if it returns false.
+// 		Set this function to optimise the initial loading of the tree if getChildren is expensive
+// 	**/
+// 	public dynamic function hasChildren(item : TreeItem) : Bool {
+// 		var children = getChildren(item);
+// 		if (children == null)
+// 			return false;
+// 		return children.length > 0;
+// 	}
+
+// 	/**
+// 		To customise the icon of an element
+// 	**/
+// 	public dynamic function getIcon(item: TreeItem) : js.html.Element {return null;}
+
+// 	/**
+// 		The display name of an item
+// 	**/
+// 	public dynamic function getName(item: TreeItem) : String {return "undefined";}
+
+// 	/**
+// 		If items in the tree can have the same name, this function should return a unique name for each of them.
+// 		Used to save the state of the open folders in the tree
+// 	**/
+// 	public dynamic function getUniqueName(item: TreeItem) : String {return getName(item);}
+
+// 	/**
+// 		Called when the selected items in the tree changed
+// 	**/
+// 	public dynamic function onSelectionChanged() {
+// 	}
+
+// 	/**
+// 		Used to filter if an item can be reparented to another. Only called if MoveFlags contains Reparent.
+// 		If this function return false, the reparent operation between the two arguments is not allowed
+// 	**/
+// 	public dynamic function canReparentTo(items: Array<TreeItem>, newParent: TreeItem) : Bool {
+// 		return true;
+// 	}
+
+// 	/**
+// 		Called to handle a reparent/reorder operation. newIndex will be -1 if the current MoveFlags don't contain can reorder, and t
+// 	**/
+// 	public dynamic function onMove(items: Array<TreeItem>, newParent: TreeItem, newIndex: Int) : Void {
+// 	}
+
+// 	/**
+// 		Called when the user renamed the item via F2 / Context menu
+// 	**/
+// 	public dynamic function onNameChange(item: TreeItem, newName: String) : Void {
+// 	}
+
+// 	public function getSelectedItems() : Array<TreeItem> {
+// 		return [for (item => _ in selection) cast item];
+// 	}
+
+// 	/**
+// 		Destroy and recreate all the elements in the tree.
+// 		All the cached children will be erased
+// 	**/
+// 	public function rebuildTree() {
+// 		resetItemCache();
+// 		redrawItems();
+// 		resetSearch(null);
+// 	}
+
+// 	function resetItemCache() {
+// 		itemMap.clear();
+// 		rootData = {element: js.Browser.document.createDivElement() /*element.find("fancy-wrapper").get(0)*/, depth: 0, path: ""};
+// 		openState = getDisplayState("openState") ?? openState;
+// 	}
+
+// 	function redrawItems() {
+// 		untyped rootData.element.replaceChildren();
+// 		initChildren(null);
+// 	}
+
+// 	function toggleItemOpen(item: TreeItem, ?force: Bool, animate: Bool = true, temporary: Bool = false) : Void {
+
+// 		var data = itemMap.get(cast item);
+
+// 		var wantOpen = force ?? !isDataVisuallyOpen(data);
+// 		var wasOpen = isDataVisuallyOpen(data);
+
+// 		data.temporaryOpen = wantOpen;
+// 		if (!temporary) {
+// 			openState.set(data.path, wantOpen);
+// 		}
+
+// 		if (wantOpen) {
+// 			initChildren(item);
+// 		}
+
+// 		data.element.classList.toggle("open", wantOpen);
+// 		var childrenElement = data.element.querySelector("fancy-tree-children");
+// 		if (animate && childrenElement != null && wantOpen != wasOpen) {
+// 			animateReveal(childrenElement, wantOpen);
+// 		}
+
+// 	}
+
+// 	public static function animateReveal(element: js.html.Element, reveal: Bool, durationMs: Int = 75) {
+// 		function finish() {
+// 			if (reveal) {
+// 				element.style.height = "auto";
+// 			} else {
+// 				element.style.height = null;
+// 			}
+// 		};
+
+// 		for (anim in element.getAnimations()) {
+// 			anim.cancel();
+// 		}
+
+// 		if (durationMs > 0) {
+// 			var anim = element.animate([
+// 				{height: "0px"},
+// 				{height: '${element.scrollHeight}px'},
+// 			], {
+// 				duration: durationMs,
+// 				iterations: 1,
+// 				direction: reveal ? js.html.PlaybackDirection.NORMAL : js.html.PlaybackDirection.REVERSE,
+// 				easing: "ease-in",
+// 			});
+
+// 			anim.onfinish = (e) -> finish();
+// 		}
+// 		else {
+// 			finish();
+// 		}
+// 	}
+
+// 	function syncOpen(item: TreeItem) : Void {
+// 		var data = getDataOrRoot(item);
+
+// 		if (item != null)
+// 			toggleItemOpen(item, openState.get(data.path) ?? false, false, false);
+
+// 		if (data.children != null) {
+// 			for (child in data.children) {
+// 				syncOpen(child);
+// 			}
+// 		}
+// 	}
+
+// 	function resetSearch(item: TreeItem) : Void {
+// 		var data = getDataOrRoot(item);
+// 		data.passSearch = true;
+// 		data.element.classList.toggle("hide-search", !data.passSearch);
+// 		//initChildren(item);
+// 		if (data.children != null) {
+// 			for (child in data.children) {
+// 				resetSearch(child);
+// 			}
+// 		}
+// 		if (resetSearch == null)
+// 			syncOpen(null);
+// 	}
+
+// 	static function filterMatch(haystack: String, needle: String) {
+// 		return StringTools.contains(haystack.toLowerCase(), needle.toLowerCase());
+// 	}
+
+// 	/**
+// 		Returns true if any children passes the current filter
+// 	**/
+// 	function filterRec(item: TreeItem, currentFilter: String) : Bool {
+// 		if (item == null) {
+// 			resetSearch(null);
+// 		}
+// 		var data = getDataOrRoot(item);
+
+// 		data.passSearch = data.name != null ? filterMatch(data.name, currentFilter) : false;
+
+// 		var anyChildrenPass = false;
+// 		if (!data.passSearch) {
+// 			if (data.children == null)
+// 				initChildren(data.item);
+// 			if (data.children != null) {
+// 				for (child in data.children) {
+// 					anyChildrenPass = filterRec(child, currentFilter) || anyChildrenPass;
+// 				}
+// 			}
+// 		}
+
+// 		if (anyChildrenPass) {
+// 			data.passSearch = true;
+// 			if (item != null) {
+// 				toggleItemOpen(item, true, false, true);
+// 			}
+// 		}
+
+// 		if (item != null) {
+// 			data.element.classList.toggle("hide-search", !data.passSearch);
+// 		}
+
+// 		return data.passSearch;
+// 	}
+
+// 	function initChildren(item: TreeItem) : Void {
+// 		var data = getDataOrRoot(item);
+// 		if (data?.element == null || data?.depth == null)
+// 			throw "Data is not properly initialised";
+
+// 		if (data.children == null) {
+// 			data.children = getChildren(item);
+
+// 			if (data.children != null) {
+// 				var childrenElement = js.Browser.document.createElement("fancy-tree-children");
+// 				data.element.append(childrenElement);
+
+// 				for (child in data.children) {
+// 					if (child == null)
+// 						continue;
+// 					var childElem = getElement(child, item, data.depth + 1);
+// 					if (childElem != null)
+// 						childrenElement.append(childElem);
+// 				}
+// 			}
+// 		}
+// 	}
+
+// 	function getDataOrRoot(item: TreeItem) {
+// 		if (item != null) {
+// 			return hrt.tools.MapUtils.getOrPut(itemMap, cast item, {item: item});
+// 		} else {
+// 			return rootData;
+// 		}
+// 	}
+
+// 	/**
+// 		The type used by this tree in the drag/drop operations
+// 	**/
+// 	inline public function getDragDataType() {
+// 		return ("application/x." + saveDisplayKey + ".move").toLowerCase();
+// 	}
+
+// 	function refreshHeader(data: TreeItemData<TreeItem>) {
+// 		data.header.innerHTML = "";
+
+// 		var fold = js.Browser.document.createElement("fancy-tree-icon");
+// 		data.header.append(fold);
+
+// 		if (hasChildren(data.item)) {
+// 			fold.classList.add("caret");
+// 			fold.addEventListener("click", (e) -> {
+// 				toggleItemOpen(data.item);
+// 				saveState();
+// 			});
+// 		}
+
+// 		function clickHandler(e: js.html.MouseEvent) {
+// 			var lastSelected = currentItem;
+// 			if (!e.ctrlKey) {
+// 				clearSelection();
+// 			}
+
+// 			var flat = flattenTreeItems();
+// 			var currentIndex = flat.indexOf(currentItem);
+// 			if (e.shiftKey && currentIndex >= 0) {
+// 				var currentIndex = flat.indexOf(currentItem);
+// 				var newIndex = flat.indexOf(data.item);
+
+// 				var min = hxd.Math.imin(currentIndex, newIndex);
+// 				var max = hxd.Math.imax(currentIndex, newIndex);
+
+// 				for (i in min...max + 1) {
+// 					setSelection(flat[i], true);
+// 				}
+// 			} else {
+// 				setSelection(data.item, !selection.exists(cast data.item));
+// 			}
+
+// 			if (!(e.shiftKey && !e.ctrlKey) || currentItem == null)
+// 				setCurrent(data.item);
+// 			onSelectionChanged();
+// 		}
+
+// 		var iconContent = getIcon(data.item);
+// 		if (iconContent != null) {
+// 			var icon = js.Browser.document.createElement("fancy-tree-icon");
+// 			icon.append(iconContent);
+// 			data.header.append(icon);
+// 			icon.onclick = clickHandler;
+// 		}
+
+// 		var nameElement = js.Browser.document.createElement("fancy-tree-name");
+// 		var name = getName(data.item) ?? "undefined";
+// 		data.name = name;
+// 		data.header.title = name;
+// 		nameElement.innerText = name;
+// 		data.header.append(nameElement);
+
+
+
+// 		if (moveFlags.toInt() != 0) {
+// 			data.header.draggable = true;
+
+// 			data.header.ondragstart = (e: js.html.DragEvent) -> {
+// 				var draggedPaths = [];
+// 				moveLastDragOver = null;
+// 				if (!selection.get(cast data.item)) {
+// 					clearSelection();
+// 					setSelection(data.item, true);
+// 				}
+// 				for (item in getSelectedItems()) {
+// 					var data = getDataOrRoot(item);
+// 					draggedPaths.push(data.path);
+// 				}
+// 				e.dataTransfer.setData(getDragDataType(), haxe.Json.stringify(draggedPaths));
+// 				trace(e.dataTransfer.types);
+// 				e.dataTransfer.effectAllowed = "move";
+// 				e.dataTransfer.setDragImage(data.header, 0, 0);
+// 			}
+
+// 			data.header.ondragover = (e: js.html.DragEvent) -> {
+// 				if (e.dataTransfer.types.contains(getDragDataType())) {
+// 					var target = getDragTarget(data,e);
+
+// 					if (canPreformMove(data, target) == null)
+// 						return;
+
+// 					if (target == In) {
+// 						if (moveLastDragOver == data.item) {
+// 							moveLastDragTime += 1;
+// 						}
+// 						else {
+// 							moveLastDragOver = data.item;
+// 							moveLastDragTime = 0;
+// 						}
+
+// 						if (moveLastDragTime > 25 && !isDataVisuallyOpen(data)) {
+// 							toggleItemOpen(data.item, true, true, false);
+// 							saveState();
+// 						}
+// 					}
+
+// 					setDragStyle(data.header, target);
+// 					e.preventDefault();
+// 				}
+// 			}
+
+// 			data.header.ondragenter = (e: js.html.DragEvent) -> {
+// 				if (e.dataTransfer.types.contains(getDragDataType())) {
+// 					var target = getDragTarget(data,e);
+// 					if (canPreformMove(data, target) == null)
+// 						return;
+// 					setDragStyle(data.header, target);
+// 					e.preventDefault();
+// 				}
+// 			}
+
+// 			data.header.ondragleave = (e: js.html.DragEvent) -> {
+// 				if (e.dataTransfer.types.contains(getDragDataType())) {
+// 					setDragStyle(data.header, None);
+// 					e.preventDefault();
+// 				}
+// 			}
+
+// 			data.header.ondragexit = (e: js.html.DragEvent) -> {
+// 				if (e.dataTransfer.types.contains(getDragDataType())) {
+// 					setDragStyle(data.header, None);
+// 					e.preventDefault();
+// 				}
+// 			}
+
+// 			data.header.ondrop = (e: js.html.DragEvent) -> {
+// 				if (e.dataTransfer.types.contains(getDragDataType())) {
+// 					var target = getDragTarget(data,e);
+// 					var moveOp = canPreformMove(data, target);
+
+// 					setDragStyle(data.header, None);
+// 					e.preventDefault();
+
+// 					if (moveOp == null)
+// 						return;
+
+// 					onMove(moveOp.toMove, moveOp.newParent, moveOp.newIndex);
+// 				}
+// 			}
+// 		}
+// 		nameElement.onclick = clickHandler;
+// 	}
+
+// 	function canPreformMove(data: TreeItemData<TreeItem>, target: GetDragTarget) : {toMove: Array<TreeItem>, newParent: TreeItem, newIndex: Int} {
+// 		var toMove = getSelectedItems();
+
+// 		var newParent = getDataOrRoot(data.parent);
+// 		var newIndex = 0;
+// 		var currIndex = newParent.children.indexOf(data.item);
+
+// 		switch(target) {
+// 			case Top:
+// 				newIndex = currIndex;
+// 			case Bot:
+// 				newIndex = currIndex + 1;
+// 			case In:
+// 				newParent = data;
+// 			default:
+// 		}
+
+// 		// Take into account that the item are removed from the children list before getting inserted at the new index,
+// 		// which causes the index to not match if these items are before the target
+// 		if (target == Top || target == Bot) {
+// 			for (item in toMove) {
+// 				var indexInParent = newParent.children.indexOf(item);
+// 				if (indexInParent >= 0 && indexInParent < currIndex) {
+// 					newIndex --;
+// 				}
+// 			}
+// 		}
+
+// 		if (!canReparentTo(toMove, newParent.item)) {
+// 			return null;
+// 		}
+
+// 		return {toMove: toMove, newParent: newParent.item, newIndex: newIndex};
+// 	}
+
+
+// 	function setDragStyle(element: js.html.Element, target: GetDragTarget) {
+// 		trace(target);
+// 		element.classList.toggle("feedback-drop-top", target == Top);
+// 		element.classList.toggle("feedback-drop-bot", target == Bot);
+// 		element.classList.toggle("feedback-drop-in", target == In);
+// 	}
+
+// 	function getDragTarget(data: TreeItemData<TreeItem>, event: js.html.DragEvent) : GetDragTarget {
+// 		var element = data.element;
+// 		// var canDropIn = moveFlags.has(Reparent) && canReparentTo()
+// 		if (!moveFlags.has(Reorder)) {
+// 			return In;
+// 		}
+
+// 		var rect = element.getBoundingClientRect();
+// 		var name = element.querySelector("fancy-tree-name");
+// 		var nameRect = element.getBoundingClientRect();
+
+// 		var padding = js.Browser.window.getComputedStyle(element).getPropertyValue;
+// 		if (moveFlags.has(Reparent) && event.clientX > nameRect.left + 100) {
+// 			return In;
+// 		}
+
+// 		if (event.clientY > rect.top + rect.height / 2) {
+// 			return Bot;
+// 		}
+// 		return Top;
+// 	}
+
+// 	function getElement(item : TreeItem, parent: TreeItem, depth: Int) : js.html.Element {
+// 		var data = getDataOrRoot(item);
+// 		data.depth = depth;
+// 		data.parent = parent;
+
+// 		if (data.element == null) {
+// 			data.element = js.Browser.document.createElement("fancy-tree-item");
+// 			data.element.style.setProperty("--depth", Std.string(depth));
+
+// 			var header = js.Browser.document.createElement("fancy-tree-header");
+// 			data.header = header;
+// 			data.element.append(header);
+// 			refreshHeader(data);
+// 		}
+
+// 		data.path = getDataOrRoot(parent).path + "/" + getUniqueName(item);
+
+// 		refreshItemSelection(item, selection.get(cast item) ?? false);
+
+// 		return data.element;
+// 	}
+
+// 	public function clearSelection() {
+// 		for (item => _ in selection) {
+// 			var item : TreeItem = cast item;
+// 			refreshItemSelection(item, false);
+// 		}
+// 		selection.clear();
+// 	}
+
+// 	public function setSelection(item: TreeItem, newStatus: Bool) {
+// 		if (newStatus == true)
+// 			selection.set(cast item, true);
+// 		else
+// 			selection.remove(cast item);
+// 		refreshItemSelection(item, newStatus);
+// 	}
+
+// 	public function setCurrent(item: TreeItem) {
+// 		if (currentItem != null) {
+// 			var data = getDataOrRoot(currentItem);
+// 			data?.element?.classList.remove("current");
+// 		}
+// 		currentItem = item;
+// 		var data = getDataOrRoot(item);
+// 		data?.element?.classList.add("current");
+
+// 		ensureVisible(data);
+// 	}
+
+// 	public function ensureVisible(data: TreeItemData<TreeItem>) {
+// 		if (data?.element != null) {
+// 			var root = element.get(0);
+// 			var rootRect = root.getBoundingClientRect();
+// 			var elemRect = data.element.getBoundingClientRect();
+
+// 			if (elemRect.top < rootRect.top) {
+// 				data.element.scrollIntoView(true);
+// 			}
+
+// 			if (elemRect.bottom > rootRect.bottom) {
+// 				data.element.scrollIntoView(false);
+// 			}
+// 		}
+// 	}
+
+// 	public function flattenTreeItems() : Array<TreeItem> {
+// 		var flat : Array<TreeItem> = [];
+// 		function flatten(item: TreeItem) {
+// 			var data = getDataOrRoot(item);
+// 			if (!data.passSearch)
+// 				return;
+// 			if (item != null)
+// 				flat.push(item);
+
+// 			if (data.children != null && isDataVisuallyOpen(data)) {
+// 				for (child in data.children) {
+// 					flatten(child);
+// 				}
+// 			}
+// 		}
+// 		flatten(null);
+// 		return flat;
+// 	}
+
+// 	public function moveCurrent(delta: Int) {
+// 		if (delta == 0)
+// 			return;
+
+// 		var flat = flattenTreeItems();
+
+// 		var currentIndex = flat.indexOf(currentItem);
+// 		if (currentIndex < 0) {
+// 			if (rootData.children == null)
+// 				return;
+
+// 			currentItem = flat[0];
+// 			setCurrent(currentItem);
+
+// 			if (searchBar.isOpen() && searchBar.hasFocus()) {
+// 				searchBar.blur();
+// 				element.focus();
+// 			}
+// 			return;
+// 		}
+
+// 		var nextIndex = currentIndex + delta;
+// 		if (nextIndex < 0) {
+// 			if (searchBar.isOpen()) {
+// 				searchBar.focus();
+// 				setCurrent(null);
+// 				return;
+// 			}
+// 			else {
+// 				nextIndex = 0;
+// 			}
+// 		}
+// 		else {
+// 			if (searchBar.isOpen() && searchBar.hasFocus()) {
+// 				searchBar.blur();
+// 				element.focus();
+// 			}
+// 		}
+
+// 		if (nextIndex > flat.length-1)
+// 			nextIndex = flat.length-1;
+
+// 		if (nextIndex != currentIndex) {
+// 			setCurrent(flat[nextIndex]);
+// 		}
+// 	}
+
+// 	function refreshItemSelection(item: TreeItem, status: Bool) {
+// 		var data = itemMap.get(cast item);
+// 		if (data?.element == null)
+// 			return;
+// 		data.element.classList.toggle("selected", status);
+// 	}
+// }

+ 15 - 7
hide/comp/Scene.hx

@@ -24,6 +24,7 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 	public var visible(default, null) : Bool = true;
 	public var editor : hide.comp.SceneEditor;
 	public var autoDisposeOutOfDocument : Bool = true;
+	public var autoUpdate : Bool = true;
 
 	var currentRenderProps: hrt.prefab.Reference;
 
@@ -249,6 +250,8 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 	}
 
 	function sync() {
+		if (!autoUpdate)
+			return;
 		if ( ide.isDebugger )
 			doSync();
 		else {
@@ -258,7 +261,7 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 			} catch (e:haxe.Exception) {
 				var e = errorHandler(e);
 				if (e != null) {
-					throw e;
+					js.Lib.rethrow();
 				}
 			}
 			if (!errorThisFrame) {
@@ -517,10 +520,12 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 		var e;
 		if( reload )
 			@:privateAccess hxd.res.Loader.currentInstance.cache.remove(path);
-		if( ide.isDebugger )
-			e = hxd.res.Loader.currentInstance.load(relPath);
-		else
-			e = try hxd.res.Loader.currentInstance.load(relPath) catch( e : hxd.res.NotFound ) null;
+		e = try {
+			hxd.res.Loader.currentInstance.load(relPath);
+		} catch( e : hxd.res.NotFound ) {
+			ide.quickError('Failed to load HMD at path $path : $e');
+			null;
+		}
 		if( e == null ) {
 			var data = sys.io.File.getBytes(fullPath);
 			if( data.get(0) != 'H'.code ) {
@@ -553,7 +558,7 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 		return hmd;
 	}
 
-	public function resetCamera( ?obj : h3d.scene.Object, distanceFactor = 1. ) {
+	public function resetCamera( ?obj : h3d.scene.Object, distanceFactor = 1. , ?maxDist: Float) {
 
 		if( defaultCamera != null ) {
 			s3d.camera.load(defaultCamera);
@@ -568,6 +573,9 @@ class Scene extends hide.comp.Component implements h3d.IDrawable {
 		var dy = Math.max(Math.abs(b.yMax),Math.abs(b.yMin));
 		var dz = Math.max(Math.abs(b.zMax),Math.abs(b.zMin));
 		var dist = Math.max(Math.max(dx * 6, dy * 6), dz * 4) * distanceFactor;
+		if (maxDist != null) {
+			dist = Math.min(maxDist, dist);
+		}
 		var ang = Math.PI / 4;
 		var zang = Math.PI * 0.4;
 		s3d.camera.pos.set(Math.sin(zang) * Math.cos(ang) * dist, Math.sin(zang) * Math.sin(ang) * dist, Math.cos(zang) * dist);
@@ -727,7 +735,7 @@ class PreviewCamController extends h3d.scene.Object {
 	var pushing : Int = -1;
 	var ignoreNext : Bool = false;
 	function onEvent(e : hxd.Event) {
-		if (getScene().children.length <= 1)
+		if (getScene()?.children.length <= 1)
 			return;
 		switch (e.kind) {
 			case EPush: {

+ 17 - 0
hide/comp/SceneEditor.hx

@@ -1114,6 +1114,23 @@ class SceneEditor {
 			onResize();
 		};
 
+		sceneEl.get(0).ondragover = (e: js.html.DragEvent) -> {
+			if (e.dataTransfer.types.contains(hide.view.FileBrowser.dragKey)) {
+				e.preventDefault();
+				return;
+			}
+		}
+
+		sceneEl.get(0).ondrop = (e: js.html.DragEvent) -> {
+			if (e.dataTransfer.types.contains(hide.view.FileBrowser.dragKey)) {
+				var files : Array<String> = haxe.Json.parse(e.dataTransfer.getData(hide.view.FileBrowser.dragKey));
+				@:privateAccess scene.canvas.focus();
+				onDragDrop(files, true, e);
+				e.preventDefault();
+				return;
+			}
+		}
+
 		editorDisplay = true;
 
 		view.keys.register("copy", {name: "Copy", category: "Edit"}, onCopy);

+ 6 - 0
hide/comp/ScenePreview.hx

@@ -20,7 +20,9 @@ class ScenePreview extends Scene {
 	public function new(config, parent, el, save: String) {
 		this.saveDisplayKey = save;
 		super(config, parent, el);
+	}
 
+	public function addToolbar() {
 		var toolbar = new Element('
 		<div class="hide-toolbar2">
 			<div class="tb-group">
@@ -112,6 +114,10 @@ class ScenePreview extends Scene {
 	}
 
 	function listRenderProps() : Array<{name: String, value: String}> {
+		return listRenderPropsStatic(config);
+	}
+
+	static public function listRenderPropsStatic(config: hide.Config) : Array<{name: String, value: String}> {
 		var renderProps = config.getLocal("scene.renderProps");
 		var ret : Array<{name: String, value: String}> = [];
 

+ 212 - 0
hide/tools/FileManager.hx

@@ -0,0 +1,212 @@
+package hide.tools;
+
+
+enum abstract GenToManagerCommand(String) {
+	var success;
+}
+
+enum abstract ManagerToGenCommand(String) {
+	var queue;
+	var prio;
+	var clear;
+}
+
+typedef GenToManagerSuccessMessage = {
+	var originalPath : String;
+	var thumbnailPath : String;
+}
+
+typedef FileData = {
+	name: String,
+	parent: FileData,
+}
+
+typedef MiniatureReadyCallback = (miniaturePath: String) -> Void;
+
+/**
+	Class that handle parsing and maintaining the state of the project files, and generate miniatures for them on demand
+**/
+class FileManager {
+
+	public static final thumbnailGeneratorPort = 9669;
+	public static final thumbnailGeneratorUrl = "localhost";
+
+	public static var inst(get, default) : FileManager;
+
+	var generatorWindow : nw.Window;
+
+	var onReadyCallbacks : Map<String, MiniatureReadyCallback> = [];
+
+	var serverSocket : hxd.net.Socket = null;
+	var generatorSocket : hxd.net.Socket = null;
+
+	static function get_inst() {
+		if (inst == null) {
+			inst = new FileManager();
+		}
+		return inst;
+	}
+
+	public static function onBeforeReload() {
+		if (inst != null) {
+			inst.cleanupGenerator();
+		}
+	}
+
+	var reloadQueued = false;
+
+	function queueReload() {
+		if (reloadQueued == false) {
+			reloadQueued = true;
+			haxe.Timer.delay(setupGenerator, 5000);
+		}
+	}
+
+	function setupGenerator() {
+		reloadQueued = false;
+		serverSocket = new hxd.net.Socket();
+		serverSocket.onError = (msg) -> {
+			hide.Ide.inst.quickError("FileManager socket error : " + msg);
+			cleanupGenerator();
+			queueReload();
+		}
+		serverSocket.bind(thumbnailGeneratorUrl, thumbnailGeneratorPort, (remoteSocket) -> {
+			if (generatorSocket != null) {
+				generatorSocket.close();
+			}
+			generatorSocket = remoteSocket;
+			generatorSocket.onError = (msg) -> {
+				hide.Ide.inst.quickError("Generator socket error : " + msg);
+				cleanupGenerator();
+				queueReload();
+			}
+
+			var handler = new hide.tools.ThumbnailGenerator.MessageHandler(generatorSocket, processThumbnailGeneratorMessage);
+
+			// resend command that weren't completed
+			for (path => _ in onReadyCallbacks) {
+				sendGenerateCommand(path);
+			}
+		});
+
+		nw.Window.open('app.html?thumbnail=true', cast {
+				new_instance: true,
+				show: false,
+				title: "HideThumbnailGenerator"
+			}, (win: nw.Window) -> {
+				generatorWindow = win;
+			win.on("close", () -> {
+
+				cleanupGenerator();
+			});
+		});
+	}
+
+	function cleanupGenerator() {
+		if (generatorSocket != null) {
+			generatorSocket.close();
+			generatorSocket = null;
+		}
+
+		if (serverSocket != null) {
+			serverSocket.close();
+			serverSocket = null;
+		}
+
+		if (generatorWindow != null) {
+			generatorWindow.close(true);
+			generatorWindow = null;
+		}
+		untyped nw.Window.getAll((win:nw.Window) -> {
+			if (win.title == "HideThumbnailGenerator") {
+				win.close(true);
+			}
+		});
+	}
+
+	function new() {
+		setupGenerator();
+	}
+
+	function processThumbnailGeneratorMessage(message: String) {
+		try {
+			var message = haxe.Json.parse(message);
+			switch(message.type) {
+				case success:
+					var message : GenToManagerSuccessMessage = message.data;
+					var cb = onReadyCallbacks.get(message.originalPath);
+					if (cb == null) {
+						return;
+						//throw "Generated a thumbnail for a file not registered";
+					}
+					cb(message.thumbnailPath);
+					onReadyCallbacks.remove(message.originalPath);
+				default:
+					throw "Unknown message type " + message.type;
+			}
+		} catch(e) {
+			hide.Ide.inst.quickError("Thumb Generator invalid message : " + e + "\n" + message);
+		}
+	}
+
+	var queued = false;
+
+	/**
+		Asyncrhonusly generates a miniature.
+		onReady is called back with the path of the loaded miniature, or null if the miniature couldn't be loaded
+	**/
+	public function renderMiniature(path: String, onReady: MiniatureReadyCallback) {
+		var ext = path.split(".").pop();
+		switch(ext) {
+			case "prefab" | "fbx" | "l3d" | "fx" | "shgraph" | "jpg" | "jpeg" | "png":
+				if (!onReadyCallbacks.exists(path)) {
+					onReadyCallbacks.set(path, onReady);
+					sendGenerateCommand(path);
+				}
+			default:
+				onReady(null);
+		}
+	}
+
+	public function clearRenderQueue() {
+		onReadyCallbacks.clear();
+		if (generatorSocket == null) {
+			return;
+		}
+		var message = {
+			type: ManagerToGenCommand.clear,
+		};
+		var cmd = haxe.Json.stringify(message) + "\n";
+		generatorSocket.out.writeString(cmd);
+	}
+
+	public function setPriority(path: String, newPriority: Int) {
+		if (!onReadyCallbacks.exists(path)) {
+			return;
+		}
+		if (generatorSocket == null) {
+			return;
+		}
+		var message = {
+			type: ManagerToGenCommand.prio,
+			path: path,
+			prio: newPriority
+		};
+		var cmd = haxe.Json.stringify(message) + "\n";
+		generatorSocket.out.writeString(cmd);
+	}
+
+	function sendGenerateCommand(path: String) {
+		if (generatorSocket == null) {
+			return;
+		}
+		var message = {
+			type: ManagerToGenCommand.queue,
+			path: path,
+		};
+		var cmd = haxe.Json.stringify(message) + "\n";
+		generatorSocket.out.writeString(cmd);
+	}
+
+
+}

+ 414 - 0
hide/tools/ThumbnailGenerator.hx

@@ -0,0 +1,414 @@
+package hide.tools;
+
+typedef RenderInfo = {path: String, cb: hide.tools.FileManager.MiniatureReadyCallback, priority: Int};
+
+/**
+	Handle recieving messages separated by `\n` characters by a socket, correctly buffering the data
+**/
+class MessageHandler {
+	var socket: hxd.net.Socket;
+	var bufferedData : haxe.io.Bytes;
+	var bufferSize = 0;
+	static final maxBufferSize = 16384;
+
+	public function new(socket: hxd.net.Socket, callback: (content: String) -> Void) {
+		this.socket = socket;
+		bufferedData = haxe.io.Bytes.alloc(maxBufferSize);
+		bufferSize = 0;
+
+		socket.onData = () -> {
+			while(socket.input.available > 0) {
+				var read = hxd.Math.imin(maxBufferSize - bufferSize, socket.input.available);
+				if (read == 0) {
+					throw "message too long";
+				}
+
+				socket.input.readFullBytes(bufferedData, bufferSize, read);
+				bufferSize += read;
+
+				var last = 0;
+				var pos = 0;
+
+				// split on newLines
+				while(pos < bufferSize) {
+					if (bufferedData.get(pos) == 10) {
+						var command = bufferedData.getString(last, pos-last);
+						callback(command);
+						last = pos+1;
+					}
+					pos ++;
+				}
+
+				if (last > 0) {
+					var remaining = bufferSize - last;
+					if (remaining > 0) {
+						bufferedData.blit(0, bufferedData, last, remaining);
+						bufferSize = remaining;
+					} else {
+						bufferSize = 0;
+					}
+				} else if (bufferSize == maxBufferSize) {
+					throw "message too long";
+				}
+			}
+		}
+	}
+
+}
+
+@:access(hide.tools.FileManager)
+class ThumbnailGenerator {
+	var miniaturesToRender : Array<RenderInfo> = [];
+	var prioDirty = false;
+	var renderCanvas : hide.comp.Scene;
+	var renderTexture : h3d.mat.Texture;
+	final renderRes = 512;
+
+	var sceneRoot : h3d.scene.Object;
+
+	var socket : hxd.net.Socket = null;
+	var ready : Bool = false;
+
+	static final debugBypassCache = false;
+	static final debugShowWindow = false;
+
+	function sendSuccess(originalPath: String, finalPath: String) {
+		var message = {
+			type: hide.tools.FileManager.GenToManagerCommand.success,
+			data: ({
+				originalPath: originalPath,
+				thumbnailPath: finalPath,
+			}:hide.tools.FileManager.GenToManagerSuccessMessage)
+		};
+		var serialized = haxe.Json.stringify(message);
+		socket.out.writeString(serialized + "\n");
+	}
+
+	var bufferedData : haxe.io.Bytes;
+	var bufferSize = 0;
+	static final maxBufferSize = 16384;
+
+
+
+	function new() {
+		if (debugShowWindow) {
+			nw.Window.get().show(true);
+		} else {
+			untyped nw.Window.get().hide();
+		}
+		nw.Window.get().resizeTo(128,128);
+
+		bufferedData = haxe.io.Bytes.alloc(maxBufferSize);
+
+		socket = new hxd.net.Socket();
+
+		// Destroy the generator if any error occurs
+		socket.onError = (msg) -> {
+			nw.Window.get().close(true);
+		}
+
+		var handler = new MessageHandler(socket, handleCommand);
+
+		socket.connect(hide.tools.FileManager.thumbnailGeneratorUrl, hide.tools.FileManager.thumbnailGeneratorPort, () -> {
+		});
+
+		var cont = new Element('<div style="width: 512px; height: 512px; z-index: 10000; position: absolute; top:0; left: 0;"></div>').appendTo(js.Browser.document.body);
+		renderCanvas = new hide.comp.Scene(hide.Ide.inst.currentConfig, cont, null);
+		renderCanvas.enableNewErrorSystem = true;
+		renderCanvas.errorHandler = (e) -> {
+			// do nothing;
+			return null;
+		}
+		renderCanvas.autoUpdate = false;
+		renderCanvas.onReady = () -> {
+			renderCanvas.engine.setCurrent();
+
+			renderCanvas.s3d.removeChildren();
+			renderCanvas.s2d.removeChildren();
+
+			renderTexture = new h3d.mat.Texture(renderRes,renderRes, [Target]);
+
+			sceneRoot = new h3d.scene.Object(renderCanvas.s3d);
+
+			renderCanvas.errorHandler = (e) -> null;
+
+			var renderPropsList = hide.comp.ScenePreview.listRenderPropsStatic(hide.Ide.inst.config.current);
+			if (renderPropsList.length > 0) {
+				renderCanvas.setRenderProps(renderPropsList[0].value);
+			}
+
+
+			haxe.Timer.delay(() -> {
+				this.ready = true;
+			}, 100);
+		};
+	}
+
+	function handleCommand(command: String) {
+		var message : Dynamic = {};
+		try {
+			message = haxe.Json.parse(command);
+		} catch (e) {
+			return;
+		}
+		switch((message.type:FileManager.ManagerToGenCommand)) {
+			case queue:
+					var thumbPath = getThumbPath(message.path).toString();
+
+					var shouldGenerate = true;
+					if (!debugBypassCache && sys.FileSystem.exists(thumbPath)) {
+						var thumbStat = sys.FileSystem.stat(thumbPath);
+						var fileStat = sys.FileSystem.stat(message.path);
+						if (thumbStat.mtime.getTime() > fileStat.mtime.getTime()) {
+							shouldGenerate = false;
+							sendSuccess(message.path, thumbPath);
+						}
+					}
+
+					if (shouldGenerate) {
+						renderMiniature(message.path, sendSuccess.bind(message.path));
+					}
+			case clear:
+				miniaturesToRender = [];
+			case prio:
+				var toSet = Lambda.find(miniaturesToRender, (m) -> m.path == message.path);
+				if (toSet != null) {
+					toSet.priority = message.prio;
+					prioDirty = true;
+				}
+		}
+	}
+
+	var queued = false;
+
+	/**
+		Asynchronously generates a miniature.
+		onReady is called back with the path of the loaded miniature, or null if the miniature couldn't be loaded
+	**/
+	public function renderMiniature(path: String, onReady: hide.tools.FileManager.MiniatureReadyCallback) {
+		miniaturesToRender.push({path: path, cb: onReady, priority: 0});
+		if (!queued) {
+			haxe.Timer.delay(processMiniature, 1);
+		}
+	}
+
+	public static final thumbRoot = ".tmp/";
+	public static final thumbExt = "thumb.jpg";
+
+	static public function getThumbPath(basePath: String) : haxe.io.Path {
+		basePath = StringTools.replace(basePath, hide.Ide.inst.resourceDir, "");
+		var path = new haxe.io.Path(haxe.io.Path.join([hide.Ide.inst.resourceDir, thumbRoot, basePath]));
+		path.ext = thumbExt;
+		return path;
+	}
+
+	function handleModel(toRender: RenderInfo) {
+		renderCanvas.engine.setCurrent();
+
+		sceneRoot.removeChildren();
+
+		var engine = renderCanvas.engine;
+
+		var ctx = new hide.prefab.ContextShared(null, sceneRoot);
+		ctx.scene = renderCanvas;
+
+		var ext = toRender.path.split(".").pop();
+
+		var abort = false;
+		if (ext == "fbx") {
+			var model = new hrt.prefab.Model(null, null);
+			model.source = toRender.path;
+			model.make(ctx);
+		} else if (ext == "prefab" || ext == "l3d" || ext == "fx") {
+			try {
+				var ref = new hrt.prefab.Reference(null, null);
+				var cut = StringTools.replace(toRender.path, hide.Ide.inst.resourceDir + "/", "");
+				ref.source = cut;
+
+				var prefab = ref.make(ctx);
+
+				if (ext == "fx") {
+					var fx = prefab.find(hrt.prefab.fx.FX, true, false);
+					if (fx != null) {
+						var fxAnim = Std.downcast(fx.local3d, hrt.prefab.fx.FX.FXAnimation);
+						// Forward the animations a little bit to show something more usefull
+						if (fxAnim != null) {
+							var duration = fxAnim.duration;
+							fxAnim.setTime(duration * 0.25);
+						}
+					}
+				}
+
+			} catch (e) {
+				hide.Ide.inst.quickError('miniature render fail for ${toRender.path} : $e');
+				abort = true;
+			}
+		} else if (ext == "shgraph") {
+			try {
+				var spherePrim = new h3d.prim.Sphere(1.0, 32, 32, 1);
+				spherePrim.addNormals();
+				spherePrim.addUVs();
+				spherePrim.addTangents();
+
+				var sphere = new h3d.scene.Mesh(spherePrim, sceneRoot);
+
+				var shgraph = new hrt.prefab.DynamicShader(null, null);
+				var cut = StringTools.replace(toRender.path, hide.Ide.inst.resourceDir + "/", "");
+				shgraph.source = cut;
+				ctx = new hide.prefab.ContextShared(null, sphere);
+				shgraph.makeShader();
+				for (m in sphere.getMaterials()) {
+					@:privateAccess shgraph.applyShader(sphere, m, shgraph.shader);
+				}
+			} catch(e) {
+				hide.Ide.inst.quickError('miniature render fail for ${toRender.path} : $e');
+				abort = true;
+			}
+		}
+		if (!abort) {
+			try {
+
+				renderCanvas.resetCamera(sceneRoot, 0.85, 32.0);
+
+				renderTexture.clear(0,0);
+
+				@:privateAccess renderCanvas.doSync();
+
+				engine.pushTarget(renderTexture);
+				engine.clear();
+				renderCanvas.s3d.render(engine);
+				engine.popTarget();
+
+				sceneRoot.removeChildren();
+
+				var path = convertAndWriteThumbnail(toRender.path, renderTexture);
+				toRender.cb(path);
+			}
+			catch (e) {
+				hide.Ide.inst.quickError('miniature render fail for ${toRender.path} : $e');
+				toRender.cb(null);
+			}
+		} else {
+			toRender.cb(null);
+		}
+	}
+
+	function convertAndWriteThumbnail(basePath: String, texture: h3d.mat.Texture) {
+		var path = getThumbPath(basePath).toString();
+		path = StringTools.replace(path, "\\", "/");
+
+		var dir = path.split("/");
+		dir.pop();
+		var dirPath = dir.join("/") + "/";
+		if(!sys.FileSystem.isDirectory( hide.Ide.inst.getPath(dirPath)))
+			sys.FileSystem.createDirectory( hide.Ide.inst.getPath(dirPath));
+
+		var pixels = texture.capturePixels();
+		pixels.convert(ARGB);
+		//sys.io.File.saveBytes(path, renderTexture.capturePixels().toPNG());
+		var bytes = new haxe.io.BytesOutput();
+		var writer = new format.jpg.Writer(bytes);
+		writer.write({
+			width: texture.width,
+			height: texture.height,
+			pixels: pixels.bytes,
+			quality: 50
+		});
+
+		sys.io.File.saveBytes(path, bytes.getBytes());
+		return path;
+	}
+
+	function handleTexture(toRender: RenderInfo) {
+		renderCanvas.engine.setCurrent();
+
+		var cut = StringTools.replace(toRender.path, hide.Ide.inst.resourceDir + "/", "");
+		var img = hxd.res.Loader.currentInstance.load(cut).toTexture();
+		var width = img.width;
+		var height = img.height;
+
+		final size = 512;
+
+		if (width > height) {
+			height = hxd.Math.floor(height / width * size);
+			width = size;
+		} else if (width < height) {
+			width = hxd.Math.floor(width / height * size);
+			height = size;
+		} else {
+			width = size;
+			height = size;
+		}
+
+		renderCanvas.s2d.removeChildren();
+
+		var bg = new h2d.Bitmap(h2d.Tile.fromColor(0), renderCanvas.s2d);
+		bg.width = size;
+		bg.height = size;
+
+		var bmp = new h2d.Bitmap(h2d.Tile.fromTexture(img), renderCanvas.s2d);
+		bmp.width = width;
+		bmp.height = height;
+		bmp.x = (size - width) / 2;
+		bmp.y = (size - height) / 2;
+
+		bmp.blendMode = None;
+
+		var shader = new hide.view.GraphEditor.PreviewShaderAlpha();
+		bmp.addShader(shader);
+
+
+		var engine = renderCanvas.engine;
+
+		engine.pushTarget(renderTexture);
+		engine.clear();
+		renderCanvas.render(engine);
+		engine.popTarget();
+
+		renderCanvas.s2d.removeChildren();
+
+		var path = convertAndWriteThumbnail(toRender.path, renderTexture);
+
+		// restore renderTexture original size
+		renderTexture.resize(512, 512);
+
+		toRender.cb(path);
+	}
+
+	function processMiniature() {
+		if (!ready || renderCanvas.s3d == null) {
+			haxe.Timer.delay(processMiniature, 1);
+			return;
+		}
+
+		queued = false;
+
+		if (miniaturesToRender.length == 0) {
+			return;
+		}
+
+		if (prioDirty) {
+			miniaturesToRender.sort((a, b) -> Reflect.compare(a.priority, b.priority));
+			prioDirty = false;
+		}
+
+		var toRender = miniaturesToRender.pop();
+
+		var ext = toRender.path.split(".").pop();
+		switch(ext) {
+			case "prefab" | "fbx" | "l3d" | "fx" | "shgraph":
+				handleModel(toRender);
+			case "jpg" | "jpeg" | "png":
+				handleTexture(toRender);
+			default:
+				toRender.cb(null);
+		}
+
+
+		if (miniaturesToRender.length > 0) {
+			haxe.Timer.delay(processMiniature, 1);
+			return;
+		}
+
+	}
+}

+ 19 - 0
hide/ui/Keys.hx

@@ -77,6 +77,25 @@ class Keys {
 		return false;
 	}
 
+	static public function matchJsEvent(shortcutName : String, event: js.html.KeyboardEvent, config : Config) {
+		var keyCode : String = config.get("key."+shortcutName);
+
+		var split = keyCode.split("-");
+		for (part in split) {
+			switch (part) {
+				case "Shift":
+					if(!event.shiftKey) return false;
+				case "Ctrl":
+					if(!event.ctrlKey) return false;
+				case "Alt":
+					if(!event.altKey) return false;
+				default:
+					if(hxd.Key.getKeyName(event.keyCode) != part) return false;
+			}
+		}
+		return true;
+	}
+
 	public function triggerKey( e : Element.Event, key : String, config : Config ) {
 		for( l in listeners )
 			if( l(e) )

+ 7 - 6
hide/ui/View.hx

@@ -1,13 +1,14 @@
 package hide.ui;
 
-enum DisplayPosition {
-	Left;
-	Center;
-	Right;
-	Bottom;
+enum abstract DisplayPosition(String) from String to String {
+	var Left = "content_left";
+	var Center = "content_center";
+	var Right = "content_right";
+	var Bottom = "content_bottom";
+	var MiddleColumnInternal = "content_middle_internal";
 }
 
-typedef ViewOptions = { ?position : DisplayPosition, ?width : Int }
+typedef ViewOptions = { ?position : DisplayPosition, ?width : Int, ?id: String }
 
 @:keepSub @:allow(hide.Ide)
 class View<T> extends hide.comp.Component {

+ 313 - 0
hide/view/FileBrowser.hx

@@ -0,0 +1,313 @@
+package hide.view;
+
+typedef FileBrowserState = {
+
+}
+
+enum FileKind {
+	Dir;
+	File;
+}
+
+typedef FileEntry = {
+	name: String,
+	children: Array<FileEntry>,
+	kind: FileKind,
+	parent: FileEntry,
+	iconPath: String,
+}
+
+class FileBrowser extends hide.ui.View<FileBrowserState> {
+
+	var fileTree: Element;
+	var fileIcons: Element;
+
+	var root : FileEntry;
+
+	override function new(state) {
+		super(state);
+	}
+
+	override function onDragDrop(items:Array<String>, isDrop:Bool, event:js.html.DragEvent):Bool {
+		return false;
+	}
+
+	function populateChildren(file: FileEntry) {
+		var fullPath = getFileEntryPath(file);
+		var paths = js.node.Fs.readdirSync(fullPath);
+		file.children = [];
+		for (path in paths) {
+			if (StringTools.startsWith(path, "."))
+				continue;
+			var info = js.node.Fs.statSync(fullPath + "/" + path);
+			file.children.push({
+				name: path,
+				kind: info.isDirectory() ? Dir : File,
+				parent: file,
+				children: null,
+				iconPath: null,
+			});
+		}
+
+		file.children.sort(compareFile);
+	}
+
+	// sort directories before files, and then dirs and files alphabetically
+	function compareFile(a: FileEntry, b: FileEntry) {
+		if (a.kind != b.kind) {
+			if (a.kind == Dir) {
+				return -1;
+			}
+			return 1;
+		}
+		return Reflect.compare(a.name, b.name);
+	}
+
+	function getFileEntryPath(file: FileEntry) {
+		if (file.parent == null) return ide.resourceDir;
+		return getFileEntryPath(file.parent) + "/" + file.name;
+	}
+
+	public static final dragKey = "application/x.filemove";
+
+	var currentFolder : FileEntry;
+	var currentSearch = [];
+	var searchString: String = "";
+	var fancyGallery : hide.comp.FancyGallery<FileEntry>;
+	var fancyTree: hide.comp.FancyTree<FileEntry>;
+
+
+	function onSearch() {
+		hide.tools.FileManager.inst.clearRenderQueue();
+		currentSearch = [];
+		if (searchString.length == 0) {
+			currentSearch = currentFolder.children;
+		} else {
+			function rec(files: Array<FileEntry>) {
+				for (file in files) {
+					if (file.kind == Dir) {
+						rec(file.children);
+					}
+					else {
+						var range = hide.comp.FancySearch.computeSearchRanges(file.name, searchString);
+						if (range != null) {
+							currentSearch.push(file);
+						}
+					}
+				}
+			}
+
+			rec(currentFolder.children);
+		}
+
+		for (i => _ in currentSearch) {
+			var child = currentSearch[currentSearch.length - i - 1];
+			if ((child.iconPath == null || child.iconPath == "loading") && child.kind == File) {
+				child.iconPath = "loading";
+				hide.tools.FileManager.inst.renderMiniature(getFileEntryPath(child), (path: String) -> {child.iconPath = path; fancyGallery.queueRefresh();} );
+			}
+		}
+
+		fancyGallery.queueRefresh(Items);
+		fancyGallery.queueRefresh(RegenHeader);
+	}
+
+	override function onDisplay() {
+		root = {
+			name: "res",
+			kind: Dir,
+			children: null,
+			parent: null,
+			iconPath: null,
+		};
+
+		populateChildren(root);
+
+		var layout = new Element('
+			<file-browser>
+				<div class="left"></div>
+				<div class="right" tabindex="-1">
+					<fancy-toolbar class="fancy-small shadow"><fancy-button class="btn-parent quiet" title="Go to parent folder"><fancy-image style="background-image:url(\'res/icons/svg/file_parent.svg\')"></fancy-image></fancy-button><fancy-flex-fill></fancy-flex-fill><fancy-search class="fb-search"></fancy-search></fancy-toolbar>
+					<fancy-gallery></fancy-gallery>
+				</div>
+			</file-browser>
+		').appendTo(element);
+
+		var resize = new hide.comp.ResizablePanel(Horizontal, layout.find(".left"), After);
+
+		var search = new hide.comp.FancySearch(null, layout.find(".fb-search"));
+		search.onSearch = (string, _) -> {
+			searchString = string;
+			onSearch();
+		};
+
+		var btnParent = layout.find(".btn-parent");
+		btnParent.get(0).onclick = (e: js.html.MouseEvent) -> {
+			if (currentFolder.parent != null) {
+				currentFolder = currentFolder.parent;
+				onSearch();
+			}
+		}
+
+		fancyTree = new hide.comp.FancyTree<FileEntry>(resize.element);
+		fancyTree.saveDisplayKey = "fileBrowserTree";
+		fancyTree.getChildren = (file: FileEntry) -> {
+			if (file == null)
+				return [root];
+			if (file.kind == File)
+				return null;
+			if (file.children == null)
+				populateChildren(file);
+			return file.children.filter((file) -> file.kind == Dir);
+		};
+		//fancyTree.hasChildren = (file: FileEntry) -> return file.kind == Dir;
+		fancyTree.getName = (file: FileEntry) -> return file?.name;
+		fancyTree.getIcon = (file: FileEntry) -> return '<div class="ico ico-folder"></div>';
+
+		fancyTree.onNameChange = (item: FileEntry, newName: String) -> {
+			item.name = newName;
+		}
+
+		fancyTree.dragAndDropInterface =
+		{
+			onDragStart: function(file: FileEntry, dataTransfer: js.html.DataTransfer) : Bool {
+				var selection = fancyTree.getSelectedItems();
+				if (selection.length <= 0)
+					return false;
+				var ser = [];
+				for (item in selection) {
+					ser.push(getFileEntryPath(file));
+				}
+				dataTransfer.setData(dragKey, haxe.Json.stringify(ser));
+				return true;
+			},
+			getItemDropFlags: function(target: FileEntry, dataTransfer: js.html.DataTransfer) : hide.comp.FancyTree.DropFlags {
+				var containsFiles = false;
+				if (dataTransfer.types.contains("Files")) {
+					containsFiles = true;
+				}
+				if (dataTransfer.types.contains(dragKey)) {
+					containsFiles = true;
+				}
+
+				if (!containsFiles) {
+					return hide.comp.FancyTree.DropFlags.ofInt(0);
+				}
+
+				if (target.kind == Dir) {
+					return (Reorder:hide.comp.FancyTree.DropFlags) | Reparent;
+				}
+				return Reorder;
+			},
+			onDrop: function(target: FileEntry, operation: hide.comp.FancyTree.DropOperation, dataTransfer: js.html.DataTransfer) : Bool {
+				var files : Array<String> = [];
+				for (file in dataTransfer.files) {
+					var path : String = untyped file.path; //file.path is an extension from nwjs or node
+					path = StringTools.replace(path, "\\", "/");
+					files.push(path);
+				}
+
+				var fileMoveData = dataTransfer.getData(dragKey);
+				if (fileMoveData.length > 0) {
+					try {
+						var unser = haxe.Json.parse(fileMoveData);
+						for (file in (unser:Array<String>)) {
+							files.push(file);
+						}
+					} catch (e) {
+						trace("Invalid data " + e);
+					}
+				}
+
+				return true;
+			}
+		}
+
+		fancyTree.rebuildTree();
+
+		fancyTree.openItem(root);
+
+		currentFolder = root;
+
+		var right = layout.find(".right");
+		right.get(0).onkeydown = (e: js.html.KeyboardEvent) -> {
+			if (hide.ui.Keys.matchJsEvent("search", e, ide.currentConfig)) {
+				e.stopPropagation();
+				e.preventDefault();
+
+				search.focus();
+				return;
+			}
+		}
+
+		fancyGallery = new hide.comp.FancyGallery<FileEntry>(null, layout.find(".right fancy-gallery"));
+		fancyGallery.getItems = () -> {
+			return currentSearch;
+		}
+
+		fancyGallery.getName = (item : FileEntry) -> item.name;
+
+		fancyGallery.getIcon = (item : FileEntry) -> {
+			if (item.kind == Dir) {
+				return '<fancy-image style="background-image:url(\'res/icons/svg/big_folder.svg\')"></fancy-image>';
+
+			}
+			else if (item.iconPath == "loading") {
+				return '<fancy-image class="loading" style="background-image:url(\'res/icons/svg/loading.svg\')"></fancy-image>';
+			}
+			else if (item.iconPath != null) {
+				var url = "file://" + item.iconPath;
+				return '<fancy-image class="thumb" style="background-image:url(\'${url}\')"></fancy-image>';
+			}
+			else {
+				return '<fancy-image style="background-image:url(\'res/icons/svg/file.svg\')"></fancy-image>';
+			}
+		};
+
+		fancyGallery.onDoubleClick = (item: FileEntry) -> {
+			if (item.kind == File) {
+				ide.openFile(getFileEntryPath(item));
+			} else {
+				openDir(item, true);
+			}
+		}
+
+		fancyGallery.visibilityChanged = (item: FileEntry, visible: Bool) -> {
+			var path = getFileEntryPath(item);
+			hide.tools.FileManager.inst.setPriority(path, visible ? 1 : 0);
+		}
+
+		fancyGallery.dragAndDropInterface = {
+			onDragStart: (item: FileEntry, dataTransfer: js.html.DataTransfer) -> {
+				dataTransfer.setData(dragKey, haxe.Json.stringify([getFileEntryPath(item)]));
+				return true;
+			}
+		}
+
+		fancyGallery.rebuild();
+
+
+		fancyTree.onSelectionChanged = () -> {
+			var selection = fancyTree.getSelectedItems();
+
+			if (selection.length > 0) {
+				openDir(selection[0], false);
+			}
+		}
+
+		onSearch();
+	}
+
+	function openDir(item: FileEntry, syncTree: Bool) {
+		if (item.kind == Dir) {
+			currentFolder = item;
+			onSearch();
+		}
+
+		if (syncTree) {
+			fancyTree.selectItem(item, true);
+		}
+	}
+
+	static var _ = hide.ui.View.register(FileBrowser, { width : 350, position : Bottom });
+}

+ 1 - 0
hide/view/GenericGraphEditor.hx

@@ -54,6 +54,7 @@ class GenericGraphEditor extends hide.view.FileView implements IGraphEditor {
 
         // Scene init
         scenePreview = new hide.comp.ScenePreview(config, previewContainer, null, saveDisplayKey + "/scenePreview");
+		  scenePreview.addToolbar();
         scenePreview.element.addClass("scene-preview");
 
         scenePreview.onReady = onScenePreviewReady;

+ 177 - 0
hide/view/Gym.hx

@@ -141,12 +141,189 @@ class Gym extends hide.ui.View<{}> {
 					</fancy-button>
 				</fancy-toolbar>'));
 		}
+
+		{
+			var toolbar = section(element, "Windows");
+
+			var btn = new Element("<fancy-button><span class='label'>Open subwindow 'test'</span></h1>");
+
+			toolbar.append(btn);
+
+			var subwindow : js.html.Window;
+			var scene : hide.comp.Scene;
+
+			btn.on("click", (_) -> {
+				subwindow = js.Browser.window.open("", "test","popup=true");
+
+					var jq = new Element(subwindow.document.body);
+					jq.empty();
+					jq.append(new Element("<p>This is a triumph</p>"));
+					var container = new Element("<div></div>");
+					jq.append(container);
+
+					// var paragraphs = subwindow.document.querySelectorAll("p");
+					// for (p in paragraphs) {
+					// 	p.textContent = "This is the begining of something great";
+					// }
+
+					scene = new hide.comp.Scene(config, container, null);
+
+					scene.onReady = () -> {
+						new h3d.scene.CameraController(scene.s3d);
+						var box = new h3d.scene.Box(scene.s3d);
+						box.material.mainPass.setPassName("overlay");
+
+						var text = new h2d.Text(hxd.res.DefaultFont.get(), scene.s2d);
+						text.text = "Hello world";
+						text.x = 8;
+						text.y = 8;
+					};
+
+					var drag = new Element('<div draggable="true">Drag Me</div>').appendTo(jq);
+					drag.get(0).addEventListener("dragstart", (ev: js.html.DragEvent) -> {
+						ev.dataTransfer.setData("text/plain", "foo");
+						ev.dataTransfer.dropEffect = "copy";
+					});
+			});
+
+			var btn = new Element("<fancy-button><span class='label'>Spawn cube in subwindow</span></h1>");
+			toolbar.append(btn);
+
+			btn.on("click", (_) -> {
+				var box = new h3d.scene.Box(0xFFFFFFFF, scene.s3d);
+				box.setPosition(hxd.Math.random(10),hxd.Math.random(10),hxd.Math.random(10));
+				box.material.mainPass.setPassName("overlay");
+				box.material.color.r = hxd.Math.random();
+				box.material.color.g = hxd.Math.random();
+				box.material.color.b = hxd.Math.random();
+			});
+
+			var dropZone = new Element("<div>Drop something on me from the other window</div>").appendTo(toolbar);
+			dropZone.get(0).addEventListener("drop", (ev : js.html.DragEvent) -> {
+				ev.preventDefault();
+				var data = ev.dataTransfer.getData("text/plain");
+				dropZone.text(data);
+			});
+
+			dropZone.get(0).addEventListener("dragover", (ev : js.html.DragEvent) -> {
+				ev.preventDefault();
+				ev.dataTransfer.dropEffect = "copy";
+			});
+
+			var btn2 = new Element("<fancy-button><span class='label'>Localhost 5500</span></h1>").appendTo(toolbar);
+			btn2.on("click", (_) -> {
+				subwindow = js.Browser.window.open("http://127.0.0.1:5500/", "test","popup=true");
+			});
+		}
+
+
+		{
+			var toolbar = section(element, "Offscreen Rendering");
+			var btn = new Element("<fancy-button><span class='label'>Render test.thumb.png</span>").appendTo(toolbar);
+
+			btn.get(0).onclick = (e) -> {
+				var fm = hide.tools.FileManager.inst;
+			}
+
+			var btn = new Element("<fancy-button><span class='label'>Test thumbnail generator</span>").appendTo(toolbar);
+
+			var sub : js.node.child_process.ChildProcess = null;
+
+			var remoteSocket : hxd.net.Socket = null;
+
+			btn.get(0).onclick = (e) -> {
+
+				if (sock != null) {
+					sock.close();
+				}
+
+				sock = new hxd.net.Socket();
+
+				sock.onError = (msg) -> {
+					trace("Socket error " + msg);
+				}
+
+				sock.onData = () -> {
+					trace("sock.onData");
+					while(sock.input.available > 0) {
+						var data = sock.input.readLine().toString();
+
+						trace("recieved data sock.onData", data);
+					}
+				}
+
+				sock.bind("localhost", 9669, (rs: hxd.net.Socket) -> {
+					trace("new connexion");
+					remoteSocket = rs;
+
+					remoteSocket.onError = (msg) -> {
+						trace("Socket error " + msg);
+					}
+
+					remoteSocket.onData = () -> {
+						trace("rawsocket.onData");
+
+						while(remoteSocket.input.available > 0) {
+							var data = remoteSocket.input.readLine().toString();
+
+							trace("recieved data", data);
+						}
+					}
+				});
+
+				nw.Window.open('app.html?thumbnail=true', {new_instance: true}, (win: nw.Window) -> {
+					win.on("close", () -> {
+						sock.close();
+						sock = null;
+					});
+				});
+			}
+
+			var btn = new Element("<fancy-button><span class='label'>Send message</span>").appendTo(toolbar);
+			btn.get(0).onclick = (e) -> {
+				remoteSocket.out.writeString("Test message\n");
+			}
+
+			var btn = new Element("<fancy-button><span class='label'>Rethrow test</span>").appendTo(toolbar);
+			btn.get(0).onclick = (e) -> {
+				rethrowTest1();
+			}
+		}
+	}
+
+	function rethrowTest1() {
+		try {
+			rethrowTest2();
+		} catch (e) {
+			js.Lib.rethrow();
+		}
 	}
 
+	function rethrowTest2() {
+		try {
+			rethrowTest3();
+		} catch(e) {
+			js.Lib.rethrow();
+		}
+	}
+
+	function rethrowTest3() {
+		throw "Error Lol";
+	}
+
+	var subwin: js.html.Window;
+	static var sock: hxd.net.Socket;
+
+
 	static function section(parent: Element, name: String) : Element {
 		return new Element('<details><summary>$name</summary></details>').appendTo(parent);
 	}
 
+	static public function onBeforeReload() {
+		sock?.close();
+		sock = null;
+	}
+
 	static function getContextMenuContent() : Array<hide.comp.ContextMenu.MenuItem> {
 
 		var radioState = 0;

+ 17 - 0
hide/view/Inspector.hx

@@ -0,0 +1,17 @@
+package hide.view;
+
+typedef InspectorState = {
+
+}
+
+class Inspector extends hide.ui.View<InspectorState> {
+
+	override function new(state) {
+		super(state);
+	}
+	override function onDisplay() {
+		element.html("<p>Hello world</p>");
+	}
+
+	static var _ = hide.ui.View.register(Inspector, { width : 350, position : Right, id: "inspector" });
+}

+ 9 - 0
hide/view/Prefab.hx

@@ -242,6 +242,13 @@ class Prefab extends hide.view.FileView {
 		sceneReadyDelayed.empty();
 	}
 
+	override function onHide() {
+		super.onHide();
+
+		//ide.closeInspector();
+
+	}
+
 	override function onDisplay() {
 		if( sceneEditor != null ) sceneEditor.dispose();
 		createData();
@@ -391,6 +398,8 @@ class Prefab extends hide.view.FileView {
 			tools.refreshToggles();
 
 		setRenderPropsEditionVisibility(Ide.inst.currentConfig.get("sceneeditor.renderprops.edit", false));
+
+		//ide.getOrOpenInspector();
 	}
 
 	public function hideColumns(?_) {

+ 2 - 0
hide/view/RemoteConsoleView.hx

@@ -180,6 +180,8 @@ class RemoteConsoleView extends hide.ui.View<{}> {
 				haxe.Timer.delay(wait, 10);
 				return;
 			}
+			if (Ide.inst.thumbnailMode)
+				return;
 			var config = Ide.inst.config.project;
 			var pconfig = config.get("remoteconsole");
 			if( pconfig != null && pconfig.disableAutoStartServer != true ) {

+ 1 - 0
hide/view/animgraph/BlendSpace2DEditor.hx

@@ -300,6 +300,7 @@ class BlendSpace2DEditor extends hide.view.FileView {
 			panel.onResize = refreshGraph;
 
 			scenePreview = new hide.comp.ScenePreview(config, previewContainer, null, saveDisplayKey + "/preview");
+			scenePreview.addToolbar();
 			scenePreview.listLoadableMeshes = () -> {
 				var ret : Array<{label: String, path: String}> = [];
 				var list = AnimGraphEditor.gatherAllPreviewModels(blendSpace2D.animFolder);

+ 1 - 2
hrt/prefab/Model.hx

@@ -55,8 +55,7 @@ class Model extends Object3D {
 			return obj;
 		#if editor
 		} catch( e : Dynamic ) {
-			e.message = "Could not load model " + source + ": " + e.message;
-			shared.onError(e);
+			hide.Ide.inst.quickError("Could not load model " + source + ": " + e.message);
 		}
 		#end
 		return new h3d.scene.Object(parent3d);

+ 1 - 1
hrt/prefab/fx/FX.hx

@@ -301,7 +301,7 @@ class FXAnimation extends h3d.scene.Object {
 						var visible = anim.elt.visible;
 						#if editor
 						var editor = anim.elt.shared.editor;
-						visible = visible && editor.isVisible(anim.elt);
+						visible = visible && (editor?.isVisible(anim.elt) ?? true);
 						#end
 						anim.obj.visible = visible && evaluator.getFloat(anim.visibility, time) > 0.5;
 					}

+ 5 - 2
hrt/prefab/l3d/Trails.hx

@@ -233,8 +233,11 @@ class TrailObj extends h3d.scene.Mesh {
 				fxAnim.push(fx);
 			p = p.parent;
 		}
-		for ( fx in fxAnim )
-			fx.trails.remove(this);
+		for ( fx in fxAnim ) {
+			if (fx.trails != null) {
+				fx.trails.remove(this);
+			}
+		}
 		dprim.dispose();
 	}
 

+ 3 - 1
libs/golden/ContentItem.hx

@@ -9,6 +9,7 @@ extern class ContentItem {
 	var childElementContainer : Container;
 	var config : Config.ItemConfig;
 	var header : Header;
+	var id : String;
 
 	var __view : Dynamic;
 
@@ -19,5 +20,6 @@ extern class ContentItem {
 	public function getItemsByFilter( f : ContentItem -> Bool ) : Array<ContentItem>;
 	public function getActiveContentItem() : ContentItem;
 	public function setActiveContentItem( item : ContentItem ) : Void;
-
+	public function getItemsById(id : String) : Array<ContentItem>;
+	public function remove() : Void ;
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików