techkaczynski.dev / code

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortech kaczynski <tk@techkaczynski.dev>2026-09-22 22:38:57 +1000
committertech kaczynski <tk@techkaczynski.dev>2026-09-22 22:38:57 +1000
commit08cb8d6a585dd552b69a488d9db1be123f01467a (patch)
tree77d50ce30bbc102b08486929ba654b5bef70af43
initial commit.
-rw-r--r--LICENSE675
-rw-r--r--README6
-rw-r--r--app.c132
-rw-r--r--app.h33
-rw-r--r--b3internalassert.c9
-rw-r--r--cam.c88
-rw-r--r--cam.h24
-rw-r--r--common.h111
-rw-r--r--editor.c1744
-rw-r--r--editor.h99
-rw-r--r--ent.c814
-rw-r--r--ent.h96
-rw-r--r--env.c384
-rw-r--r--env.h46
-rw-r--r--ext/fuzzy_match.c158
-rw-r--r--ext/fuzzy_match.h8
-rw-r--r--ext/hashmap.c193
-rw-r--r--ext/hashmap.h77
-rw-r--r--ext/tinyfiledialogs/README.txt233
-rw-r--r--ext/tinyfiledialogs/hello.c307
-rw-r--r--ext/tinyfiledialogs/hello_wchar_t.c241
-rw-r--r--ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.c259
-rw-r--r--ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.h48
-rw-r--r--ext/tinyfiledialogs/tinyfiledialogs.c8401
-rw-r--r--ext/tinyfiledialogs/tinyfiledialogs.h314
-rw-r--r--game.c46
-rw-r--r--game.h31
-rw-r--r--physics.c40
-rw-r--r--physics.h156
-rw-r--r--raygui.h6319
-rw-r--r--res.c251
-rw-r--r--res.h43
-rw-r--r--res/fnt/Aileron-Regular.otfbin0 -> 27644 bytes
-rw-r--r--res/fnt/Inter-Regular.ttfbin0 -> 411640 bytes
-rw-r--r--res/mdl/Car.glbbin0 -> 42936 bytes
-rw-r--r--res/mdl/Car2.glbbin0 -> 40648 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard.glbbin0 -> 75648 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard_light.glbbin0 -> 75652 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/retro_bollard.001.glbbin0 -> 80392 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/retro_bollard.glbbin0 -> 76184 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/retro_bollard_detailed.glbbin0 -> 80396 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/security_bollard.glbbin0 -> 69456 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/simple_bollard.glbbin0 -> 69452 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/simple_bollard_short.glbbin0 -> 69460 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/square_bollard.glbbin0 -> 73476 bytes
-rw-r--r--res/mdl/Cones & Barriers/Bollards/square_bollard_reflectors.glbbin0 -> 74000 bytes
-rw-r--r--res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier.glbbin0 -> 42652 bytes
-rw-r--r--res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center.glbbin0 -> 47420 bytes
-rw-r--r--res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center_dirty.glbbin0 -> 47780 bytes
-rw-r--r--res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge.glbbin0 -> 48220 bytes
-rw-r--r--res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge_dirty.glbbin0 -> 48448 bytes
-rw-r--r--res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_dirty.glbbin0 -> 42848 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Construction_cone._thin.glbbin0 -> 38044 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Construction_cone._thin_rubber.glbbin0 -> 38048 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Construction_cone.glbbin0 -> 37908 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Construction_cone_rubber.glbbin0 -> 37916 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Traffic_cone_round.glbbin0 -> 38908 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Traffic_cone_round_rubber.glbbin0 -> 38592 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Traffic_cone_square.glbbin0 -> 37100 bytes
-rw-r--r--res/mdl/Cones & Barriers/Cones/Traffic_cone_square_rubber.glbbin0 -> 37104 bytes
-rw-r--r--res/mdl/Cones & Barriers/Textures/bollards.pngbin0 -> 64222 bytes
-rw-r--r--res/mdl/Cones & Barriers/Textures/concrete_barriers.pngbin0 -> 39044 bytes
-rw-r--r--res/mdl/Cones & Barriers/Textures/cones.pngbin0 -> 32058 bytes
-rw-r--r--res/mdl/Cones & Barriers/Textures/type_barriers.pngbin0 -> 38061 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top.glbbin0 -> 44468 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom.glbbin0 -> 45896 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom_wood.glbbin0 -> 46160 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.fbxbin0 -> 58876 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.glbbin0 -> 45576 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.fbxbin0 -> 59036 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.glbbin0 -> 46376 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.fbxbin0 -> 59068 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.glbbin0 -> 45540 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.fbxbin0 -> 57308 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.glbbin0 -> 40260 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.fbxbin0 -> 58364 bytes
-rw-r--r--res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.glbbin0 -> 43852 bytes
-rw-r--r--res/mdl/PSX-Waste/glb/barrel.glbbin0 -> 48680 bytes
-rw-r--r--res/mdl/PSX-Waste/glb/cardboard-box.glbbin0 -> 11928 bytes
-rw-r--r--res/mdl/PSX-Waste/glb/crumpled-can.glbbin0 -> 6888 bytes
-rw-r--r--res/mdl/PSX-Waste/glb/dumpster.glbbin0 -> 24976 bytes
-rw-r--r--res/mdl/PSX-Waste/glb/glass-bottle.glbbin0 -> 10168 bytes
-rw-r--r--res/mdl/PSX-Waste/glb/trash-bag.glbbin0 -> 9484 bytes
-rw-r--r--res/mdl/PSX-Waste/textures/barrel256x256.pngbin0 -> 44820 bytes
-rw-r--r--res/mdl/PSX-Waste/textures/can128x128.pngbin0 -> 3658 bytes
-rw-r--r--res/mdl/PSX-Waste/textures/cardboard-box256x256.pngbin0 -> 9770 bytes
-rw-r--r--res/mdl/PSX-Waste/textures/dumpster256x256.pngbin0 -> 20624 bytes
-rw-r--r--res/mdl/PSX-Waste/textures/glass-bottle128x128.pngbin0 -> 5166 bytes
-rw-r--r--res/mdl/PSX-Waste/textures/trash-bag128x128.pngbin0 -> 5133 bytes
-rw-r--r--res/mdl/female_gunner_003.glbbin0 -> 783652 bytes
-rw-r--r--res/mdl/female_gunner_003_posed.glbbin0 -> 176456 bytes
-rw-r--r--res/mdl/female_gunner_004.glbbin0 -> 783856 bytes
-rw-r--r--res/mdl/lighthouse.glbbin0 -> 90388 bytes
-rw-r--r--res/mdl/male_casual.glbbin0 -> 189760 bytes
-rw-r--r--res/mdl/map_prototype.glbbin0 -> 2185768 bytes
-rw-r--r--res/mdl/map_prototype_orig.glbbin0 -> 2682220 bytes
-rw-r--r--res/save/scene.fgsbin0 -> 1552 bytes
-rw-r--r--res/sdr/mdl_lit.fs69
-rw-r--r--res/sdr/mdl_lit.vs28
-rw-r--r--res/sdr/mdl_vtx_lit.fs29
-rw-r--r--res/sdr/mdl_vtx_lit.vs64
-rw-r--r--res/sdr/ocean.fs264
-rw-r--r--res/sdr/ocean.vs16
-rw-r--r--res/sdr/sky.fs72
-rw-r--r--res/sdr/sky_halo_old.fs279
-rw-r--r--res/sdr/sky_old.fs92
-rw-r--r--res/tex/moon.pngbin0 -> 36095 bytes
-rw-r--r--res/tex/sand.pngbin0 -> 1930220 bytes
-rw-r--r--res/tex/sand2.pngbin0 -> 43585 bytes
-rw-r--r--res/tex/sun.pngbin0 -> 56174 bytes
-rw-r--r--res/tex/water.pngbin0 -> 45017 bytes
-rw-r--r--save.c1056
-rw-r--r--save.h13
113 files changed, 23368 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..53d1f3d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,675 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
+
diff --git a/README b/README
new file mode 100644
index 0000000..cd5864e
--- /dev/null
+++ b/README
@@ -0,0 +1,6 @@
+# FUN GAME
+
+Fun game is an open world immersive sim sandbox game about a 29 year old game
+developer who feels compelled to "make it" before he turns 30. The game comes
+bundled with engine and editor tooling so that anyone can edit, modify, learn
+from and even publish their own projects.
diff --git a/app.c b/app.c
new file mode 100644
index 0000000..bc82a3b
--- /dev/null
+++ b/app.c
@@ -0,0 +1,132 @@
+#include "app.h"
+#include "physics.h"
+#include "res.h"
+#include "ent.h"
+#include "game.h"
+#include "env.h"
+#include "cam.h"
+#include "editor.h"
+
+#define RAYGUI_IMPLEMENTATION
+#include "raygui.h"
+
+App app = {0};
+u64 dude_id = ENTITY_INVALID_IDX;
+u64 car_id = ENTITY_INVALID_IDX;
+
+static void
+app_init(void) {
+ SetTraceLogLevel(LOG_WARNING);
+ SetConfigFlags(FLAG_MSAA_4X_HINT |
+ FLAG_VSYNC_HINT |
+ FLAG_WINDOW_RESIZABLE);
+
+ InitWindow(1280, 720, "Fun Game");
+ SetExitKey(KEY_NULL);
+ SetTargetFPS(60);
+ app.run = true;
+
+ FLAG_SET(app.flags, BIT(APP_FLAG_EDIT));
+ FLAG_SET(app.flags, BIT(APP_FLAG_DEBUG));
+
+ physics_init();
+ res_init();
+ env_init();
+ entity_system_init();
+ game_init();
+ editor_init();
+ cam_init();
+
+ // for (int i = 0; i < 10000; ++i) {
+ // u64 id = entity_create(false);
+ // Entity *ent = entity_get(id);
+
+ // char name[16];
+ // snprintf(name, 16, "Entity %i", i);
+ // free(ent->name);
+ // ent->name = strdup(name);
+
+ // ent->tr.pos = (Vec3){
+ // GetRandomValue(-50, 50),
+ // GetRandomValue(-10, 10),
+ // GetRandomValue(-50, 50),
+ // };
+ // }
+
+ // for (int i = 0; i < entity_count(); ++i) {
+ // u64 id = entity_id_at(i);
+
+ // if (i == 5) {
+ // u64 parent_id = entity_id_at(4);
+ // entity_set_parent(id, parent_id);
+ // }
+
+ // if (i == 10) {
+ // entity_delete(id);
+ // }
+ // }
+}
+
+static void
+app_exit(void) {
+ editor_exit();
+ game_exit();
+ entity_system_exit();
+ env_exit();
+ res_exit();
+ physics_exit();
+}
+
+static void
+app_loop(void) {
+ app.run = !WindowShouldClose();
+
+ /* this is some hypothetical bullshit, probably will change */
+ if (app.mode != APP_MODE_PLAY) {
+ editor_update();
+ }
+
+ physics_update();
+ res_update();
+ env_update();
+ entity_system_update();
+
+ BeginDrawing();
+ ClearBackground(RAYWHITE);
+ env_draw_sky(main_cam.cam);
+
+ BeginMode3D(main_cam.cam);
+ entity_system_draw_3d();
+
+ // delete this shit later
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Matrix world = entity_world_matrix(entity_id_at(i));
+
+ rlPushMatrix();
+ rlMultMatrixf(MatrixToFloatV(world).v);
+ DrawCubeWiresV(Vector3Zero(), Vector3One(), GREEN);
+ rlPopMatrix();
+ }
+ EndMode3D();
+
+ if (FLAG_CHECK(app.flags, BIT(APP_FLAG_DEBUG)) ||
+ FLAG_CHECK(app.flags, BIT(APP_FLAG_EDIT))) {
+ BeginMode3D(main_cam.cam);
+ rlDisableDepthTest();
+ editor_draw_3d();
+ EndMode3D();
+ }
+
+ editor_draw_ui();
+ EndDrawing();
+}
+
+int
+main(void) {
+ app_init();
+ while (app.run) {
+ app_loop();
+ }
+ app_exit();
+ return 0;
+}
diff --git a/app.h b/app.h
new file mode 100644
index 0000000..b39ae56
--- /dev/null
+++ b/app.h
@@ -0,0 +1,33 @@
+#ifndef APP_H
+#define APP_H
+
+#include "common.h"
+
+typedef enum {
+ APP_MODE_EDIT_NONE,
+ APP_MODE_EDIT_SELECT,
+ APP_MODE_EDIT_CTRL,
+ APP_MODE_EDIT_MOVE,
+ APP_MODE_EDIT_ROTATE,
+ APP_MODE_EDIT_SPACE,
+ APP_MODE_EDIT_CMD,
+ APP_MODE_PLAY,
+} AppMode;
+
+typedef enum {
+ APP_FLAG_EDIT,
+ APP_FLAG_DEBUG,
+} AppFlag;
+
+typedef struct {
+ int w, h;
+ AppMode mode;
+ AppFlag flags;
+ bool run;
+} App;
+
+extern App app;
+
+void app_notify(cstr title, cstr msg);
+
+#endif
diff --git a/b3internalassert.c b/b3internalassert.c
new file mode 100644
index 0000000..5beecbe
--- /dev/null
+++ b/b3internalassert.c
@@ -0,0 +1,9 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+/* deepseek gave me this when I pasted errors from cproc C compiler */
+int b3InternalAssert(const char* condition, const char* fileName, int lineNumber) {
+ fprintf(stderr, "Box3D Assertion Failed: (%s) in %s at line %d\n",
+ condition, fileName, lineNumber);
+ return 1;
+}
diff --git a/cam.c b/cam.c
new file mode 100644
index 0000000..f14487c
--- /dev/null
+++ b/cam.c
@@ -0,0 +1,88 @@
+#include "cam.h"
+#include <rcamera.h>
+
+CameraInfo main_cam = {0};
+
+void
+cam_init(void) {
+ rlSetClipPlanes(0.3, 5000);
+ main_cam.cam = (Camera3D) {
+ .position = { 0, 10, -10 },
+ .target = { 0, 1, 0 },
+ .up = { 0, 1, 0},
+ .fovy = 70.0f,
+ .projection = CAMERA_PERSPECTIVE,
+ };
+ main_cam.update = false;
+ main_cam.mv_speed = 10;
+ main_cam.mv_scale = 1;
+ main_cam.lk_speed = 0.2;
+}
+
+void
+cam_ctrl_editor(void) {
+ if (!main_cam.update) {
+ return;
+ }
+
+ Vec3 input_dir = {0};
+ f32 mv_speed = main_cam.mv_speed;
+ f32 lk_speed = main_cam.lk_speed;
+
+ if (IsKeyDown(KEY_W)) {
+ input_dir.z += 1;
+ }
+
+ if (IsKeyDown(KEY_A)) {
+ input_dir.x -= 1;
+ }
+
+ if (IsKeyDown(KEY_S)) {
+ input_dir.z -= 1;
+ }
+
+ if (IsKeyDown(KEY_D)) {
+ input_dir.x += 1;
+ }
+
+ if (IsKeyDown(KEY_Q)) {
+ input_dir.y -= 1;
+ }
+
+ if (IsKeyDown(KEY_E)) {
+ input_dir.y += 1;
+ }
+
+ input_dir = Vector3Normalize(input_dir);
+
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ mv_speed *= 3;
+ }
+
+ // makes it so you can stop faster than you can speed up
+ f32 ms_amount = GetMouseWheelMove() < 0 ?
+ GetMouseWheelMove() * 1.5 :
+ GetMouseWheelMove() * 0.75;
+
+ main_cam.mv_scale += ms_amount;
+ main_cam.mv_scale = CLAMP(main_cam.mv_scale, 0.1, 50);
+
+ mv_speed *= main_cam.mv_scale;
+ mv_speed = CLAMP(mv_speed, 2, 1000);
+
+ CameraMoveForward(&main_cam.cam,
+ input_dir.z * mv_speed * GetFrameTime(),
+ false);
+
+ CameraMoveRight(&main_cam.cam,
+ input_dir.x * mv_speed * GetFrameTime(),
+ false);
+
+ CameraMoveUp(&main_cam.cam, input_dir.y * mv_speed * GetFrameTime());
+
+ CameraYaw(&main_cam.cam,
+ -GetMouseDelta().x * lk_speed * GetFrameTime(), 0);
+
+ CameraPitch(&main_cam.cam,
+ -GetMouseDelta().y * lk_speed * GetFrameTime(), 1, 0, 0);
+}
diff --git a/cam.h b/cam.h
new file mode 100644
index 0000000..4b2465b
--- /dev/null
+++ b/cam.h
@@ -0,0 +1,24 @@
+#ifndef CAM_H
+#define CAM_H
+
+#include "common.h"
+
+typedef enum {
+ CAM_MODE_NONE,
+ CAM_MODE_EDIT_FLY,
+ // CAM_MODE_EDIT_ORTHO,
+} CamState;
+
+typedef struct {
+ Camera cam;
+ CamState state;
+ f32 mv_speed, mv_scale, lk_speed;
+ bool update;
+} CameraInfo;
+
+extern CameraInfo main_cam;
+
+void cam_init(void);
+void cam_ctrl_editor(void);
+
+#endif
diff --git a/common.h b/common.h
new file mode 100644
index 0000000..a33e3c5
--- /dev/null
+++ b/common.h
@@ -0,0 +1,111 @@
+#ifndef COMMON_H
+#define COMMON_H
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <float.h>
+
+#include <raylib.h>
+#include <raymath.h>
+#include <rlgl.h>
+
+#include <box3d/box3d.h>
+#include <box3d/collision.h>
+#include <box3d/math_functions.h>
+#include <box3d/types.h>
+
+#include <luajit-2.1/luajit.h>
+
+#include "ext/hashmap.h"
+#include "ext/fuzzy_match.h"
+#include "ext/tinyfiledialogs/tinyfiledialogs.h"
+
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+#define CLAMP(x, lo, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))
+
+#define BIT(x) (1ULL << (x))
+#define FLAG_SET(n, f) ((n) |= (f))
+#define FLAG_CLEAR(n, f) ((n) &= ~(f))
+#define FLAG_TOGGLE(n, f) ((n) ^= (f))
+#define FLAG_CHECK(n, f) ((n) & (f))
+
+#define BYTES_TO_KB(b) ((b) / 1024)
+#define BYTES_TO_MB(b) ((b) / (1024 * 1024))
+#define KILOBYTES(kb) ((kb) * 1024)
+#define MEGABYTES(mb) ((mb) * 1024 * 1024)
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+
+typedef int8_t i8;
+typedef int16_t i16;
+typedef int32_t i32;
+typedef int64_t i64;
+
+typedef float f32;
+typedef double f64;
+typedef const char *cstr;
+
+typedef u8 Flags8;
+typedef u32 Flags32;
+typedef u64 Flags64;
+
+typedef Rectangle Rect;
+typedef Vector2 Vec2;
+typedef Vector3 Vec3;
+
+/* POSIX/LINUX SPECIFIC PERFORMANCE MEASURE STUFF, DELETE LATER */
+#include <time.h>
+
+static inline double
+timespec_seconds(struct timespec start, struct timespec end) {
+ return (double)(end.tv_sec - start.tv_sec) +
+ (double)(end.tv_nsec - start.tv_nsec) / 1000000000.0;
+}
+
+#define PERF(...) \
+ do { \
+ struct timespec perf_start_; \
+ struct timespec perf_end_; \
+ double perf_elapsed_; \
+ \
+ clock_gettime(CLOCK_MONOTONIC, &perf_start_); \
+ __VA_ARGS__; \
+ clock_gettime(CLOCK_MONOTONIC, &perf_end_); \
+ \
+ perf_elapsed_ = timespec_seconds(perf_start_, perf_end_); \
+ fprintf(stderr, "%s: %.9f s (%.3f ms)\n", #__VA_ARGS__, perf_elapsed_, \
+ perf_elapsed_ * 1000.0); \
+ } while (0)
+
+#define PERF_N(count, ...) \
+ do { \
+ struct timespec perf_start_; \
+ struct timespec perf_end_; \
+ double perf_elapsed_; \
+ size_t perf_count_ = (count); \
+ \
+ clock_gettime(CLOCK_MONOTONIC, &perf_start_); \
+ for (size_t perf_i_ = 0; perf_i_ < perf_count_; perf_i_++) { \
+ __VA_ARGS__; \
+ } \
+ clock_gettime(CLOCK_MONOTONIC, &perf_end_); \
+ \
+ perf_elapsed_ = timespec_seconds(perf_start_, perf_end_); \
+ fprintf(stderr, \
+ "%s:\n" \
+ "\t%zu calls\n" \
+ "\t%.9f s total\n" \
+ "\t%.3f ns per call\n", \
+ #__VA_ARGS__, perf_count_, perf_elapsed_, \
+ perf_elapsed_ * 1000000000.0 / (double)perf_count_); \
+ } while (0)
+
+#endif
diff --git a/editor.c b/editor.c
new file mode 100644
index 0000000..6550ecf
--- /dev/null
+++ b/editor.c
@@ -0,0 +1,1744 @@
+#include "editor.h"
+#include "save.h"
+#include "cam.h"
+#include "physics.h"
+#include "ent.h"
+#include "app.h"
+#include "res.h"
+#include "raygui.h"
+
+static bool started = false;
+static Vec2 start_point = {0};
+static Vec2 end_point = {0};
+static Rect sel_rec = {0};
+static f32 sel_dist = 0;
+static EditorAction tr_action = {0};
+
+EditorInfo ed = {
+ .panel = {
+ [ED_PNL_LEFT] = {
+ .type = ED_PNL_TYPE_SCENE,
+ // .enabled = true,
+ },
+ [ED_PNL_RIGHT] = {
+ .type = ED_PNL_TYPE_PROPERTIES,
+ },
+ [ED_PNL_CENTER] = {
+ .type = ED_PNL_TYPE_SPACE,
+ },
+ },
+};
+
+/* forward decl */
+static bool selection_root(Entity *ent);
+static void action_apply(EditorAction*, bool);
+static void action_free(EditorAction*);
+
+static void
+history_push(EditorAction action) {
+ if (ed.history_save_csr > ed.history_csr) {
+ ed.history_save_csr = UINT32_MAX;
+ }
+
+ /* discard redo branch */
+ while (ed.history_cnt > ed.history_csr) {
+ action_free(&ed.history[--ed.history_cnt]);
+ }
+
+ if (ed.history_cnt == ed.history_cap) {
+ u32 cap = ed.history_cap ? ed.history_cap * 2 : 64;
+ void *history = realloc(ed.history, cap * sizeof(action));
+ if (!history) {
+ perror("editor history alloc failed.");
+ exit(1);
+ }
+ ed.history = history;
+ ed.history_cap = cap;
+ }
+
+ ed.history[ed.history_cnt++] = action;
+ ed.history_csr = ed.history_cnt;
+}
+
+static void
+history_undo(void) {
+ if (ed.history_csr > 0) {
+ action_apply(&ed.history[ed.history_csr - 1], true);
+ ed.history_csr -= 1;
+ }
+}
+
+static void
+history_redo(void) {
+ if (ed.history_csr < ed.history_cnt) {
+ action_apply(&ed.history[ed.history_csr], false);
+ ed.history_csr += 1;
+ }
+}
+
+static void
+history_clear(void) {
+ for (u32 i = 0; i < ed.history_cnt; ++i) {
+ action_free(&ed.history[i]);
+ }
+
+ ed.history_cnt = 0;
+ ed.history_csr = 0;
+ ed.history_save_csr = UINT32_MAX;
+}
+
+static void
+action_entity_extract(EditorAction *action) {
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ Entity *held = &action->item[i].before.stored.ent;
+ assert(!held->pid);
+
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ *held = entity_extract_now(id);
+ }
+}
+
+static void
+action_entity_restore(EditorAction *action) {
+ /* Restore all entities before resolving their relationships. */
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ Entity *held = &action->item[i].before.stored.ent;
+ assert(held->pid == action->item[i].pid);
+
+ u64 id = entity_restore(action->item[i].pid);
+ assert(id);
+
+ *entity_get(id) = *held;
+ *held = (Entity){0};
+ }
+
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ const EditorActionData *data = &action->item[i].before;
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ Entity *ent = entity_get(id);
+
+ ent->tr.parent = entity_id_from_pid(data->stored.parent);
+ ent->tr.prev = entity_id_from_pid(data->stored.prev);
+ ent->tr.next = entity_id_from_pid(data->stored.next);
+
+ if (ent->tr.prev) {
+ entity_get(ent->tr.prev)->tr.next = id;
+ } else if (ent->tr.parent) {
+ entity_get(ent->tr.parent)->tr.first_child = id;
+ }
+
+ if (ent->tr.next) {
+ entity_get(ent->tr.next)->tr.prev = id;
+ }
+ }
+}
+
+/* collects PIDs, recursive visits children first. */
+static void
+action_collect(EditorAction *action, u64 id, bool recursive) {
+ Entity *ent = entity_get(id);
+ assert(ent && ent->pid);
+
+ if (recursive) {
+ for (u64 child = ent->tr.first_child; child;
+ child = entity_get(child)->tr.next) {
+ action_collect(action, child, true);
+ }
+ }
+
+ u32 i = action->item_cnt++;
+
+ /* First pass counts, second pass fills the allocated array. */
+ if (action->item) {
+ action->item[i].pid = ent->pid;
+ }
+}
+
+static EditorAction
+action_begin(EditorActionTag tag) {
+ EditorAction action = { .tag = tag };
+
+ bool roots_only = false;
+ bool recursive = false;
+
+ switch (tag) {
+ case ED_ACTION_TRANSFORM:
+ roots_only = true;
+ break;
+
+ case ED_ACTION_CREATE:
+ action.item_cnt = 1;
+ action.item = calloc(1, sizeof(*action.item));
+ if (!action.item) {
+ perror("action_begin: create entity alloc failed.");
+ exit(1);
+ }
+
+ u64 id = entity_create(false);
+ Entity *ent = entity_get(id);
+ action.item[0].pid = ent->pid;
+ entities_clear_selected();
+ ent->ed.selected = true;
+ return action;
+
+ case ED_ACTION_DELETE:
+ roots_only = true;
+ recursive = true;
+ break;
+
+ case ED_ACTION_RENAME:
+ break;
+
+ default:
+ TraceLog(LOG_FATAL, "action_begin: unsupported tag.");
+ }
+
+ /* Count, allocate, then collect PIDs. */
+ for (u32 pass = 0; pass < 2; ++pass) {
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+ if (!ent || !ent->pid) {
+ continue;
+ }
+
+ if (roots_only ?
+ !selection_root(ent) :
+ !ent->ed.selected) {
+ continue;
+ }
+ action_collect(&action, entity_id_at(i), recursive);
+ }
+ if (pass == 0) {
+ if (!action.item_cnt) {
+ return action;
+ }
+ action.item = calloc(action.item_cnt,
+ sizeof(*action.item));
+ if (!action.item) {
+ perror("editor action alloc failed.");
+ exit(1);
+ }
+ action.item_cnt = 0;
+ }
+ }
+ /* Capture action-specific data for the collected entities. */
+ for (u32 i = 0; i < action.item_cnt; ++i) {
+ Entity *ent = entity_get(
+ entity_id_from_pid(action.item[i].pid));
+ assert(ent);
+ EditorActionData *before = &action.item[i].before;
+ switch (tag) {
+ case ED_ACTION_TRANSFORM:
+ before->transform.pos = ent->tr.pos;
+ before->transform.rot = ent->tr.rot;
+ before->transform.scale = ent->tr.scale;
+ break;
+ case ED_ACTION_DELETE: {
+ Entity *parent = entity_get(ent->tr.parent);
+ Entity *prev = entity_get(ent->tr.prev);
+ Entity *next = entity_get(ent->tr.next);
+ assert(!parent || parent->pid);
+ assert(!prev || prev->pid);
+ assert(!next || next->pid);
+ before->stored.parent = parent ? parent->pid : 0;
+ before->stored.prev = prev ? prev->pid : 0;
+ before->stored.next = next ? next->pid : 0;
+ break;
+ }
+ case ED_ACTION_RENAME:
+ // Name ownership will transfer when the rename is submitted.
+ break;
+ default:
+ TraceLog(LOG_FATAL,
+ "action_begin: unhandled action tag.");
+ break;
+ }
+ }
+
+ return action;
+}
+
+static void
+action_apply(EditorAction *action, bool undo) {
+ switch (action->tag) {
+ case ED_ACTION_TRANSFORM:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ const EditorActionData *data = undo ?
+ &action->item[i].before :
+ &action->item[i].after;
+
+ ent->tr.pos = data->transform.pos;
+ ent->tr.rot = data->transform.rot;
+ ent->tr.scale = data->transform.scale;
+ }
+ break;
+
+ case ED_ACTION_CREATE:
+ if (undo) {
+ action_entity_extract(action);
+ } else {
+ action_entity_restore(action);
+ }
+ break;
+
+ case ED_ACTION_DELETE:
+ if (undo) {
+ action_entity_restore(action);
+ } else {
+ action_entity_extract(action);
+ }
+ break;
+
+ case ED_ACTION_RENAME:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ char *name = ent->name;
+ ent->name = action->item[i].before.rename.name;
+ action->item[i].before.rename.name = name;
+ }
+ break;
+
+ default:
+ TraceLog(LOG_FATAL, "action with invalid tag.");
+ break;
+ }
+}
+
+static void
+action_free(EditorAction *action) {
+ switch (action->tag) {
+ case ED_ACTION_TRANSFORM:
+ break;
+
+ case ED_ACTION_CREATE:
+ case ED_ACTION_DELETE:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ entity_clean(&action->item[i].before.stored.ent);
+ }
+ break;
+
+ case ED_ACTION_RENAME:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ free(action->item[i].before.rename.name);
+ }
+ break;
+
+ default:
+ TraceLog(LOG_FATAL, "action_free: unhandled action tag.");
+ break;
+ }
+ free(action->item);
+}
+
+static void
+action_commit(EditorAction *action) {
+ bool changed = false;
+
+ switch (action->tag) {
+ case ED_ACTION_TRANSFORM:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ const EditorActionData *before =
+ &action->item[i].before;
+ EditorActionData *after = &action->item[i].after;
+ after->transform.pos = ent->tr.pos;
+ after->transform.rot = ent->tr.rot;
+ after->transform.scale = ent->tr.scale;
+
+ changed |= !Vector3Equals(before->transform.pos,
+ after->transform.pos)
+ || !QuaternionEquals(before->transform.rot,
+ after->transform.rot)
+ || !Vector3Equals(before->transform.scale,
+ after->transform.scale);
+ }
+
+ if (!changed) {
+ action_apply(action, true);
+ }
+ break;
+
+ case ED_ACTION_CREATE:
+ case ED_ACTION_DELETE:
+ changed = action->item_cnt != 0;
+ break;
+
+ case ED_ACTION_RENAME:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ cstr before = action->item[i].before.rename.name;
+ changed |= strcmp(before ? before : "",
+ ent->name ? ent->name : "") != 0;
+ }
+
+ if (!changed) {
+ action_apply(action, true);
+ }
+ break;
+
+ default:
+ TraceLog(LOG_FATAL, "action_commit: unhangled action tag.");
+ break;
+ }
+
+ if (changed) {
+ history_push(*action);
+ } else {
+ action_free(action);
+ }
+
+ *action = (EditorAction){0};
+}
+
+void
+editor_init(void) {
+ // GuiLoadStyle("style_amber.rgs");
+
+ Asset *fnt_asset =
+ asset_get("res/fnt/Aileron-Regular.otf");
+ if (fnt_asset) {
+ GuiSetFont(fnt_asset->fnt);
+ GuiSetStyle(DEFAULT, TEXT_SIZE, 16);
+ }
+
+ /* probably will delete this later */
+ ed.history_save_csr =
+ save_file_read("res/save/scene.fgs") ?
+ ed.history_csr : UINT32_MAX;
+}
+
+void
+editor_exit(void) {
+ action_free(&tr_action);
+ tr_action = (EditorAction){0};
+ history_clear();
+ free(ed.history);
+ ed.history = NULL;
+ ed.history_cap = 0;
+}
+
+void
+editor_update(void) {
+ bool ctrl = IsKeyDown(KEY_LEFT_CONTROL);
+ bool shift = IsKeyDown(KEY_LEFT_SHIFT);
+ bool alt = IsKeyDown(KEY_LEFT_ALT);
+
+ /* sorry about the indentation */
+ switch (app.mode) {
+case APP_MODE_EDIT_NONE:
+ if (!ed.focus_captured && IsKeyPressed(KEY_SPACE)) {
+ ed.panel[ED_PNL_CENTER].type = ED_PNL_TYPE_SPACE;
+ ed.panel[ED_PNL_CENTER].enabled = true;
+ }
+
+ if (ed.panel[ED_PNL_CENTER].enabled) {
+ ed.focus = ED_PNL_CENTER;
+ ed.focus_captured = true;
+ break;
+ }
+
+ if (!ed.focus_captured) {
+ ed.focus = 0;
+ Vec2 mpos = GetMousePosition();
+
+ if (ed.panel[ED_PNL_LEFT].enabled &&
+ CheckCollisionPointRec(mpos,
+ ed.panel[ED_PNL_LEFT].bounds)) {
+ ed.focus = ED_PNL_LEFT;
+ }
+
+ if (ed.panel[ED_PNL_RIGHT].enabled &&
+ CheckCollisionPointRec(mpos,
+ ed.panel[ED_PNL_RIGHT].bounds)) {
+ ed.focus = ED_PNL_RIGHT;
+ }
+ }
+
+ if (ed.focus != 0) {
+ break;
+ }
+
+ if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
+ app.mode = APP_MODE_EDIT_CTRL;
+ main_cam.update = true;
+ HideCursor();
+ DisableCursor();
+ break;
+ }
+
+ if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+ app.mode = APP_MODE_EDIT_SELECT;
+ editor_select(); // capture this frame
+ break;
+ }
+
+ if (IsKeyPressed(KEY_G)) {
+ if (!ctrl && ed.sel_cnt) {
+ app.mode = APP_MODE_EDIT_MOVE;
+ break;
+ }
+ }
+
+ if (IsKeyPressed(KEY_R)) {
+ if (!ctrl && ed.sel_cnt) {
+ app.mode = APP_MODE_EDIT_ROTATE;
+ break;
+ }
+
+ if (ctrl) {
+ history_clear();
+ entity_system_clear();
+
+ ed.history_save_csr =
+ save_file_read("res/save/scene.fgs") ?
+ ed.history_csr : UINT32_MAX;
+ }
+ }
+
+ if (IsKeyPressed(KEY_S)) {
+ if (ctrl) {
+ ed.history_save_csr =
+ save_file_write("res/save/scene.fgs") ?
+ ed.history_csr : UINT32_MAX;
+ }
+
+ /* scale */
+ }
+
+ if (IsKeyPressed(KEY_X)) {
+ if (!ctrl && ed.sel_cnt) {
+ EditorAction action = action_begin(ED_ACTION_DELETE);
+ if (action.item_cnt) {
+ action_apply(&action, false);
+ action_commit(&action);
+ }
+ }
+ }
+
+ if (IsKeyPressed(KEY_A)) {
+ if (ctrl) {
+ for (u32 i = 0; i < entity_count(); ++i) {
+ u64 id = entity_id_at(i);
+ Entity *ent = entity_get(id);
+ if (ent && ent->pid) {
+ ent->ed.selected = true;
+ }
+ }
+ }
+ }
+
+ if (ctrl && IsKeyPressed(KEY_Z)) {
+ if (!shift) {
+ history_undo();
+ } else {
+ history_redo();
+ }
+ }
+ break;
+
+case APP_MODE_EDIT_SELECT:
+ editor_select();
+ break;
+
+case APP_MODE_EDIT_CTRL:
+ cam_ctrl_editor();
+ if (IsMouseButtonReleased(MOUSE_RIGHT_BUTTON)) {
+ app.mode = APP_MODE_EDIT_NONE;
+ main_cam.update = false;
+ ShowCursor();
+ }
+ break;
+
+case APP_MODE_EDIT_MOVE:
+ editor_move();
+ break;
+
+case APP_MODE_EDIT_ROTATE:
+ editor_rotate();
+ break;
+
+case APP_MODE_EDIT_SPACE:
+ break;
+
+// case APP_MODE_EDIT_CMD:
+ // break;
+
+default:
+ TraceLog(LOG_FATAL, "editor_update: unhandled app.mode value");
+ break;
+ }
+
+ /* reset selection data */
+ ed.sel_cnt = 0;
+ ed.sel_point = Vector3Zero();
+ ed.sel_rot = QuaternionIdentity();
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ u64 id = entity_id_at(i);
+ Entity *ent = entity_get(id);
+
+ if (!ent->ed.selected) {
+ continue;
+ }
+
+ ed.sel_cnt++;
+ ed.sel_point = Vector3Add(ed.sel_point, entity_world_pos(id));
+ ed.sel_rot = entity_world_rot(id);
+ }
+
+ if (ed.sel_cnt > 0) {
+ ed.sel_point = Vector3Scale(
+ ed.sel_point, 1.0f / ed.sel_cnt);
+ }
+
+ if (ed.sel_cnt != 1) {
+ ed.sel_rot = QuaternionIdentity();
+ }
+}
+
+/* tools */
+static void
+sel_ray(void) {
+ Ray ray = GetScreenToWorldRay(end_point, main_cam.cam);
+
+ b3QueryFilter filter = {
+ .categoryBits = BIT(PHYS_MASK_EDITOR_RAYCAST),
+ .maskBits = BIT(PHYS_MASK_EDITOR_SELECTABLE),
+ };
+
+ Vec3 dir = Vector3Scale(ray.direction, 1000.0f);
+ b3RayResult res =
+ b3World_CastRayClosest(physics.world_id,
+ *(b3Pos*)&ray.position,
+ *(b3Pos*)&dir,
+ filter);
+
+ if (ed.focus == ED_PNL_NULL &&
+ !IsKeyDown(KEY_LEFT_SHIFT)) {
+ entities_clear_selected();
+ }
+
+ if (res.hit) {
+ b3BodyId bid = b3Shape_GetBody(res.shapeId);
+ u64 eid = *(u64*)b3Body_GetUserData(bid);
+ Entity *ent = entity_get(eid);
+
+ if (ent->pid == 0) {
+ return;
+ }
+
+ ent->ed.selected = !ent->ed.selected;
+ }
+}
+
+static void
+sel_area(void) {
+ Vec3 fwd = Vector3Subtract(main_cam.cam.target, main_cam.cam.position);
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ u64 id = entity_id_at(i);
+ Entity *ent = entity_get(id);
+
+ if (ent->pid == 0) {
+ continue;
+ }
+
+ if (!IsKeyDown(KEY_LEFT_SHIFT)) {
+ ent->ed.selected = false;
+ }
+
+ Vec3 world_pos = entity_world_pos(id);
+ Vec3 offset =
+ Vector3Subtract(world_pos, main_cam.cam.position);
+
+ // Only the dot product’s sign matters, so neither vector
+ // needs normalization.
+ if (Vector3DotProduct(fwd, offset) <= 0) {
+ continue;
+ }
+
+ Vec2 pos = GetWorldToScreen(world_pos, main_cam.cam);
+ if (CheckCollisionPointRec(pos, sel_rec)) {
+ ent->ed.selected = true;
+ }
+ }
+}
+
+static void
+sel_rec_draw(void) {
+ DrawRectangleRec(sel_rec, (Color) { 255, 255, 255, 40 });
+ DrawRectangleLines(sel_rec.x, sel_rec.y,
+ sel_rec.width, sel_rec.height,
+ (Color) { 255, 255, 255, 80 });
+}
+
+static bool
+selection_root(Entity *ent) {
+ if (!ent->ed.selected) {
+ return false;
+ }
+
+ for (u64 parent = ent->tr.parent; parent;) {
+ Entity *ancestor = entity_get(parent);
+ assert(ancestor);
+
+ if (ancestor->ed.selected) {
+ return false;
+ }
+
+ parent = ancestor->tr.parent;
+ }
+
+ return true;
+}
+
+static Vec3
+selected_pivot(void) {
+ Vec3 pivot = {0};
+ int count = 0;
+
+ for (int i = 0; i < entity_count(); ++i) {
+ u64 id = entity_id_at(i);
+ Entity *ent = entity_get(id);
+
+ if (!ent || !ent->ed.selected) {
+ continue;
+ }
+
+ pivot = Vector3Add(pivot, entity_world_pos(id));
+ count++;
+ }
+
+ if (count > 0) {
+ pivot = Vector3Scale(pivot, 1.0f / (f32)count);
+ }
+
+ return pivot;
+}
+
+void
+editor_select(void) {
+ if (!started) {
+ start_point = GetMousePosition();
+ started = true;
+ }
+
+ end_point = GetMousePosition();
+ sel_dist = Vector2Distance(start_point, end_point);
+ sel_rec = (Rect) {
+ fminf(start_point.x, end_point.x),
+ fminf(start_point.y, end_point.y),
+ fabs(end_point.x - start_point.x),
+ fabs(end_point.y - start_point.y)
+ };
+
+ if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
+ if (sel_dist < 20) {
+ sel_ray();
+ } else {
+ sel_area();
+ }
+ started = false;
+ sel_dist = 0;
+ app.mode = APP_MODE_EDIT_NONE;
+ }
+}
+
+static bool
+ray_plane_hit(Vec3 plane_pos, Vec3 plane_dir, Vec3 *out) {
+ Ray ray = GetScreenToWorldRay(GetMousePosition(), main_cam.cam);
+
+ f32 denom = Vector3DotProduct(ray.direction, plane_dir);
+ if (fabsf(denom) < 0.0001f) {
+ return false;
+ }
+
+ Vec3 offset = Vector3Subtract(plane_pos, ray.position);
+ f32 dist = Vector3DotProduct(offset, plane_dir) / denom;
+
+ if (dist < 0.0f) {
+ return false;
+ }
+
+ *out = Vector3Add(ray.position, Vector3Scale(ray.direction, dist));
+
+ return true;
+}
+
+void
+editor_move(void) {
+ static Vec3 mv_start = {0};
+
+ if (!tr_action.item) {
+ tr_action = action_begin(ED_ACTION_TRANSFORM);
+ started = false;
+ }
+
+ bool cancel = IsKeyPressed(KEY_ESCAPE) || !tr_action.item_cnt;
+ if (cancel || IsKeyPressed(KEY_ENTER) ||
+ IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ if (cancel) {
+ action_apply(&tr_action, true);
+ action_free(&tr_action);
+ tr_action = (EditorAction){0};
+ } else {
+ action_commit(&tr_action);
+ }
+ started = false;
+ app.mode = APP_MODE_EDIT_NONE;
+ return;
+ }
+
+ Vec3 pivot = selected_pivot();
+ Vec3 axis = { 0, 1, 0 }; // mv plane normal
+ Vec3 delta = {0};
+
+ if (IsKeyDown(KEY_LEFT_ALT)) {
+ started = false;
+
+ Vec2 a = GetWorldToScreen(pivot, main_cam.cam);
+ Vec2 b = GetWorldToScreen(
+ Vector3Add(pivot, axis), main_cam.cam);
+
+ Vec2 scr_axis = Vector2Subtract(b, a);
+ f32 lensqr = Vector2LengthSqr(scr_axis);
+
+ if (lensqr <= 0.001f) {
+ return;
+ }
+
+ f32 amount =
+ Vector2DotProduct(GetMouseDelta(), scr_axis) / lensqr;
+
+ delta = Vector3Scale(axis, amount);
+ } else {
+ Vec3 hit;
+ if (!ray_plane_hit(pivot, axis, &hit)) {
+ return;
+ }
+
+ if (!started) {
+ mv_start = hit;
+ started = true;
+ return;
+ }
+
+ delta = Vector3Subtract(hit, mv_start);
+ mv_start = hit;
+ }
+
+ for (u32 i = 0; i < tr_action.item_cnt; ++i) {
+ u64 id = entity_id_from_pid(tr_action.item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ Vec3 pos = Vector3Add(entity_world_pos(id), delta);
+ entity_set_world_pos(ent, pos);
+ }
+}
+
+void
+editor_rotate(void) {
+ if (!tr_action.item) {
+ tr_action = action_begin(ED_ACTION_TRANSFORM);
+ started = false;
+ }
+
+ bool cancel = IsKeyPressed(KEY_ESCAPE) || !tr_action.item_cnt;
+ if (cancel || IsKeyPressed(KEY_ENTER) ||
+ IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ if (cancel) {
+ action_apply(&tr_action, true);
+ action_free(&tr_action);
+ tr_action = (EditorAction){0};
+ } else {
+ action_commit(&tr_action);
+ }
+
+ started = false;
+ app.mode = APP_MODE_EDIT_NONE;
+ return;
+ }
+
+ Vec3 pivot = selected_pivot();
+ Vec3 mouse_hit = {0};
+ if (!ray_plane_hit(pivot, (Vec3) {0,1,0}, &mouse_hit)) {
+ return;
+ }
+
+ Vec3 dir = Vector3Subtract(mouse_hit, pivot);
+ if (Vector3LengthSqr(dir) < 0.0001f) {
+ return;
+ }
+
+ f32 yaw = atan2f(dir.x, dir.z);
+ Quaternion rot = QuaternionFromAxisAngle((Vec3){0,1,0}, yaw);
+
+ for (u32 i = 0; i < tr_action.item_cnt; ++i) {
+ u64 id = entity_id_from_pid(tr_action.item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+ entity_set_world_rot(ent, rot);
+ }
+}
+
+/* draw */
+static void
+panel_vec3(Rect bounds, Vec3 *val, cstr label) {
+
+}
+
+static void
+statusline(void) {
+ char mode_str[16] = {0};
+ char ctx_str[256] = {0};
+ // Color bg = { 255, 255, 255, 150 };
+ Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR));
+
+ switch (app.mode) {
+ case APP_MODE_EDIT_NONE:
+ snprintf(mode_str, 16, "EDITOR");
+ if (ed.sel_cnt > 0) {
+ snprintf(ctx_str, sizeof(ctx_str),
+ "%i Selected | "
+ "G: move, R: rotate, D: duplicate, "
+ "X: delete",
+ ed.sel_cnt);
+ } else {
+ snprintf(ctx_str, sizeof(ctx_str), "Nothing selected");
+ }
+ break;
+
+ case APP_MODE_EDIT_SELECT:
+ snprintf(mode_str, 16, "SELECT");
+ break;
+
+ case APP_MODE_EDIT_CTRL:
+ snprintf(mode_str, 16, "VIEW");
+ snprintf(ctx_str, sizeof(ctx_str), "controls: "
+ "shift: boost speed, scroll: adjust speed");
+ break;
+
+ case APP_MODE_EDIT_MOVE:
+ snprintf(mode_str, 16, "MOVE");
+
+ cstr axis_str = IsKeyDown(KEY_LEFT_ALT) ?
+ "up axis" :
+ "xz plane";
+
+ snprintf(ctx_str, sizeof(ctx_str),
+ "%i Selected | "
+ "current axis: %s | "
+ "controls: shift: grid snap, "
+ "alt: up axis, X/Y/Z: toggle axis",
+ ed.sel_cnt, axis_str);
+ break;
+
+ case APP_MODE_EDIT_ROTATE:
+ snprintf(mode_str, 16, "ROTATE");
+ snprintf(ctx_str, sizeof(ctx_str), "%i Selected | "
+ "axis: global up, pivot: each entity | "
+ "controls: ctrl: change pivot, alt: local/global, "
+ "X/Y/Z: toggle axis",
+ ed.sel_cnt);
+ break;
+
+ default:
+ return;
+ break;
+ };
+
+ /* statusline bg */
+ Rect rec = {
+ // 0, 0, /* top bar */
+ 0, GetScreenHeight() - 25,
+ GetScreenWidth(), 25
+ // editor_label_w(mode_str) , 25
+ };
+ DrawRectangleRec(rec, bg);
+
+ /* mode str */
+ f32 mode_str_w = editor_label_w(mode_str);
+ rec.width = 70;
+ DrawRectangleRec(rec, (Color){0,0,0,25});
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ GuiLabel(rec, mode_str);
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+
+ /* context str */
+ f32 ctx_str_w = editor_label_w(ctx_str);
+ rec.x = 80;
+ rec.width = ctx_str_w;
+ GuiLabel(rec, ctx_str);
+ rec.x += ctx_str_w;
+
+ /* scene file status str */
+ char scene_str[96];
+ if (ed.history_save_csr == UINT32_MAX) {
+ snprintf(scene_str, sizeof(scene_str),
+ "| scene.fgs * - unsaved changes");
+ } else {
+ u32 changes = ed.history_csr > ed.history_save_csr
+ ? ed.history_csr - ed.history_save_csr
+ : ed.history_save_csr - ed.history_csr;
+
+ snprintf(scene_str, sizeof(scene_str),
+ "| scene.fgs%s - %u change%s",
+ changes ? " *" : "",
+ changes, changes == 1 ? "" : "s");
+ }
+
+ f32 scene_str_w = editor_label_w(scene_str);
+ rec.x = GetScreenWidth() - scene_str_w - 5;
+ rec.width = scene_str_w;
+
+ DrawRectangleRec(rec, bg);
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ GuiLabel(rec, scene_str);
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+}
+
+/* Scene panel */
+static void
+scene_entity(u64 id, Rect view, f32 *y, int depth, bool input) {
+ Entity *ent = entity_get(id);
+ if (!ent) {
+ return;
+ }
+
+ /* Transient entities are hidden, but their children are visited. */
+ if (ent->pid) {
+ Rect row = { view.x, *y, view.width, 26 };
+ bool hovered = input &&
+ CheckCollisionPointRec(GetMousePosition(), view) &&
+ CheckCollisionPointRec(GetMousePosition(), row);
+
+ if (hovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ ent->ed.selected = !ent->ed.selected;
+ } else {
+ entities_clear_selected();
+ ent->ed.selected = true;
+ }
+ }
+
+ if (row.y + row.height > view.y &&
+ row.y < view.y + view.height) {
+ if (ent->ed.selected || hovered) {
+ int colour = ent->ed.selected
+ ? BASE_COLOR_PRESSED
+ : BASE_COLOR_FOCUSED;
+
+ DrawRectangleRec(row,
+ GetColor(GuiGetStyle(DEFAULT,
+ colour)));
+ }
+
+ Rect label = {
+ row.x + 8 + depth * 18,
+ row.y,
+ fmaxf(0, row.width - 8 - depth * 18),
+ row.height
+ };
+
+ if (label.width > 0) {
+ GuiLabel(label,
+ ent->name && ent->name[0] ?
+ ent->name : "Entity");
+ }
+ }
+
+ *y += row.height;
+ depth++;
+ }
+
+ for (u64 child = ent->tr.first_child; child;) {
+ Entity *next = entity_get(child);
+ if (!next) {
+ break;
+ }
+
+ u64 sibling = next->tr.next;
+ scene_entity(child, view, y, depth, input);
+ child = sibling;
+ }
+}
+
+static void
+panel_scene(EditorPanelContainer container, bool input) {
+ Rect view = {0};
+ Rect bounds = ed.panel[container].bounds;
+ Vec2 *scroll = &ed.panel[container].scroll;
+
+ input = input && app.mode == APP_MODE_EDIT_NONE;
+
+ int state = GuiGetState();
+ if (!input) {
+ GuiSetState(STATE_DISABLED);
+ }
+
+ if (input && state != STATE_DISABLED &&
+ IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ ed.focus_captured = true;
+ }
+
+ GuiPanel(bounds, "Entities");
+
+ f32 btn_w = editor_label_w("Create Entity") + 16;
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ if (GuiButton(
+ (Rect){ bounds.x + 8, bounds.y + 30, btn_w, 26 },
+ "Create Entity")) {
+ EditorAction action = action_begin(ED_ACTION_CREATE);
+ action_commit(&action);
+ }
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+
+ bounds.x += 8;
+ bounds.y += 64;
+ bounds.width -= 16;
+ bounds.height -= 72;
+
+ u32 count = 0;
+ for (u32 i = 0; i < entity_count(); ++i) {
+ if (entity_get(entity_id_at(i))->pid) {
+ count++;
+ }
+ }
+
+ f32 width = bounds.width
+ - 2 * GuiGetStyle(DEFAULT, BORDER_WIDTH)
+ - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH);
+
+ GuiScrollPanel(bounds, NULL,
+ (Rect){ 0, 0, width, count * 26.0f },
+ scroll, &view);
+
+ BeginScissorMode(
+ (int)view.x, (int)view.y,
+ (int)view.width, (int)view.height);
+
+ f32 y = view.y + scroll->y;
+ for (u32 i = 0; i < entity_count(); ++i) {
+ u64 id = entity_id_at(i);
+ Entity *ent = entity_get(id);
+
+ if (!ent->tr.parent) {
+ scene_entity(id, view, &y, 0,
+ input && state != STATE_DISABLED);
+ }
+ }
+
+ EndScissorMode();
+ GuiSetState(state);
+
+ if (input && container != ED_PNL_CENTER &&
+ !IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
+ ed.focus_captured = false;
+ }
+}
+
+// static void
+// panel_properties(EditorPanelContainer container, bool input) {
+// Rect view = {0};
+// Rect bounds = ed.panel[container].bounds;
+// Vec2 *scroll = &ed.panel[container].scroll;
+
+// int state = GuiGetState();
+
+// input = input && app.mode ==
+// APP_MODE_EDIT_NONE && state != STATE_DISABLED;
+
+// if (!input) {
+// GuiSetState(STATE_DISABLED);
+// }
+
+// if (input && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+// ed.focus_captured = true;
+// }
+
+// GuiPanel(bounds, "Entity Properties");
+
+// /* needed for input field focus */
+// enum {
+// TEXT_FIELD_NONE,
+
+// TEXT_FIELD_NAME,
+
+// /* transform */
+// TEXT_FIELD_TR_POS_X,
+// TEXT_FIELD_TR_POS_Y,
+// TEXT_FIELD_TR_POS_Z,
+// TEXT_FIELD_TR_ROT_X,
+// TEXT_FIELD_TR_ROT_Y,
+// TEXT_FIELD_TR_ROT_Z,
+// TEXT_FIELD_TR_SCALE_X,
+// TEXT_FIELD_TR_SCALE_Y,
+// TEXT_FIELD_TR_SCALE_Z,
+// };
+
+// static int text_focus;
+// static char text_input[64];
+// char display_text[sizeof(text_input)];
+
+// Rect prec = bounds;
+// f32 pad = 8;
+// cstr substr = "";
+
+// if (!ed.sel_cnt) {
+// GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+// cstr str = "no entities selected";
+// GuiLabel(bounds, str);
+// GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+// text_focus = 0;
+// goto reset;
+// }
+
+// /* ed.sel_cnt can't be 0 because we've already checked and returned */
+// if (ed.sel_cnt > 1) {
+// substr = "<multiple entities selected>";
+// } else {
+// for (u32 i = 0; i < entity_count(); ++i) {
+// Entity *ent = entity_get(entity_id_at(i));
+// if (ent && ent->pid && ent->ed.selected) {
+// substr = ent->name ? ent->name : "";
+// break;
+// }
+// }
+// }
+
+// if (text_focus != TEXT_FIELD_NAME) {
+// snprintf(display_text, sizeof(display_text), "%s", substr);
+// }
+
+// cstr name_lbl = "Name";
+// u32 name_lbl_w = editor_label_w(name_lbl);
+// prec.x += pad;
+// prec.y += 30;
+// prec.width = name_lbl_w;
+// prec.height = 20;
+// GuiLabel(prec, name_lbl);
+
+// prec.x += name_lbl_w + pad;
+// prec.width = bounds.x + bounds.width - prec.x - pad;
+
+// bool name_edit = text_focus == TEXT_FIELD_NAME;
+// bool name_toggle =
+// GuiTextBox(
+// prec,
+// name_edit ? text_input : display_text,
+// sizeof(text_input),
+// input && name_edit
+// );
+
+// if (input) {
+// if (IsKeyPressed(KEY_ESCAPE)) {
+// if (name_edit) {
+// text_focus = 0;
+// }
+// } else if (name_toggle) {
+// if (name_edit) {
+// if (IsKeyPressed(KEY_ENTER)) {
+// editor_rename_selected(text_input);
+// }
+// text_focus = 0;
+// } else {
+// snprintf(text_input,
+// sizeof(text_input),
+// "%s",
+// ed.sel_cnt > 1 ?
+// "" : display_text);
+// text_focus = TEXT_FIELD_NAME;
+// }
+// }
+// }
+
+// /*
+// NOTE (tech kaczynski - 21/9/6):
+// I had a choice here to either create a tagged union of components and
+// iterate over them, or to write a giant function like this. I chose the
+// latter because the former would require a big refactor that I don't
+// really care for at this stage. The former still requires a loop and a
+// switch or branches for each type, which requires extra indentation or
+// abstraction into special helper functions etc. For now, the code
+// duplication and verbosity I can live with and might not be much worse
+// than the alternative anyway, plus I get to avoid refactoring every
+// system that touches entity component data.
+// */
+
+// /* transform comp */
+// {
+// prec.x = bounds.x + pad;
+// prec.y += 30;
+// prec.width = bounds.width - pad * 2;
+// prec.height = 90 + pad * 2;
+// GuiGroupBox(prec, "Transform");
+
+// /* pos */
+// prec.height = 20;
+// prec.y += 15;
+// prec.x += pad;
+// prec.width -= pad;
+// GuiLabel(prec, "Pos");
+// // panel_vec3(prec, ent->pos, cstr label)
+
+// /* rot */
+// static Vec3 rot; // euler angle representation of entity rot
+// prec.y += 30;
+// GuiLabel(prec, "Rot");
+// {
+
+// }
+
+// /* scale */
+// prec.y += 30;
+// GuiLabel(prec, "Scale");
+
+// // GuiTextBox(prec, char *text, int textSize, false);
+// }
+
+// /* reset */
+// reset:
+// GuiSetState(state);
+// if (input && container != ED_PNL_CENTER) {
+// ed.focus_captured = text_focus;
+// }
+// }
+
+static void
+panel_properties(EditorPanelContainer container, bool input) {
+ Rect view = {0};
+ Rect bounds = ed.panel[container].bounds;
+ Vec2 *scroll = &ed.panel[container].scroll;
+
+ int state = GuiGetState();
+
+ input = input && app.mode ==
+ APP_MODE_EDIT_NONE && state != STATE_DISABLED;
+
+ if (!input) {
+ GuiSetState(STATE_DISABLED);
+ }
+
+ if (input && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ ed.focus_captured = true;
+ }
+
+ GuiPanel(bounds, "Entity Properties");
+
+ /* needed for input field focus */
+ enum {
+ TEXT_FIELD_NONE,
+
+ TEXT_FIELD_NAME,
+
+ /* transform */
+ TEXT_FIELD_TR_POS_X,
+ TEXT_FIELD_TR_POS_Y,
+ TEXT_FIELD_TR_POS_Z,
+ TEXT_FIELD_TR_ROT_X,
+ TEXT_FIELD_TR_ROT_Y,
+ TEXT_FIELD_TR_ROT_Z,
+ TEXT_FIELD_TR_SCALE_X,
+ TEXT_FIELD_TR_SCALE_Y,
+ TEXT_FIELD_TR_SCALE_Z,
+ };
+
+ static int text_focus;
+ static char text_input[64];
+ char display_text[sizeof(text_input)];
+
+ Rect prec = bounds;
+ f32 pad = 8;
+ cstr substr = "";
+
+ if (!ed.sel_cnt) {
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ cstr str = "no entities selected";
+ GuiLabel(bounds, str);
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+ text_focus = 0;
+ goto reset;
+ }
+
+ /* ed.sel_cnt can't be 0 because we've already checked and returned */
+ Entity *ent = NULL; // only set if sel_cnt == 1
+ if (ed.sel_cnt > 1) {
+ substr = "<multiple entities selected>";
+ } else {
+ for (u32 i = 0; i < entity_count(); ++i) {
+ ent = entity_get(entity_id_at(i));
+ if (ent && ent->pid && ent->ed.selected) {
+ substr = ent->name ? ent->name : "";
+ break;
+ }
+ }
+ }
+
+ if (text_focus != TEXT_FIELD_NAME) {
+ snprintf(display_text, sizeof(display_text), "%s", substr);
+ }
+
+ cstr name_lbl = "Name";
+ u32 name_lbl_w = editor_label_w(name_lbl);
+ prec.x += pad;
+ prec.y += 30;
+ prec.width = name_lbl_w;
+ prec.height = 20;
+ GuiLabel(prec, name_lbl);
+
+ prec.x += name_lbl_w + pad;
+ prec.width = bounds.x + bounds.width - prec.x - pad;
+
+ bool name_edit = text_focus == TEXT_FIELD_NAME;
+ bool name_toggle =
+ GuiTextBox(
+ prec,
+ name_edit ? text_input : display_text,
+ sizeof(text_input),
+ input && name_edit
+ );
+
+ if (input) {
+ if (IsKeyPressed(KEY_ESCAPE)) {
+ if (name_edit) {
+ text_focus = 0;
+ }
+ } else if (name_toggle) {
+ if (name_edit) {
+ if (IsKeyPressed(KEY_ENTER)) {
+ editor_rename_selected(text_input);
+ }
+ text_focus = 0;
+ } else {
+ snprintf(text_input,
+ sizeof(text_input),
+ "%s",
+ ed.sel_cnt > 1 ?
+ "" : display_text);
+ text_focus = TEXT_FIELD_NAME;
+ }
+ }
+ }
+
+ /*
+ NOTE (tech kaczynski - 21/9/6):
+ I had a choice here to either create a tagged union of components and
+ iterate over them, or to write a giant function like this. I chose the
+ latter because the former would require a big refactor that I don't
+ really care for at this stage. The former still requires a loop and a
+ switch or branches for each type, which requires extra indentation or
+ abstraction into special helper functions etc. For now, the code
+ duplication and verbosity I can live with and might not be much worse
+ than the alternative anyway, plus I get to avoid refactoring every
+ system that touches entity component data.
+ */
+
+ /* transform comp */
+ {
+ prec.x = bounds.x + pad;
+ prec.y += 30;
+ prec.width = bounds.width - pad * 2;
+ prec.height = 90 + pad * 2;
+ GuiGroupBox(prec, "Transform");
+
+ static Vec3 pos, rot, scale;
+ // pos = panel_vec3(prec, pos, "pos");
+ // rot = panel_vec3(prec, rot, "rot");
+ // scale = panel_vec3(prec, scale, "scale");
+
+ // /* pos */
+ // prec.height = 20;
+ // prec.y += 15;
+ // prec.x += pad;
+ // prec.width -= pad;
+ // GuiLabel(prec, "Pos");
+ // // panel_vec3(prec, ent->pos, cstr label)
+
+ // /* rot */
+ // static Vec3 rot; // euler angle representation of entity rot
+ // prec.y += 30;
+ // GuiLabel(prec, "Rot");
+ // if (ent) {
+ // rot = QuaternionToEuler(ent->tr.rot);
+ // }
+ // for (int i = 0; i < 3; ++i) {
+
+ // }
+
+ // /* scale */
+ // prec.y += 30;
+ // GuiLabel(prec, "Scale");
+
+ // GuiTextBox(prec, char *text, int textSize, false);
+ }
+
+ /* reset */
+ reset:
+ GuiSetState(state);
+ if (input && container != ED_PNL_CENTER) {
+ ed.focus_captured = text_focus;
+ }
+}
+
+static void
+panel_space(EditorPanelContainer container) {
+ static cstr strs[][2] = {
+ { "Q", "Toggle left panel" },
+ { "E", "Toggle right panel" },
+ { "/", "Command Menu" },
+ };
+ static const int str_cnt = sizeof(strs) / sizeof(strs[0]);
+
+ Rect bounds = {
+ .x = (f32)GetScreenWidth() / 2 - 125,
+ .y = (f32)GetScreenHeight() / 2 - 65,
+ .width = 250,
+ .height = 130,
+ };
+
+ Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR));
+ // GuiPanel(bounds, "Space Menu");
+ DrawRectangleRec(bounds, bg);
+ GuiGroupBox((Rect) {
+ .x = bounds.x + 5,
+ .y = bounds.y + 10,
+ .width = bounds.width - 10,
+ .height = bounds.height - 15,
+ }, "Space Menu");
+
+ Rect lrec = bounds;
+ lrec.height = 20;
+ lrec.x += 16;
+
+ for (int i = 0; i < str_cnt; ++i) {
+ lrec.y += 30;
+ GuiLabel(lrec, strs[i][0]);
+
+ lrec.x += 50;
+ GuiLabel(lrec, strs[i][1]);
+ lrec.x -= 50;
+ }
+}
+
+static void
+panel_draw(EditorPanelContainer container) {
+ if (!ed.panel[container].enabled) {
+ return;
+ }
+
+ bool input = app.mode == APP_MODE_EDIT_NONE &&
+ ed.focus == container &&
+ (!ed.panel[ED_PNL_CENTER].enabled ||
+ container == ED_PNL_CENTER);
+
+ Rect bounds = ed.panel[container].bounds;
+ int state = GuiGetState();
+
+ if (!input) {
+ GuiSetState(STATE_DISABLED);
+ }
+
+ switch (ed.panel[container].type) {
+ case ED_PNL_TYPE_SCENE:
+ panel_scene(container, input);
+ break;
+
+ case ED_PNL_TYPE_PROPERTIES:
+ panel_properties(container, input);
+ break;
+
+ case ED_PNL_TYPE_SPACE:
+ bool exit = false;
+
+ if (IsKeyPressed(KEY_ESCAPE)) {
+ exit = true;
+ }
+
+ if (IsKeyPressed(KEY_Q)) {
+ ed.panel[ED_PNL_LEFT].enabled =
+ !ed.panel[ED_PNL_LEFT].enabled;
+ exit = true;
+ }
+
+ if (IsKeyPressed(KEY_E)) {
+ ed.panel[ED_PNL_RIGHT].enabled =
+ !ed.panel[ED_PNL_RIGHT].enabled;
+ exit = true;
+ }
+
+ if (exit) {
+ ed.panel[container].enabled = false;
+ ed.focus = ED_PNL_NULL;
+ ed.focus_captured = false;
+ app.mode = APP_MODE_EDIT_NONE;
+ }
+ panel_space(container);
+ break;
+
+ case ED_PNL_TYPE_CMD:
+ GuiPanel(bounds, "Command Menu");
+ break;
+ }
+
+ GuiSetState(state);
+}
+
+void
+editor_draw_ui(void) {
+ statusline();
+
+ /* L/R panel info */
+ f32 screen_w = GetScreenWidth();
+ f32 screen_h = GetScreenHeight();
+ f32 left_w = Clamp(screen_w * 0.16f, 210.0f, 280.0f);
+ f32 right_w = Clamp(screen_w * 0.22f, 260.0f, 360.0f);
+ f32 status_h = 25.0f;
+ f32 margin = 8.0f;
+
+ ed.panel[ED_PNL_LEFT].bounds = (Rect) {
+ margin,
+ margin,
+ left_w,
+ screen_h - status_h - margin * 2.0f
+ };
+
+ ed.panel[ED_PNL_RIGHT].bounds = (Rect) {
+ screen_w - right_w - margin,
+ margin,
+ right_w,
+ screen_h - status_h - margin * 2.0f
+ };
+
+ panel_draw(ED_PNL_LEFT);
+ panel_draw(ED_PNL_RIGHT);
+
+ if (app.mode == APP_MODE_EDIT_SELECT) {
+ if (started && sel_dist > 20) {
+ sel_rec_draw();
+ }
+ }
+
+ if (ed.panel[ED_PNL_CENTER].enabled) {
+ f32 width = fminf(640, screen_w - margin * 2);
+ f32 height = fminf(420, screen_h - status_h - margin * 2);
+
+ ed.panel[ED_PNL_CENTER].bounds = (Rect){
+ (screen_w - width) * 0.5f,
+ (screen_h - status_h - height) * 0.5f,
+ width, height
+ };
+
+ DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(),
+ Fade(BLACK, 0.35f));
+ panel_draw(ED_PNL_CENTER);
+ }
+}
+
+/* 3D gizmos */
+static void
+gizmo_transform(void) {
+ Vec3 point = ed.sel_point;
+ Quaternion rot = ed.sel_rot;
+
+ Vec3 right = Vector3RotateByQuaternion((Vec3){1, 0, 0}, rot);
+ Vec3 up = Vector3RotateByQuaternion((Vec3){0, 1, 0}, rot);
+ Vec3 fwd = Vector3RotateByQuaternion((Vec3){0, 0, 1}, rot);
+
+ f32 len = 0.75f;
+
+ right = Vector3Scale(right, len);
+ up = Vector3Scale(up, len);
+ fwd = Vector3Scale(fwd, len);
+
+ DrawLine3D(point, Vector3Add(point, fwd), BLUE);
+ DrawLine3D(point, Vector3Add(point, up), GREEN);
+ DrawLine3D(point, Vector3Add(point, right), RED);
+}
+
+static void
+gizmo_move(void) {
+ Color col = { 255, 255, 255, 150 };
+ Vec3 point = ed.sel_point;
+
+ if (IsKeyDown(KEY_LEFT_ALT)) {
+ DrawLine3D(point, Vector3Add(point, (Vec3){0,100,0}), col); // GREEN
+ DrawLine3D(point, Vector3Add(point, (Vec3){0,-100,0}), col);
+ } else {
+ DrawLine3D(point, Vector3Add(point, (Vec3){100,0,0}), col); //RED
+ DrawLine3D(point, Vector3Add(point, (Vec3){-100,0,0}), col);
+
+ DrawLine3D(point, Vector3Add(point, (Vec3){0,0,100}), col); //BLUE
+ DrawLine3D(point, Vector3Add(point, (Vec3){0,0,-100}), col);
+ }
+}
+
+static void
+gizmo_rotate(void) {
+ Vec3 point = ed.sel_point;
+ Color line_col = { 255, 255, 255, 150 };
+ Color circle_col = { 255, 255, 255, 100 };
+ if (ray_plane_hit(point, (Vec3){0,1,0}, &point)) {
+ DrawLine3D(ed.sel_point, point, line_col);
+ DrawCircle3D(ed.sel_point, 1.5, (Vec3){1,0,0}, 90, circle_col);
+ }
+}
+
+void
+editor_draw_3d(void) {
+ if (app.mode == APP_MODE_EDIT_MOVE) {
+ gizmo_move();
+ }
+
+ if (app.mode == APP_MODE_EDIT_ROTATE) {
+ gizmo_rotate();
+ }
+
+ if (ed.sel_cnt > 0) {
+ gizmo_transform();
+ }
+}
+
+/* utils */
+f32
+editor_label_w(cstr text) {
+ return (f32)GuiGetTextWidth(text)
+ + 2.0f * GuiGetStyle(LABEL, BORDER_WIDTH)
+ + 2.0f * GuiGetStyle(LABEL, TEXT_PADDING)
+ + 2.0f;
+}
+
+void
+editor_rename_selected(cstr name) {
+ EditorAction action = action_begin(ED_ACTION_RENAME);
+
+ for (u32 i = 0; i < action.item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action.item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ char *new_name = strdup(name);
+ if (!new_name) {
+ perror("rename alloc failed.");
+ exit(1);
+ }
+
+ action.item[i].before.rename.name = ent->name;
+ ent->name = new_name;
+ }
+
+ action_commit(&action);
+}
diff --git a/editor.h b/editor.h
new file mode 100644
index 0000000..f18cae0
--- /dev/null
+++ b/editor.h
@@ -0,0 +1,99 @@
+#ifndef EDITOR_H
+#define EDITOR_H
+
+#include "common.h"
+#include "ent.h"
+
+typedef enum {
+ ED_ACTION_TRANSFORM,
+ ED_ACTION_CREATE,
+ ED_ACTION_DELETE,
+ ED_ACTION_RENAME,
+} EditorActionTag;
+
+typedef union {
+ struct {
+ Vec3 pos, scale;
+ Quaternion rot;
+ } transform;
+
+ /* stored meaning 'stored entity', shared by creation/deletion */
+ struct {
+ Entity ent;
+ u64 parent, prev, next;
+ } stored;
+
+ struct {
+ char *name;
+ } rename;
+} EditorActionData;
+
+typedef struct {
+ EditorActionTag tag;
+ u32 item_cnt;
+ struct {
+ u64 pid;
+ EditorActionData before, after;
+ } *item;
+} EditorAction;
+
+typedef enum {
+ ED_PNL_NULL,
+ ED_PNL_LEFT,
+ ED_PNL_RIGHT,
+ ED_PNL_CENTER,
+ ED_PNL_CNT,
+} EditorPanelContainer;
+
+typedef enum {
+ ED_PNL_TYPE_SCENE,
+ ED_PNL_TYPE_PROPERTIES,
+ ED_PNL_TYPE_SPACE,
+ ED_PNL_TYPE_CMD,
+} EditorPanelType;
+
+typedef struct {
+ Vec3 sel_point;
+ Quaternion sel_rot;
+ int sel_cnt;
+
+ f32 grid_sz;
+
+ EditorPanelContainer focus;
+ bool focus_captured;
+ struct {
+ EditorPanelType type;
+ Rect bounds;
+ Vec2 scroll;
+ bool enabled;
+ } panel[ED_PNL_CNT];
+
+ EditorAction *history;
+
+ // history_save_csr UINT32_MAX means no saved position and marks file
+ // as having unsaved changes.
+ u32 history_cnt, history_cap, history_csr, history_save_csr;
+} EditorInfo;
+
+extern EditorInfo ed;
+
+void editor_init(void);
+void editor_exit(void);
+void editor_update(void);
+
+/* tools */
+void editor_select(void);
+void editor_move(void);
+void editor_rotate(void);
+
+/* draw */
+void editor_draw_ui(void);
+void editor_draw_3d(void);
+
+/* utils */
+f32 editor_label_w(cstr);
+void editor_rename_selected(cstr);
+
+
+
+#endif
diff --git a/ent.c b/ent.c
new file mode 100644
index 0000000..ca642db
--- /dev/null
+++ b/ent.c
@@ -0,0 +1,814 @@
+#include "ent.h"
+
+typedef struct {
+ u32 gen, dense_id, next_free;
+} EntitySlot;
+
+typedef struct {
+ Entity *entity;
+ EntitySlot *slot;
+ u32 *dense_to_slot;
+ u64 *delete_queue;
+ u64 *pid_to_id;
+
+ u64 next_pid;
+
+ /* caps are count not byte size */
+ u32 entity_cnt, entity_cap;
+ u32 slot_cnt, slot_cap;
+ u32 delete_queue_cnt, delete_queue_cap;
+ u32 pid_to_id_cap;
+ u32 free_slot;
+} EntitySystem;
+
+static EntitySystem en = {0};
+
+static u64
+entity_id_create(u32 slot, u32 gen) {
+ return ((u64)gen << 32) | (u64)slot;
+}
+
+static u32
+entity_slot(u64 id) {
+ return (u32)id;
+}
+
+static u32
+entity_gen(u64 id) {
+ return (u32)(id >> 32);
+}
+
+static void
+check_entity_cap(u32 req) {
+ if (req <= en.entity_cap) {
+ return;
+ }
+
+ while (en.entity_cap < req) {
+ en.entity_cap *= 2;
+ }
+
+ u32 *tmp_dts = realloc(en.dense_to_slot, en.entity_cap * sizeof(u32));
+ Entity *tmp_ent = realloc(en.entity, en.entity_cap * sizeof(Entity));
+ if (!tmp_dts || !tmp_ent) {
+ perror("err: entity array realloc failed.");
+ exit(1);
+ }
+
+ en.dense_to_slot = tmp_dts;
+ en.entity = tmp_ent;
+}
+
+static void
+check_slot_cap(u32 req) {
+ if (req <= en.slot_cap) {
+ return;
+ }
+
+ while (en.slot_cap < req) {
+ en.slot_cap *= 2;
+ }
+
+ EntitySlot *tmp = realloc(en.slot, en.slot_cap * sizeof(EntitySlot));
+ if (!tmp) {
+ perror("err: entity slot array realloc failed.");
+ exit(1);
+ }
+
+ en.slot = tmp;
+}
+
+static void
+check_delete_queue_cap(u32 req) {
+ if (req <= en.delete_queue_cap) {
+ return;
+ }
+
+ while (en.delete_queue_cap < req) {
+ en.delete_queue_cap *= 2;
+ }
+
+ u64 *tmp = realloc(en.delete_queue, en.delete_queue_cap * sizeof(u64));
+ if (!tmp) {
+ perror("err: failed to realloc delete queue.");
+ exit(1);
+ }
+
+ en.delete_queue = tmp;
+}
+
+static void
+check_pid_to_id_cap(u64 pid) {
+ if (pid < en.pid_to_id_cap) {
+ return;
+ }
+
+ u32 limit = UINT32_MAX / sizeof(u64);
+ if (pid >= limit) {
+ TraceLog(LOG_FATAL, "pid exceeds addressable table size");
+ }
+
+ u32 start_cap = en.pid_to_id_cap;
+
+ if (!en.pid_to_id_cap) {
+ en.pid_to_id_cap = 256;
+ }
+
+ while (en.pid_to_id_cap <= pid) {
+ en.pid_to_id_cap *= 2;
+ }
+
+ u64 *items = realloc(en.pid_to_id, en.pid_to_id_cap * sizeof(u64));
+ if (!items) {
+ perror("entity PID table alloc failed.");
+ exit(1);
+ }
+ en.pid_to_id = items;
+ memset(items + start_cap, 0,
+ (en.pid_to_id_cap - start_cap) * sizeof(u64));
+}
+
+
+static EntitySlot*
+entity_get_slot(u64 id) {
+ EntitySlot *slot;
+ u32 slot_id;
+ u32 gen;
+
+ if (id == 0) {
+ return NULL;
+ }
+
+ slot_id = entity_slot(id);
+ gen = entity_gen(id);
+
+ if (slot_id >= en.slot_cnt) {
+ return NULL;
+ }
+
+ slot = &en.slot[slot_id];
+
+ if (slot->dense_id == ENTITY_INVALID_IDX ||
+ slot->gen != gen ||
+ slot->dense_id >= en.entity_cnt) {
+ return NULL;
+ }
+
+ return slot;
+}
+
+static void
+entity_clear_hierarchy(u64 id) {
+ Entity *ent = entity_get(id);
+ if (!ent) {
+ return;
+ }
+
+ entity_set_parent(id, 0);
+
+ while (ent->tr.first_child) {
+ entity_set_parent(ent->tr.first_child, 0);
+ }
+}
+
+static void
+entity_delete_now(u64 id) {
+ if (!entity_valid(id)) {
+ return;
+ }
+
+ u64 child;
+ while ((child = entity_get(id)->tr.first_child) != 0) {
+ entity_delete_now(child);
+ }
+
+ Entity extracted = entity_extract_now(id);
+ entity_clean(&extracted);
+}
+
+
+static void
+entity_process_delete_queue(void) {
+ for (int i = 0; i < en.delete_queue_cnt; ++i) {
+ u64 id = en.delete_queue[i];
+
+ if (entity_valid(id)) {
+ entity_delete_now(id);
+ }
+ }
+ en.delete_queue_cnt = 0;
+}
+
+void
+entity_system_init(void) {
+ en.entity_cap = 256;
+ en.slot_cap = 256;
+ en.delete_queue_cap = 32;
+ en.entity = calloc(en.entity_cap, sizeof(Entity));
+ en.slot = calloc(en.slot_cap, sizeof(EntitySlot));
+ en.dense_to_slot = calloc(en.entity_cap, sizeof(u32));
+ en.delete_queue = calloc(en.delete_queue_cap, sizeof(u64));
+ en.free_slot = ENTITY_INVALID_IDX;
+ en.next_pid = 1;
+}
+
+void
+entity_system_exit(void) {
+ for (u32 i = 0; i < en.entity_cnt; ++i) {
+ entity_clean(&en.entity[i]);
+ }
+ free(en.entity);
+ free(en.dense_to_slot);
+ free(en.slot);
+ free(en.delete_queue);
+ free(en.pid_to_id);
+ en = (EntitySystem) {0};
+}
+
+void
+entity_system_clear(void) {
+ for (u32 i = 0; i < en.entity_cnt; ++i) {
+ entity_clean(&en.entity[i]);
+ }
+
+ memset(en.entity, 0, en.entity_cap * sizeof(Entity));
+ memset(en.slot, 0, en.slot_cap * sizeof(EntitySlot));
+ memset(en.dense_to_slot, 0, en.entity_cap * sizeof(u32));
+ memset(en.delete_queue, 0, en.delete_queue_cap * sizeof(u64));
+ memset(en.pid_to_id, 0, en.pid_to_id_cap * sizeof(u64));
+
+ en.entity_cnt = 0;
+ en.slot_cnt = 0;
+ en.delete_queue_cnt = 0;
+ en.free_slot = ENTITY_INVALID_IDX;
+ en.next_pid = 1;
+
+
+}
+
+// static void
+// entity_draw_mdl(Entity *ent) {
+// Matrix T = MatrixTranslate(
+// ent->tr.pos.x,
+// ent->tr.pos.y,
+// ent->tr.pos.z);
+
+// Matrix R = MatrixRotateXYZ((Vec3) {
+// ent->tr.rot.x * DEG2RAD,
+// ent->tr.rot.y * DEG2RAD,
+// ent->tr.rot.z * DEG2RAD,
+// });
+
+// Matrix S = MatrixScale(
+// ent->tr.scale.x,
+// ent->tr.scale.y,
+// ent->tr.scale.z);
+
+// Model mdl = *ent->mdl.mdl;
+// mdl.transform = MatrixMultiply(S, MatrixMultiply(R, T));
+
+// Color tint = ent->ed.selected ?
+// (Color){ 100, 100, 100, 255 } :
+// BLACK;
+
+// DrawModel(mdl, (Vec3) {0}, 1, tint);
+// }
+
+// static void
+// entity_draw_col(u64 id) {
+// Entity *ent = entity_get(id);
+
+// if (!b3Body_IsValid(ent->body_id)) {
+// TraceLog(LOG_WARNING, "tried to draw invalid physics body");
+// return;
+// }
+
+// if (b3Body_GetShapes(ent->body_id, &ent->shape_id, 1) == 0) {
+// return;
+// }
+
+// const b3HullData *hull = b3Shape_GetHull(ent->shape_id);
+// const b3Vec3 *points = b3GetHullPoints(hull);
+// const b3HullHalfEdge *edges = b3GetHullEdges(hull);
+// b3WorldTransform tr = b3Body_GetTransform(ent->body_id);
+
+// for (int i = 0; i < hull->edgeCount; ++i) {
+// if (i > edges[i].twin) {
+// continue;
+// }
+
+// const b3HullHalfEdge *edge = &edges[i];
+// const b3HullHalfEdge *twin = &edges[edge->twin];
+
+// b3Pos world_a =
+// b3TransformWorldPoint(tr, points[edge->origin]);
+// b3Pos world_b =
+// b3TransformWorldPoint(tr, points[twin->origin]);
+
+// Color tint = GRAY;
+// if (FLAG_CHECK(ent->flags, BIT(EFLAG_EDITOR_SELECTED))) {
+// tint = LIME;
+// }
+
+// DrawLine3D(*(Vec3*)&world_a, *(Vec3*)&world_b, tint);
+// }
+// }
+
+// static void
+// entity_set_b3_transform(u64 id) {
+// Entity *ent = entity_get(id);
+
+// if (!b3Body_IsValid(ent->body_id)) {
+// return;
+// }
+
+// b3WorldTransform tr = b3Body_GetTransform(ent->body_id);
+
+// // if in edit mode, entity transform is authoritative
+// if (FLAG_CHECK(app.flags, BIT(APP_FLAG_DEBUG))) {
+// Quaternion eq = QuaternionFromEuler(
+// ent->rot.x * DEG2RAD,
+// ent->rot.y * DEG2RAD,
+// ent->rot.z * DEG2RAD);
+
+// Quaternion bq = {
+// .x = tr.q.v.x,
+// .y = tr.q.v.y,
+// .z = tr.q.v.z,
+// .w = tr.q.s
+// };
+
+// if (Vector3Equals(ent->pos, *(Vec3*)&tr.p) &&
+// QuaternionEquals(eq, bq)) {
+// return;
+// }
+
+// b3Quat b3rot = { .v = { eq.x, eq.y, eq.z }, .s = eq.w };
+// b3Body_SetTransform(ent->body_id, *(b3Pos*)&ent->pos, b3rot);
+// } else {
+// ent->pos.x = tr.p.x;
+// ent->pos.y = tr.p.y;
+// ent->pos.z = tr.p.y;
+
+// Vec3 euler_angles =
+// QuaternionToEuler((Quaternion) {
+// .x = tr.q.v.x,
+// .y = tr.q.v.y,
+// .z = tr.q.v.z,
+// .w = tr.q.s
+// });
+
+// ent->rot.x = euler_angles.x * RAD2DEG;
+// ent->rot.y = euler_angles.y * RAD2DEG;
+// ent->rot.z = euler_angles.z * RAD2DEG;
+// }
+// }
+
+void
+entity_system_update(void) {
+ entity_process_delete_queue();
+}
+
+void
+entity_system_draw_3d(void) {
+ // for (u32 i = 0; i < entity_count(); ++i) {
+ // u64 id = entity_id_at(i);
+ // Entity *ent = entity_get(id);
+
+ // if (ent->mdl.has && ent->mdl.draw) {
+ // entity_draw_mdl(ent);
+ // }
+ // }
+}
+
+void
+entity_system_set_next_pid(u64 pid) {
+ en.next_pid = pid;
+}
+
+void
+entities_clear_selected(void) {
+ for (int i = 0; i < en.entity_cnt; ++i) {
+ Entity *ent = entity_get(entity_id_at(i));
+ ent->ed.selected = false;
+ }
+}
+
+u64
+entity_create(bool transient) {
+ check_entity_cap(en.entity_cnt + 1);
+
+ u32 slot_id = 0;
+ EntitySlot *slot;
+
+ if (en.free_slot != ENTITY_INVALID_IDX) {
+ slot_id = en.free_slot;
+ slot = &en.slot[slot_id];
+ en.free_slot = slot->next_free;
+ } else {
+ check_slot_cap(en.slot_cnt + 1);
+ slot_id = en.slot_cnt;
+ en.slot_cnt += 1;
+ slot = &en.slot[slot_id];
+ slot->gen = 1;
+ }
+
+ u32 dense_id = en.entity_cnt;
+ en.entity_cnt += 1;
+
+ slot->dense_id = dense_id;
+ slot->next_free = ENTITY_INVALID_IDX;
+ en.dense_to_slot[dense_id] = slot_id;
+
+ en.entity[dense_id] = (Entity){0};
+ en.entity[dense_id].tr.rot = QuaternionIdentity();
+ en.entity[dense_id].tr.scale = Vector3One();
+
+ u64 id = entity_id_create(slot_id, slot->gen);
+ if (!transient) {
+ u64 pid = en.next_pid++;
+ check_pid_to_id_cap(pid);
+ en.entity[dense_id].name = strdup("entity");
+ en.entity[dense_id].pid = pid;
+ en.pid_to_id[pid] = id;
+ }
+ return id;
+}
+
+u64
+entity_restore(u64 pid) {
+ if (!pid || entity_id_from_pid(pid)) {
+ return 0;
+ }
+
+ check_pid_to_id_cap(pid);
+
+ u64 id = entity_create(true);
+ Entity *ent = entity_get(id);
+ ent->pid = pid;
+ en.pid_to_id[pid] = id;
+ en.next_pid = MAX(en.next_pid, pid + 1);
+
+ return id;
+}
+
+/* deferred delete */
+void
+entity_delete(u64 id) {
+ if (!entity_valid(id)) {
+ return;
+ }
+
+ check_delete_queue_cap(en.delete_queue_cnt + 1);
+ en.delete_queue[en.delete_queue_cnt] = id;
+ en.delete_queue_cnt += 1;
+}
+
+static void
+entity_physics_clear(Entity *ent) {
+ PhysicsCmp *phys = &ent->phys;
+
+ if (phys->runtime.joints) {
+ for (int i = 0; i < phys->joint_cnt; ++i) {
+ b3JointId joint_id = phys->runtime.joints[i];
+ if (b3Joint_IsValid(joint_id)) {
+ b3DestroyJoint(joint_id, false);
+ }
+ }
+ }
+
+ if (phys->runtime.bodies) {
+ for (int i = 0; i < phys->body_cnt; ++i) {
+ PhysicsBodyRuntime *bdy = &phys->runtime.bodies[i];
+ if (b3Body_IsValid(bdy->id)) {
+ b3DestroyBody(bdy->id);
+ }
+ free(bdy->shapes);
+ }
+ }
+
+ free(phys->runtime.joints);
+ free(phys->runtime.bodies);
+ phys->runtime.joints = NULL;
+ phys->runtime.bodies = NULL;
+}
+
+void
+entity_clean(Entity *ent) {
+ free(ent->name);
+
+ /* model component data */
+ free(ent->mdl.key);
+
+ /* physics component data */
+ entity_physics_clear(ent); // runtime data
+ for (int i = 0; i < ent->phys.body_cnt; ++i) {
+ PhysicsBody *body = &ent->phys.bodies[i];
+
+ for (int j = 0; j < body->shape_cnt; ++j) {
+ PhysicsShape *shape = &body->shapes[j];
+
+ switch (shape->tag) {
+ case PHYS_SHAPE_CONVEX_HULL:
+ free(shape->ConvexHull.points);
+ break;
+
+ case PHYS_SHAPE_STATIC_TRIMESH:
+ free(shape->TriMesh.points);
+ free(shape->TriMesh.indices);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ free(body->shapes);
+ }
+
+ free(ent->phys.bodies);
+ free(ent->phys.joints);
+}
+
+Entity
+entity_extract_now(u64 id) {
+ EntitySlot *slot = entity_get_slot(id);
+ assert(slot);
+
+ u32 slot_id = entity_slot(id);
+ u32 dense_id = slot->dense_id;
+ u32 last_dense_id = en.entity_cnt - 1;
+
+ assert(!en.entity[dense_id].tr.first_child);
+
+ entity_physics_clear(&en.entity[dense_id]);
+ entity_set_parent(id, 0);
+ Entity extracted = en.entity[dense_id];
+
+ if (extracted.pid) {
+ en.pid_to_id[extracted.pid] = 0;
+ }
+
+ if (dense_id != last_dense_id) {
+ u32 moved_slot_id = en.dense_to_slot[last_dense_id];
+ en.entity[dense_id] = en.entity[last_dense_id];
+ en.dense_to_slot[dense_id] = moved_slot_id;
+ en.slot[moved_slot_id].dense_id = dense_id;
+ }
+
+ en.entity[last_dense_id] = (Entity){0};
+ en.dense_to_slot[last_dense_id] = ENTITY_INVALID_IDX;
+ en.entity_cnt--;
+
+ slot->dense_id = ENTITY_INVALID_IDX;
+ slot->gen++;
+
+ if (slot->gen == 0) {
+ slot->gen = 1;
+ }
+
+ slot->next_free = en.free_slot;
+ en.free_slot = slot_id;
+
+ return extracted;
+}
+
+static void
+entity_unlink_parent(u64 id) {
+ Entity *ent = entity_get(id);
+ if (!ent) {
+ return;
+ }
+
+ TransformCmp *tr = &ent->tr;
+
+ if (tr->prev) {
+ Entity *prev = entity_get(tr->prev);
+ if (prev) {
+ prev->tr.next = tr->next;
+ }
+ } else if (tr->parent) {
+ Entity *parent = entity_get(tr->parent);
+ if (parent && parent->tr.first_child == id) {
+ parent->tr.first_child = tr->next;
+ }
+ }
+
+ if (tr->next) {
+ Entity *next = entity_get(tr->next);
+ if (next) {
+ next->tr.prev = tr->prev;
+ }
+ }
+
+ tr->parent = 0;
+ tr->prev = 0;
+ tr->next = 0;
+}
+
+static bool
+entity_is_ancestor(u64 ancestor_id, u64 id) {
+ while (id) {
+ if (id == ancestor_id) {
+ return true;
+ }
+
+ Entity *ent = entity_get(id);
+ if (!ent) {
+ return false;
+ }
+
+ id = ent->tr.parent;
+ }
+
+ return false;
+}
+
+bool
+entity_set_parent(u64 id, u64 parent_id) {
+ Entity *ent = entity_get(id);
+ if (!ent) {
+ TraceLog(LOG_WARNING, "entity_set_parent: entity id null");
+ return false;
+ }
+
+ if (parent_id == ent->tr.parent) {
+ return true;
+ }
+
+ Entity *parent = NULL;
+ if (parent_id) {
+ parent = entity_get(parent_id);
+ if (!parent ||
+ parent_id == id ||
+ entity_is_ancestor(id, parent_id)) {
+ TraceLog(LOG_ERROR, "entity hierarchy failed.");
+ return false;
+ }
+ }
+
+ entity_unlink_parent(id);
+
+ if (!parent) {
+ return true;
+ }
+
+ u64 first_child = parent->tr.first_child;
+ ent->tr.parent = parent_id;
+ ent->tr.next = first_child;
+
+ if (first_child) {
+ Entity *first = entity_get(first_child);
+ if (first) {
+ first->tr.prev = id;
+ }
+ }
+
+ parent->tr.first_child = id;
+ return true;
+}
+
+bool
+entity_valid(u64 id) {
+ return entity_get_slot(id) != NULL;
+}
+
+Entity*
+entity_get(u64 id) {
+ EntitySlot *slot = entity_get_slot(id);
+ if (!slot) {
+ return NULL;
+ }
+
+ return &en.entity[slot->dense_id];
+}
+
+u64
+entity_id_at(u32 dense_id) {
+ if (dense_id >= en.entity_cnt) {
+ return 0;
+ }
+
+ u32 slot_id = en.dense_to_slot[dense_id];
+ EntitySlot *slot = &en.slot[slot_id];
+
+ return entity_id_create(slot_id, slot->gen);
+}
+
+u64
+entity_id_from_pid(u64 pid) {
+ return pid < en.pid_to_id_cap ? en.pid_to_id[pid] : 0;
+}
+
+Entity*
+entity_data(void) {
+ return en.entity;
+}
+
+u32
+entity_count(void) {
+ return en.entity_cnt;
+}
+
+u64
+entity_next_pid(void) {
+ return en.next_pid;
+}
+
+/* entity transform section */
+Matrix
+entity_world_matrix(u64 id) {
+ Matrix world = MatrixIdentity();
+
+ while (id) {
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ TransformCmp *tr = &ent->tr;
+
+ Matrix scale = MatrixScale(
+ tr->scale.x, tr->scale.y, tr->scale.z);
+ Matrix rotation = QuaternionToMatrix(tr->rot);
+ Matrix translation = MatrixTranslate(
+ tr->pos.x, tr->pos.y, tr->pos.z);
+
+ /* Raymath order: apply scale, rotation, translation. */
+ Matrix local = MatrixMultiply(
+ MatrixMultiply(scale, rotation), translation);
+
+ world = MatrixMultiply(world, local);
+ id = tr->parent;
+ }
+
+ return world;
+}
+
+Vec3
+entity_point_to_world(u64 space, Vec3 point) {
+ return Vector3Transform(point, entity_world_matrix(space));
+}
+
+bool
+entity_point_to_local(u64 space, Vec3 point, Vec3 *out) {
+ if (space) {
+ Entity *ent = entity_get(space);
+ assert(ent);
+
+ TransformCmp *tr = &ent->tr;
+ if (!tr->scale.x || !tr->scale.y || !tr->scale.z) {
+ return false;
+ }
+
+ if (!entity_point_to_local(tr->parent, point, &point)) {
+ return false;
+ }
+ point = Vector3Subtract(point, tr->pos);
+ point = Vector3RotateByQuaternion(point,
+ QuaternionInvert(tr->rot));
+ point = Vector3Divide(point, tr->scale);
+ }
+
+ *out = point;
+ return true;
+}
+
+Vec3
+entity_world_pos(u64 id) {
+ return entity_point_to_world(id, Vector3Zero());
+}
+
+Quaternion
+entity_world_rot(u64 id) {
+ Quaternion rot = QuaternionIdentity();
+
+ while (id) {
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ rot = QuaternionMultiply(ent->tr.rot, rot);
+ id = ent->tr.parent;
+ }
+
+ return QuaternionNormalize(rot);
+}
+
+void
+entity_set_world_pos(Entity *ent, Vec3 pos) {
+ if (!entity_point_to_local(ent->tr.parent, pos, &ent->tr.pos)) {
+ TraceLog(LOG_WARNING, "Cannot set world position through a "
+ "zero-scale ancestor.");
+ }
+}
+
+void
+entity_set_world_rot(Entity *ent, Quaternion rot) {
+ f32 qlen = QuaternionLength(rot);
+ if (!ent || !qlen || !isfinite(qlen)) {
+ return;
+ }
+
+ ent->tr.rot =
+ QuaternionNormalize(QuaternionMultiply(
+ QuaternionInvert(entity_world_rot(ent->tr.parent)),
+ QuaternionNormalize(rot)));
+}
diff --git a/ent.h b/ent.h
new file mode 100644
index 0000000..48ecbec
--- /dev/null
+++ b/ent.h
@@ -0,0 +1,96 @@
+#ifndef ENT_H
+#define ENT_H
+
+#include "common.h"
+#include "physics.h"
+
+#define ENTITY_INVALID_IDX UINT32_MAX
+
+// typedef enum {
+// ENT_COMP_TRANSFORM,
+// ENT_COMP_MODEL,
+// ENT_COMP_PHYSICS,
+// ENT_COMP_EDITOR,
+// ENT_COMP_CNT,
+// } EntityComponentType;
+
+typedef struct {
+ u64 parent;
+ u64 first_child;
+ u64 prev, next; // siblings
+ /* parent-local */
+ Vec3 pos, scale;
+ Quaternion rot;
+} TransformCmp;
+
+typedef struct {
+ char *key;
+ Model *mdl;
+ bool has, draw;
+} ModelCmp;
+
+typedef struct {
+ PhysicsBody *bodies;
+ PhysicsJoint *joints;
+ u32 body_cnt, joint_cnt;
+
+ struct {
+ PhysicsBodyRuntime *bodies;
+ b3JointId *joints;
+ } runtime;
+
+ bool has, debug_draw;
+} PhysicsCmp;
+
+typedef struct {
+ bool selected;
+} EditorCmp; // transient data for non transient entities ^___^
+
+typedef struct {
+ TransformCmp tr;
+ ModelCmp mdl;
+ PhysicsCmp phys;
+ EditorCmp ed;
+
+ u64 pid; // persistent serialized id, not runtime entity id
+ char *name;
+} Entity;
+
+void entity_system_init(void);
+void entity_system_exit(void);
+void entity_system_clear(void);
+void entity_system_update(void);
+void entity_system_draw_3d(void);
+void entity_system_set_next_pid(u64);
+
+/* clears all entities of their selected flag */
+void entities_clear_selected(void);
+
+u64 entity_create(bool transient); // transient means no pid
+u64 entity_restore(u64 pid);
+void entity_delete(u64 id);
+void entity_clean(Entity*); // clean entity data without removing
+Entity entity_extract_now(u64 id); // removes, returns ownership
+bool entity_valid(u64 id);
+
+Entity* entity_get(u64 id);
+u64 entity_id_at(u32 dense_idx);
+u64 entity_id_from_pid(u64 pid);
+Entity* entity_data(void);
+u32 entity_count(void);
+u64 entity_next_pid(void);
+
+bool entity_set_parent(u64 id, u64 parent_id);
+
+/* transform component data */
+Matrix entity_world_matrix(u64 id);
+/* space identifies the reference entity, 0 means world space. */
+Vec3 entity_point_to_world(u64 space, Vec3 point);
+bool entity_point_to_local(u64 space, Vec3 point, Vec3 *out);
+
+Vec3 entity_world_pos(u64 id);
+Quaternion entity_world_rot(u64 id);
+void entity_set_world_pos(Entity*, Vec3);
+void entity_set_world_rot(Entity*, Quaternion);
+
+#endif
diff --git a/env.c b/env.c
new file mode 100644
index 0000000..f00b669
--- /dev/null
+++ b/env.c
@@ -0,0 +1,384 @@
+#include "env.h"
+
+Environment env = {0};
+
+static const Color SKY_NIGHT_HORIZON = { 14, 11, 28, 255 };
+static const Color SKY_NIGHT_ZENITH = { 1, 2, 6, 255 };
+static const Color SKY_DAY_HORIZON = { 158, 199, 235, 255 };
+static const Color SKY_DAY_ZENITH = { 20, 71, 158, 255 };
+
+static f32
+sky_smoothstep(f32 edge_0, f32 edge_1, f32 value) {
+ f32 amount = (value - edge_0) / (edge_1 - edge_0);
+
+ if (amount < 0.0f) {
+ amount = 0.0f;
+ } else if (amount > 1.0f) {
+ amount = 1.0f;
+ }
+
+ return amount * amount * (3.0f - 2.0f * amount);
+}
+
+static Vec3
+sky_get_sun_direction(f32 cycle) {
+ f32 angle = cycle * 2.0f * PI - PI * 0.5f;
+
+ Vec3 dir = (Vec3) {
+ cosf(angle),
+ sinf(angle),
+ 0.0f
+ };
+
+ return Vector3Normalize(dir);
+}
+
+static Vec3
+sky_get_moon_direction(Vec3 sun_dir) {
+ return Vector3Negate(sun_dir);
+}
+
+static void
+sky_draw_symbol(Texture tex, Vec3 dir, Camera3D cam, f32 angular_diameter,
+ Color tint, f32 visibility) {
+ if (tex.id == 0 || visibility <= 0.0f) {
+ return;
+ }
+
+ Vec3 cam_fwd = Vector3Normalize(
+ Vector3Subtract(cam.target, cam.position));
+
+ f32 fwd_facing = Vector3DotProduct(cam_fwd, dir);
+
+ // don't draw if behind the camera
+ if (fwd_facing <= 0.0f) {
+ return;
+ }
+
+ // distance is arbitrary. Only the direction matters because result is
+ // projected into screen space.
+ Vec3 world_pos = Vector3Add(cam.position, Vector3Scale(dir, 1000.0f));
+ Vec2 scr_pos = GetWorldToScreen(world_pos, cam);
+
+ // Convert the requested angular diameter into pixels. This makes the
+ // symbols respond correctly when camera FOV changes.
+ f32 vfov = cam.fovy * DEG2RAD;
+
+ f32 diameter =
+ (f32)GetScreenHeight() *
+ tanf(angular_diameter * DEG2RAD * 0.5f) /
+ tanf(vfov * 0.5f);
+
+ if (diameter < 1.0f) {
+ return;
+ }
+
+ tint.a = (u8)((f32)tint.a * visibility);
+
+ Rect src = (Rect) { 0, 0, (f32)tex.width, (f32)tex.height };
+ Rect dst = (Rect) { scr_pos.x, scr_pos.y, diameter, diameter };
+ Vec2 origin = (Vec2) { diameter * 0.5f, diameter * 0.5f };
+
+ DrawTexturePro(tex, src, dst, origin, 0, tint);
+}
+
+static Color
+color_lerp(Color a, Color b, f32 t) {
+ t = Clamp(t, 0.0f, 1.0f);
+
+ return (Color) {
+ (u8)Lerp(a.r, b.r, t),
+ (u8)Lerp(a.g, b.g, t),
+ (u8)Lerp(a.b, b.b, t),
+ (u8)Lerp(a.a, b.a, t),
+ };
+}
+
+static Vec3
+color_to_vec3(Color col) {
+ return (Vec3) {
+ (f32)col.r / 255.0f,
+ (f32)col.g / 255.0f,
+ (f32)col.b / 255.0f,
+ };
+}
+
+static void
+sky_update_lighting(void) {
+ Vec3 sun_dir = sky_get_sun_direction(env.cycle);
+ Vec3 moon_dir = sky_get_moon_direction(sun_dir);
+
+ f32 day_amount = sky_smoothstep(-0.08f, 0.15f, sun_dir.y);
+ f32 night_amount = 1.0f - day_amount;
+
+ // Strongest when the sun is close to the horizon.
+ f32 twilight_amount = 1.0f - sky_smoothstep(
+ 0.02f,
+ 0.28f,
+ fabsf(sun_dir.y)
+ );
+
+ Color horizon_col = color_lerp(
+ SKY_NIGHT_HORIZON,
+ SKY_DAY_HORIZON,
+ day_amount
+ );
+
+ Color zenith_col = color_lerp(
+ SKY_NIGHT_ZENITH,
+ SKY_DAY_ZENITH,
+ day_amount
+ );
+
+ Color night_ambient = { 18, 22, 42, 255 };
+ Color day_ambient = { 145, 165, 185, 255 };
+ Color twilight_ambient = { 185, 90, 55, 255 };
+
+ Color ambient_col = color_lerp(
+ night_ambient,
+ day_ambient,
+ day_amount
+ );
+
+ ambient_col = color_lerp(
+ ambient_col,
+ twilight_ambient,
+ twilight_amount * 0.45f
+ );
+
+ Color sun_col = color_lerp(
+ (Color){ 255, 155, 95, 255 },
+ (Color){ 255, 244, 215, 255 },
+ day_amount
+ );
+
+ Color moon_col = { 135, 155, 205, 255 };
+
+ env.sun_dir = sun_dir;
+ env.moon_dir = moon_dir;
+
+ env.sun_col = sun_col;
+ env.moon_col = moon_col;
+ env.ambient_col = ambient_col;
+
+ env.horizon_col = horizon_col;
+ env.zenith_col = zenith_col;
+
+ env.day_amount = day_amount;
+ env.twilight_amount = twilight_amount;
+
+ env.sun_intensity = day_amount;
+ env.moon_intensity = night_amount * 0.12f;
+ env.ambient_intensity = Lerp(0.18f, 0.55f, day_amount);
+}
+
+static void
+env_update_mdl_shader(void) {
+ Vec3 sun_col = color_to_vec3(env.sun_col);
+ Vec3 moon_col = color_to_vec3(env.moon_col);
+ Vec3 ambient_col = color_to_vec3(env.ambient_col);
+
+ Shader sdr = shader_get("model")->sdr;
+ SetTraceLogLevel(LOG_DEBUG);
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "sun_direction"),
+ &env.sun_dir,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "sun_colour"),
+ &sun_col,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "sun_intensity"),
+ &env.sun_intensity,
+ SHADER_UNIFORM_FLOAT
+ );
+
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "moon_direction"),
+ &env.moon_dir,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "moon_colour"),
+ &moon_col,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "moon_intensity"),
+ &env.moon_intensity,
+ SHADER_UNIFORM_FLOAT
+ );
+
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "ambient_colour"),
+ &ambient_col,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ sdr,
+ fun_shader_uniform("model", "ambient_intensity"),
+ &env.ambient_intensity,
+ SHADER_UNIFORM_FLOAT
+ );
+
+ SetTraceLogLevel(LOG_WARNING);
+}
+
+void
+env_init(void) {
+ env.sky_sdr_key = "sky";
+ env.sun_tex_key = "res/tex/sun.png";
+ env.moon_tex_key = "res/tex/moon.png";
+ env.cycle = 0.2;
+ env.day_len = 240; // 600s = 5 min day/night cycle
+ env.sun_sz = 14;
+ env.moon_sz = 4;
+ env.sun_tint = WHITE;
+ env.moon_tint = GRAY;
+
+ sky_update_lighting();
+}
+
+void
+env_exit(void) {
+}
+
+void
+env_update(void) {
+ // if (env.day_len > 0.0f) {
+ // env.cycle += GetFrameTime() / env.day_len;
+ // env.cycle -= floorf(env.cycle);
+ // }
+ sky_update_lighting();
+ env_update_mdl_shader();
+}
+
+
+void
+env_draw_sky(Camera cam) {
+ int scr_w = GetScreenWidth();
+ int scr_h = GetScreenHeight();
+
+ if (scr_w <= 0 || scr_h <= 0) {
+ return;
+ }
+
+ Vec3 cam_fwd =
+ Vector3Normalize(Vector3Subtract(cam.target, cam.position));
+
+ Vec3 cam_right =
+ Vector3Normalize(Vector3CrossProduct(cam_fwd, cam.up));
+
+ Vec3 cam_up =
+ Vector3Normalize(Vector3CrossProduct(cam_right, cam_fwd));
+
+ Vec3 sun_dir = env.sun_dir;
+ Vec3 moon_dir = env.moon_dir;
+
+ f32 scr_res[2] = { (f32)scr_w, (f32)scr_h };
+ f32 aspect = scr_res[0] / scr_res[1];
+ f32 tan_half_fovy = tanf(cam.fovy * DEG2RAD * 0.5f);
+
+ Vec3 horizon_col = color_to_vec3(env.horizon_col);
+ Vec3 zenith_col = color_to_vec3(env.zenith_col);
+
+ FunShader *shader = shader_get(env.sky_sdr_key);
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "resolution"),
+ scr_res,
+ SHADER_UNIFORM_VEC2
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "camera_forward"),
+ &cam_fwd,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "camera_right"),
+ &cam_right,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "camera_up"),
+ &cam_up,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "sun_direction"),
+ &env.sun_dir,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "horizon_colour"),
+ &horizon_col,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "zenith_colour"),
+ &zenith_col,
+ SHADER_UNIFORM_VEC3
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "day_amount"),
+ &env.day_amount,
+ SHADER_UNIFORM_FLOAT
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "twilight_amount"),
+ &env.twilight_amount,
+ SHADER_UNIFORM_FLOAT
+ );
+
+ SetShaderValue(
+ shader->sdr,
+ fun_shader_uniform(env.sky_sdr_key, "tan_half_fovy"),
+ &tan_half_fovy,
+ SHADER_UNIFORM_FLOAT
+ );
+
+ BeginShaderMode(shader->sdr);
+ DrawRectangle(0, 0, scr_w, scr_h, WHITE);
+ EndShaderMode();
+
+ f32 sun_vis = sky_smoothstep(-0.05f, 0.25f, sun_dir.y);
+ f32 moon_vis = sky_smoothstep(-0.05f, 0.8f, moon_dir.y);
+
+ sky_draw_symbol(asset_get(env.sun_tex_key)->tex, sun_dir, cam,
+ env.sun_sz, env.sun_tint, sun_vis);
+
+ sky_draw_symbol(asset_get(env.moon_tex_key)->tex, moon_dir, cam,
+ env.moon_sz, env.moon_tint, moon_vis);
+}
diff --git a/env.h b/env.h
new file mode 100644
index 0000000..c62f884
--- /dev/null
+++ b/env.h
@@ -0,0 +1,46 @@
+#ifndef ENV_H
+#define ENV_H
+
+#include "common.h"
+#include "res.h"
+
+typedef struct {
+ cstr sky_sdr_key, ocean_sdr_key;
+
+ Vec3 sun_dir;
+ Vec3 moon_dir;
+
+ Color sun_col;
+ Color moon_col;
+ Color ambient_col;
+ Color horizon_col;
+ Color zenith_col;
+
+ f32 sun_intensity;
+ f32 moon_intensity;
+ f32 ambient_intensity;
+
+ f32 day_amount;
+ f32 twilight_amount;
+
+ f32 cycle;
+ f32 day_len;
+
+ /* sun/moon textures */
+ cstr sun_tex_key, moon_tex_key;
+ Color sun_tint, moon_tint;
+ f32 sun_sz, moon_sz;
+
+ f32 ocean_height, ocean_time;
+} Environment;
+
+extern Environment env;
+
+void env_init(void);
+void env_exit(void);
+void env_update(void);
+void env_draw_sky(Camera);
+void env_draw_ocean(Camera);
+
+#endif
+
diff --git a/ext/fuzzy_match.c b/ext/fuzzy_match.c
new file mode 100644
index 0000000..fa06d56
--- /dev/null
+++ b/ext/fuzzy_match.c
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2022 Philip Jones
+ *
+ * Licensed under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#define _GNU_SOURCE /* Required for strcasestr. */
+#include <ctype.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "fuzzy_match.h"
+
+#undef MAX
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+static int32_t compute_score(
+ int32_t jump,
+ bool first_char,
+ const char *restrict match);
+
+static int32_t fuzzy_match_recurse(
+ const char *restrict pattern,
+ const char *restrict str,
+ int32_t score,
+ bool first_char);
+
+/*
+ * Returns score if each character in pattern is found sequentially within str.
+ * Returns INT32_MIN otherwise.
+ */
+int32_t fuzzy_match(const char *restrict pattern, const char *restrict str)
+{
+ const int unmatched_letter_penalty = -1;
+ const size_t slen = strlen(str);
+ const size_t plen = strlen(pattern);
+ int32_t score = 100;
+
+ if (*pattern == '\0') {
+ return score;
+ }
+ if (slen < plen) {
+ return INT32_MIN;
+ }
+
+ /* We can already penalise any unused letters. */
+ score += unmatched_letter_penalty * (int32_t)(slen - plen);
+
+ /* Perform the match. */
+ score = fuzzy_match_recurse(pattern, str, score, true);
+
+ return score;
+}
+
+/*
+ * Recursively match the whole of pattern against str.
+ * The score parameter is the score of the previously matched character.
+ *
+ * This reaches a maximum recursion depth of strlen(pattern) + 1. However, the
+ * stack usage is small (the maximum I've seen on x86_64 is 144 bytes with
+ * gcc -O3), so this shouldn't matter unless pattern contains thousands of
+ * characters.
+ */
+int32_t fuzzy_match_recurse(
+ const char *restrict pattern,
+ const char *restrict str,
+ int32_t score,
+ bool first_char)
+{
+ if (*pattern == '\0') {
+ /* We've matched the full pattern. */
+ return score;
+ }
+
+ const char *match = str;
+ const char search[2] = { *pattern, '\0' };
+
+ int32_t best_score = INT32_MIN;
+
+ /*
+ * Find all occurrences of the next pattern character in str, and
+ * recurse on them.
+ */
+ while ((match = strcasestr(match, search)) != NULL) {
+ int32_t subscore = fuzzy_match_recurse(
+ pattern + 1,
+ match + 1,
+ compute_score(match - str, first_char, match),
+ false);
+ best_score = MAX(best_score, subscore);
+ match++;
+ }
+
+ if (best_score == INT32_MIN) {
+ /* We couldn't match the rest of the pattern. */
+ return INT32_MIN;
+ } else {
+ return score + best_score;
+ }
+}
+
+/*
+ * Calculate the score for a single matching letter.
+ * The scoring system is taken from fts_fuzzy_match v0.2.0 by Forrest Smith,
+ * which is licensed to the public domain.
+ *
+ * The factors affecting score are:
+ * - Bonuses:
+ * - If there are multiple adjacent matches.
+ * - If a match occurs after a separator character.
+ * - If a match is uppercase, and the previous character is lowercase.
+ *
+ * - Penalties:
+ * - If there are letters before the first match.
+ * - If there are superfluous characters in str (already accounted for).
+ */
+int32_t compute_score(int32_t jump, bool first_char, const char *restrict match)
+{
+ const int adjacency_bonus = 15;
+ const int separator_bonus = 30;
+ const int camel_bonus = 30;
+ const int first_letter_bonus = 15;
+
+ const int leading_letter_penalty = -5;
+ const int max_leading_letter_penalty = -15;
+
+ int32_t score = 0;
+
+ /* Apply bonuses. */
+ if (!first_char && jump == 0) {
+ score += adjacency_bonus;
+ }
+ if (!first_char || jump > 0) {
+ if (isupper((unsigned char)*match)
+ && islower((unsigned char)*(match - 1))) {
+ score += camel_bonus;
+ }
+ if (isalnum((unsigned char)*match)
+ && !isalnum((unsigned char)*(match - 1))) {
+ score += separator_bonus;
+ }
+ }
+ if (first_char && jump == 0) {
+ /* Match at start of string gets separator bonus. */
+ score += first_letter_bonus;
+ }
+
+ /* Apply penalties. */
+ if (first_char) {
+ score += MAX(leading_letter_penalty * jump,
+ max_leading_letter_penalty);
+ }
+
+ return score;
+}
diff --git a/ext/fuzzy_match.h b/ext/fuzzy_match.h
new file mode 100644
index 0000000..df4d298
--- /dev/null
+++ b/ext/fuzzy_match.h
@@ -0,0 +1,8 @@
+#ifndef FUZZY_MATCH_H
+#define FUZZY_MATCH_H
+
+#include <stdint.h>
+
+int32_t fuzzy_match(const char *restrict pattern, const char *restrict str);
+
+#endif /* FUZZY_MATCH_H */
diff --git a/ext/hashmap.c b/ext/hashmap.c
new file mode 100644
index 0000000..99def75
--- /dev/null
+++ b/ext/hashmap.c
@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) 2014 rxi
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT license. See LICENSE for details.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include "hashmap.h"
+
+struct map_node_t {
+ unsigned hash;
+ void *value;
+ map_node_t *next;
+ /* char key[]; */
+ /* char value[]; */
+};
+
+
+static unsigned map_hash(const char *str) {
+ unsigned hash = 5381;
+ while (*str) {
+ hash = ((hash << 5) + hash) ^ *str++;
+ }
+ return hash;
+}
+
+
+static map_node_t *map_newnode(const char *key, void *value, int vsize) {
+ map_node_t *node;
+ int ksize = strlen(key) + 1;
+ int voffset = ksize + ((sizeof(void*) - ksize) % sizeof(void*));
+ node = malloc(sizeof(*node) + voffset + vsize);
+ if (!node) return NULL;
+ memcpy(node + 1, key, ksize);
+ node->hash = map_hash(key);
+ node->value = ((char*) (node + 1)) + voffset;
+ memcpy(node->value, value, vsize);
+ return node;
+}
+
+
+static int map_bucketidx(map_base_t *m, unsigned hash) {
+ /* If the implementation is changed to allow a non-power-of-2 bucket count,
+ * the line below should be changed to use mod instead of AND */
+ return hash & (m->nbuckets - 1);
+}
+
+
+static void map_addnode(map_base_t *m, map_node_t *node) {
+ int n = map_bucketidx(m, node->hash);
+ node->next = m->buckets[n];
+ m->buckets[n] = node;
+}
+
+
+static int map_resize(map_base_t *m, int nbuckets) {
+ map_node_t *nodes, *node, *next;
+ map_node_t **buckets;
+ int i;
+ /* Chain all nodes together */
+ nodes = NULL;
+ i = m->nbuckets;
+ while (i--) {
+ node = (m->buckets)[i];
+ while (node) {
+ next = node->next;
+ node->next = nodes;
+ nodes = node;
+ node = next;
+ }
+ }
+ /* Reset buckets */
+ buckets = realloc(m->buckets, sizeof(*m->buckets) * nbuckets);
+ if (buckets != NULL) {
+ m->buckets = buckets;
+ m->nbuckets = nbuckets;
+ }
+ if (m->buckets) {
+ memset(m->buckets, 0, sizeof(*m->buckets) * m->nbuckets);
+ /* Re-add nodes to buckets */
+ node = nodes;
+ while (node) {
+ next = node->next;
+ map_addnode(m, node);
+ node = next;
+ }
+ }
+ /* Return error code if realloc() failed */
+ return (buckets == NULL) ? -1 : 0;
+}
+
+
+static map_node_t **map_getref(map_base_t *m, const char *key) {
+ unsigned hash = map_hash(key);
+ map_node_t **next;
+ if (m->nbuckets > 0) {
+ next = &m->buckets[map_bucketidx(m, hash)];
+ while (*next) {
+ if ((*next)->hash == hash && !strcmp((char*) (*next + 1), key)) {
+ return next;
+ }
+ next = &(*next)->next;
+ }
+ }
+ return NULL;
+}
+
+
+void map_deinit_(map_base_t *m) {
+ map_node_t *next, *node;
+ int i;
+ i = m->nbuckets;
+ while (i--) {
+ node = m->buckets[i];
+ while (node) {
+ next = node->next;
+ free(node);
+ node = next;
+ }
+ }
+ free(m->buckets);
+}
+
+
+void *map_get_(map_base_t *m, const char *key) {
+ map_node_t **next = map_getref(m, key);
+ return next ? (*next)->value : NULL;
+}
+
+
+int map_set_(map_base_t *m, const char *key, void *value, int vsize) {
+ int n, err;
+ map_node_t **next, *node;
+ /* Find & replace existing node */
+ next = map_getref(m, key);
+ if (next) {
+ memcpy((*next)->value, value, vsize);
+ return 0;
+ }
+ /* Add new node */
+ node = map_newnode(key, value, vsize);
+ if (node == NULL) goto fail;
+ if (m->nnodes >= m->nbuckets) {
+ n = (m->nbuckets > 0) ? (m->nbuckets << 1) : 1;
+ err = map_resize(m, n);
+ if (err) goto fail;
+ }
+ map_addnode(m, node);
+ m->nnodes++;
+ return 0;
+ fail:
+ if (node) free(node);
+ return -1;
+}
+
+
+void map_remove_(map_base_t *m, const char *key) {
+ map_node_t *node;
+ map_node_t **next = map_getref(m, key);
+ if (next) {
+ node = *next;
+ *next = (*next)->next;
+ free(node);
+ m->nnodes--;
+ }
+}
+
+
+map_iter_t map_iter_(void) {
+ map_iter_t iter;
+ iter.bucketidx = -1;
+ iter.node = NULL;
+ return iter;
+}
+
+
+const char *map_next_(map_base_t *m, map_iter_t *iter) {
+ if (iter->node) {
+ iter->node = iter->node->next;
+ if (iter->node == NULL) goto nextBucket;
+ } else {
+ nextBucket:
+ do {
+ if (++iter->bucketidx >= m->nbuckets) {
+ return NULL;
+ }
+ iter->node = m->buckets[iter->bucketidx];
+ } while (iter->node == NULL);
+ }
+ return (char*) (iter->node + 1);
+}
diff --git a/ext/hashmap.h b/ext/hashmap.h
new file mode 100644
index 0000000..615fb90
--- /dev/null
+++ b/ext/hashmap.h
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2014 rxi
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT license. See LICENSE for details.
+ */
+
+#ifndef HASHMAP_H
+#define HASHMAP_H
+
+#include <string.h>
+
+#define MAP_VERSION "0.1.0"
+
+struct map_node_t;
+typedef struct map_node_t map_node_t;
+
+typedef struct {
+ map_node_t **buckets;
+ unsigned nbuckets, nnodes;
+} map_base_t;
+
+typedef struct {
+ unsigned bucketidx;
+ map_node_t *node;
+} map_iter_t;
+
+
+#define map_t(T)\
+ struct { map_base_t base; T *ref; T tmp; }
+
+
+#define map_init(m)\
+ memset(m, 0, sizeof(*(m)))
+
+
+#define map_deinit(m)\
+ map_deinit_(&(m)->base)
+
+
+#define map_get(m, key)\
+ ( (m)->ref = map_get_(&(m)->base, key) )
+
+
+#define map_set(m, key, value)\
+ ( (m)->tmp = (value),\
+ map_set_(&(m)->base, key, &(m)->tmp, sizeof((m)->tmp)) )
+
+
+#define map_remove(m, key)\
+ map_remove_(&(m)->base, key)
+
+
+#define map_iter(m)\
+ map_iter_()
+
+
+#define map_next(m, iter)\
+ map_next_(&(m)->base, iter)
+
+
+void map_deinit_(map_base_t *m);
+void *map_get_(map_base_t *m, const char *key);
+int map_set_(map_base_t *m, const char *key, void *value, int vsize);
+void map_remove_(map_base_t *m, const char *key);
+map_iter_t map_iter_(void);
+const char *map_next_(map_base_t *m, map_iter_t *iter);
+
+
+typedef map_t(void*) map_void_t;
+typedef map_t(char*) map_str_t;
+typedef map_t(int) map_int_t;
+typedef map_t(char) map_char_t;
+typedef map_t(float) map_float_t;
+typedef map_t(double) map_double_t;
+
+#endif
diff --git a/ext/tinyfiledialogs/README.txt b/ext/tinyfiledialogs/README.txt
new file mode 100644
index 0000000..24f7f85
--- /dev/null
+++ b/ext/tinyfiledialogs/README.txt
@@ -0,0 +1,233 @@
+SPDX-License-Identifier: Zlib
+Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
+
+********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
+
+ http://tinyfiledialogs.sourceforge.net
+ git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+***************************************************************************
+ ____________________________________________________________________
+ | |
+ | 100% compatible C C++ -> You can rename tinfiledialogs.c as .cpp |
+ \____________________________________________________________________/
+
+tiny file dialogs ( cross-platform C C++ ) v3.21.3 [Feb 12, 2026]
+ _________
+/ \ Tray-popup InputBox PasswordBox MessageBox Notification Beep ColorPicker
+|tiny file| ColorPicker OpenFileDialog SaveFileDialog SelectFolderDialog
+| dialogs | ASCII UTF-8 (and also MBCS & UTF-16 for windows)
+\____ ___/ Native dialog library for WINDOWS MAC OSX GTK+ QT CONSOLE X Wayland
+ \| SSH: automatic switch to console mode / X forwarding / waypipe
+
+C89/C18 & C++98/C++23 compliant: tested with C & C++ compilers
+VisualStudio MinGW GCC Clang TinyCC IntelCC OpenWatcomC BorlandC SunCC
+on Windows Mac Linux Bsd Solaris Minix Raspbian Flatpak Haiku
+using Gnome Kde Mate Enlightenment Cinnamon Budgie Unity Lxde Lxqt Xfce
+ WindowMaker IceWm Cde Jds OpenBox Awesome Jwm Xdm Cwm
+
+Bindings for LUA, C#, dll, Fortran, Pascal, R.
+Included in LWJGL(java), Rust, Haskell, Allegrobasic.
+ ____________________________________________________________________________
+| ________________________________________________________________________ |
+| | ____________________________________________________________________ | |
+| | | If you like tinyfiledialogs, please upvote my stackoverflow answer | | |
+| | | https://stackoverflow.com/a/47651444 | | |
+| | |____________________________________________________________________| | |
+| |________________________________________________________________________| |
+|____________________________________________________________________________|
+
+ ———————————————————————————————————————————————————————
+ | v3.10: FORTRAN module fully implemented with examples |
+ | v3.13: PASCAL unit fully implemented with examples |
+ | v3.14: R inteface fully implemented with examples |
+ | v3.21: New HAIKU porting |
+ ———————————————————————————————————————————————————————
+ _____________________________________________________________________
+ | |
+ | my email address is at the top of the header file tinyfiledialogs.h |
+ |_____________________________________________________________________|
+ ________________________________________________________________________________
+| ____________________________________________________________________________ |
+| | | |
+| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
+| | | |
+| | on windows: | |
+| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
+| | - _wfopen() requires wchar_t | |
+| | | |
+| | - but fopen() expects MBCS (not UTF-8) | |
+| | - if you want char to be MBCS: set tinyfd_winUtf8 = 0 | |
+| | | |
+| | - alternatively, tinyfiledialogs provides | |
+| | functions to convert between UTF-8, UTF-16 and MBCS | |
+| |____________________________________________________________________________| |
+|________________________________________________________________________________|
+
+ ___________________________________________________________________________________
+| _______________________________________________________________________________ |
+| | | |
+| | wchar_t UTF-16 (windows only) prototypes are at the bottom of the header file | |
+| |_______________________________________________________________________________| |
+|___________________________________________________________________________________|
+
+ __________________________________________
+ | ______________________________________ |
+ | | | |
+ | | DO NOT USE USER INPUT IN THE DIALOGS | |
+ | |______________________________________| |
+ |__________________________________________|
+
+
+See compilation instructions at the end of this file
+
+void tinyfd_beep();
+
+int tinyfd_notifyPopup(
+ char const * aTitle , // NULL or ""
+ char const * aMessage , // NULL or "" may contain \n \t
+ char const * aIconType ); // "info" "warning" "error"
+
+int tinyfd_messageBox(
+ char const * aTitle , // NULL or ""
+ char const * aMessage , // NULL or "" may contain \n \t
+ char const * aDialogType , // "ok" "okcancel" "yesno" "yesnocancel"
+ char const * aIconType , // "info" "warning" "error" "question"
+ int aDefaultButton );
+ // 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel
+
+char const * tinyfd_inputBox(
+ char const * aTitle , // NULL or ""
+ char const * aMessage , // NULL or "" (\n and \t have no effect)
+ char const * aDefaultInput ); // NULL for a passwordBox, "" for an inputbox
+ // returns NULL on cancel
+
+char const * tinyfd_saveFileDialog(
+ char const * aTitle , // NULL or ""
+ char const * aDefaultPathAndOrFile , // NULL or "" , ends with / to set only a directory
+ int aNumOfFilterPatterns , // 0 (1 in the following example)
+ char const * const * aFilterPatterns , // NULL or char const * lFilterPatterns[1]={"*.txt"};
+ char const * aSingleFilterDescription ); // NULL or "text files"
+ // returns NULL on cancel
+
+char const * tinyfd_openFileDialog(
+ char const * aTitle , // NULL or ""
+ char const * aDefaultPathAndOrFile , // NULL or "" , ends with / to set only a directory
+ int aNumOfFilterPatterns , // 0 (2 in the following example)
+ char const * const * aFilterPatterns , // NULL or char const * lFilterPatterns[2]={"*.png","*.jpg"};
+ char const * aSingleFilterDescription , // NULL or "image files"
+ int aAllowMultipleSelects ); // 0
+ // in case of multiple files, the separator is |
+ // returns NULL on cancel
+
+char const * tinyfd_selectFolderDialog(
+ char const * aTitle , // NULL or ""
+ char const * aDefaultPath ); // NULL or ""
+ // returns NULL on cancel
+
+char const * tinyfd_colorChooser(
+ char const * aTitle , // NULL or ""
+ char const * aDefaultHexRGB , // NULL or "#FF0000‚Ç¥
+ unsigned char const aDefaultRGB[3] , // unsigned char lDefaultRGB[3] = { 0 , 128 , 255 };
+ unsigned char aoResultRGB[3] ); // unsigned char lResultRGB[3];
+ // returns the hexcolor as a string "#FF0000"
+ // aoResultRGB also contains the result
+ // aDefaultRGB is used only if aDefaultHexRGB is NULL
+ // aDefaultRGB and aoResultRGB can be the same array
+ // returns NULL on cancel
+ ___________________________________________________________________________________
+| _______________________________________________________________________________ |
+| | | |
+| | wchar_t UTF-16 (windows only) prototypes are at the bottom of the header file | |
+| |_______________________________________________________________________________| |
+|___________________________________________________________________________________|
+
+- This is not for ios nor android (it works in termux and iSH though).
+- The files can be renamed with extension ".cpp" as the code is 100% compatible C C++
+- Windows is fully supported from XP to 11 (maybe even older versions)
+- C# & LUA via dll, see files in the folder EXTRAS
+- OSX supported from 10.4 to latest (maybe even older versions)
+- Do not use " and ' as the dialogs will be display with a warning
+ instead of the title, message, etc...
+- There's one file filter only, it may contain several patterns.
+- If no filter description is provided,
+ the list of patterns will become the description.
+- On windows link against Comdlg32.lib and Ole32.lib
+ (on windows the no linking claim is a lie)
+- On unix / macos: it only tries command line calls, so no linking is need.
+- On unix /macos you need one of the following:
+ applescript, kdialog, zenity, matedialog, shellementary, qarma, shanty, boxer,
+ yad, python (2 or 3)with tkinter/python-dbus, Xdialog
+ or curses dialogs (opens terminal if running without console).
+- One of those is already included on most (if not all) desktops.
+- In the absence of those it will use gdialog, gxmessage or whiptail
+ with a textinputbox.
+- If nothing is found, it switches to basic console input,
+ it opens a console if needed (requires xterm + bash).
+- for curses dialogs you must set tinyfd_allowCursesDialogs=1
+- You can query the type of dialog that will be used (pass "tinyfd_query" as aTitle)
+- String memory is preallocated statically for all the returned values.
+- File and path names are tested before return, they should be valid.
+- tinyfd_forceConsole=1; at run time, forces dialogs into console mode.
+- On windows, console mode only make sense for console applications.
+- On windows, console mode is not implemented for wchar_T UTF-16.
+- Mutiple selects are not possible in console mode.
+- The package dialog must be installed to run in curses dialogs in console mode.
+ It is already installed on most unix systems.
+- On osx, the package dialog can be installed via
+ http://macappstore.org/dialog or http://macports.org
+- On windows, for curses dialogs console mode,
+ dialog.exe should be copied somewhere on your executable path.
+ It can be found at the bottom of the following page:
+ http://andrear.altervista.org/home/cdialog.php
+ _________________________________________________________________
+ | The project provides an Hello World example: |
+ | if a console is missing, it will use graphic dialogs |
+ | if a graphical display is absent, it will use console dialogs |
+ |_________________________________________________________________|
+
+
+UNIX (including MacOS) :
+$ clang -o hello hello.c tinyfiledialogs.c
+( or gcc tcc owcc icx suncc )
+( or g++ clang++ icpx sunCC )
+( some possible options :
+ -ansi -std=c89 -std=c++98 -pedantic -Wstrict-prototypes
+ -g3 -Wall -Wextra -Wdouble-promotion -Wconversion -Wno-sign-conversion
+ -Wno-unused-parameter -Wno-unused-function -fsanitize=undefined -fsanitize=thread
+ -Wno-deprecated -Wno-incompatible-compiler )
+( if using musl instead of glibc: clang -fuse-ld=lld --rtlib=compiler-rt )
+
+ _____________________________________________________________________________
+| Windows : |
+| You'll probably need to install The Windows SDK (Software Development Kit) |
+| http://developer.microsoft.com/en-us/windows/downloads/windows-sdk |
+| The end user doesn't need to install anything |
+|_____________________________________________________________________________|
+
+ MinGW needs gcc >= v4.9 otherwise some headers are incomplete
+ > gcc -o hello.exe hello.c tinyfiledialogs.c -LC:/mingw/lib -lcomdlg32 -lole32
+
+ TinyCC needs >= v0.9.27 (+ tweaks - contact me) otherwise some headers are missing
+ > tcc -o hello.exe hello.c tinyfiledialogs.c ^
+ -isystem C:\tcc\winapi-full-for-0.9.27\include\winapi ^
+ -lcomdlg32 -lole32 -luser32 -lshell32
+
+ Embarcadero / Borland C :
+ > bcc32c -o hello.exe hello.c tinyfiledialogs.c
+
+ Open Watcom C v2
+ > owcc -o hello.exe hello.c tinyfiledialogs.c
+
+ Windows Intel C :
+ > icx-cc -o hello.exe hello.c tinyfiledialogs.c -lcomdlg32 -lole32 -luser32 -lshell32
+ > icx-cl -o hello.exe hello.c tinyfiledialogs.c comdlg32.lib ole32.lib user32.lib shell32.lib
+ > icx -o hello.exe hello.c tinyfiledialogs.c comdlg32.lib ole32.lib user32.lib shell32.lib
+ > icpx -o hello.exe hello.c tinyfiledialogs.c -lcomdlg32 -lole32 -luser32 -lshell32 -Wno-deprecated
+
+ VisualStudio command line :
+ > cl hello.c tinyfiledialogs.c comdlg32.lib ole32.lib user32.lib shell32.lib /W4
+
+ VisualStudio
+ In the properties of your project, in the linker input field,
+ you may need to add: comdlg32.lib ole32.lib user32.lib shell32.lib
+ or maybe simply add: %(AdditionalDependencies)
diff --git a/ext/tinyfiledialogs/hello.c b/ext/tinyfiledialogs/hello.c
new file mode 100644
index 0000000..2f1ed79
--- /dev/null
+++ b/ext/tinyfiledialogs/hello.c
@@ -0,0 +1,307 @@
+/* SPDX-License-Identifier: Zlib
+Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
+ ________________________________________________________________
+ | |
+ | 100% compatible C C++ -> You can rename this .c file as .cpp |
+ |________________________________________________________________|
+
+********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
+ _________
+ / \ hello.c v3.21.3 [Feb 12, 2026]
+ |tiny file| Hello World file created [November 9, 2014]
+ | dialogs |
+ \____ ___/ http://tinyfiledialogs.sourceforge.net
+ \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+ ____________________________________________
+ | |
+ | email: tinyfiledialogs at ysengrin.com |
+ |____________________________________________|
+ _________________________________________________________________________________
+ | |
+ | the windows only wchar_t UTF-16 prototypes are at the bottom of the header file |
+ |_________________________________________________________________________________|
+ _________________________________________________________
+ | |
+ | on windows: - since v3.6 char is UTF-8 by default |
+ | - if you want MBCS set tinyfd_winUtf8 to 0 |
+ | - functions like fopen expect MBCS |
+ |_________________________________________________________|
+ ___________________________________________________________
+ | |
+ | v3.10: NEW FORTRAN module fully implemented with examples |
+ | https://stackoverflow.com/a/59657117 |
+ |___________________________________________________________|
+
+If you like tinyfiledialogs, please upvote my stackoverflow answer
+https://stackoverflow.com/a/47651444
+
+- License -
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+*/
+
+
+/*
+- Here is the Hello World:
+ if a console is missing, it will use graphic dialogs
+ if a graphical display is absent, it will use console dialogs
+ (on windows the input box may take some time to open the first time)
+
+ See compilation instructions at the end of this file
+
+ __________________________________________
+ | ______________________________________ |
+ | | | |
+ | | DO NOT USE USER INPUT IN THE DIALOGS | |
+ | |______________________________________| |
+ |__________________________________________|
+*/
+
+#include <stdio.h>
+#include <string.h>
+#include "tinyfiledialogs.h"
+
+#ifdef _MSC_VER
+#pragma warning(disable:4996) /* silences warnings about strcpy strcat fopen*/
+#endif
+
+int main( int argc , char * argv[] )
+{
+ int lIntValue;
+ char * lPassword;
+ char * lTheSaveFileName;
+ char * lTheOpenFileName;
+ char * lTheSelectFolderName;
+ char * lTheHexColor;
+ char * lWillBeGraphicMode;
+ unsigned char lRgbColor[3];
+ FILE * lIn;
+ char lBuffer[1024];
+ char const * lFilterPatterns[2] = { "*.txt", "*.json" };
+
+ (void)argv; /*to silence stupid visual studio warning*/
+
+ tinyfd_verbose = argc - 1; /* default is 0 */
+ tinyfd_silent = 1; /* default is 1 : on unix, hide errors and warnings from called dialogs */
+
+ tinyfd_forceConsole = 0; /* default is 0 */
+ /* tinyfd_assumeGraphicDisplay = 0; */ /* default is 0 */
+
+#ifdef _WIN32
+ tinyfd_winUtf8 = 1; /* default is 1 */
+/* On windows, you decide if char holds 1:UTF-8(default) or 0:MBCS */
+/* Windows is not ready to handle UTF-8 as many char functions like fopen() expect MBCS filenames.*/
+/* This hello.c file has been prepared, on windows, to convert the filenames from UTF-8 to UTF-16
+ and pass them passed to _wfopen() instead of fopen() */
+#endif
+
+ tinyfd_beep();
+
+ lWillBeGraphicMode = tinyfd_inputBox("tinyfd_query", NULL, NULL);
+
+ strcpy(lBuffer, "tinyfiledialogs\nv");
+ strcat(lBuffer, tinyfd_version);
+ if (lWillBeGraphicMode)
+ {
+ strcat(lBuffer, "\ngraphic mode: ");
+ }
+ else
+ {
+ strcat(lBuffer, "\nconsole mode: ");
+ }
+ strcat(lBuffer, tinyfd_response);
+ tinyfd_messageBox("hello", lBuffer, "ok", "info", 0);
+
+ tinyfd_notifyPopup("the title", "the message\n\tfrom outer-space", "info");
+
+ if ( lWillBeGraphicMode && ! tinyfd_forceConsole )
+ {
+#if 0
+ lIntValue = tinyfd_messageBox("Hello World", "\
+graphic dialogs [Yes]\n\
+console mode [No]\n\
+quit [Cancel]",
+ "yesnocancel", "question", 1);
+ if (!lIntValue) return 1;
+ tinyfd_forceConsole = (lIntValue == 2);
+#else
+ lIntValue = tinyfd_messageBox(
+ "Hello World", "graphic dialogs [Yes] / console mode [No]",
+ "yesno", "question", 1);
+ tinyfd_forceConsole = ! lIntValue;
+#endif
+ }
+
+ lPassword = tinyfd_inputBox(
+ "a password box", "your password will be revealed later", NULL);
+
+ if (!lPassword) return 1;
+
+ tinyfd_messageBox("your password as read", lPassword, "ok", "info", 1);
+
+ lTheSaveFileName = tinyfd_saveFileDialog(
+ "let us save this password",
+ "./passwordFile.txt",
+ 2,
+ lFilterPatterns,
+ NULL);
+
+ if (! lTheSaveFileName)
+ {
+ tinyfd_messageBox(
+ "Error",
+ "Save file name is NULL",
+ "ok",
+ "error",
+ 1);
+ return 1 ;
+ }
+
+#ifdef _WIN32
+ if (tinyfd_winUtf8)
+ lIn = _wfopen(tinyfd_utf8to16(lTheSaveFileName), L"w"); /* the UTF-8 filename is converted to UTF-16 to open the file*/
+ else
+#endif
+ lIn = fopen(lTheSaveFileName, "w");
+
+ if (!lIn)
+ {
+ tinyfd_messageBox(
+ "Error",
+ "Can not open this file in write mode",
+ "ok",
+ "error",
+ 1);
+ return 1 ;
+ }
+ fputs(lPassword, lIn);
+ fclose(lIn);
+
+ lTheOpenFileName = tinyfd_openFileDialog(
+ "let us read the password back",
+ "../",
+ 2,
+ lFilterPatterns,
+ "text files",
+ 1);
+
+ if (! lTheOpenFileName)
+ {
+ tinyfd_messageBox(
+ "Error",
+ "Open file name is NULL",
+ "ok",
+ "error",
+ 0);
+ return 1 ;
+ }
+
+#ifdef _WIN32
+ if (tinyfd_winUtf8)
+ lIn = _wfopen(tinyfd_utf8to16(lTheOpenFileName), L"r"); /* the UTF-8 filename is converted to UTF-16 */
+ else
+#endif
+ lIn = fopen(lTheOpenFileName, "r");
+
+ if (!lIn)
+ {
+ tinyfd_messageBox(
+ "Error",
+ "Can not open this file in read mode",
+ "ok",
+ "error",
+ 1);
+ return(1);
+ }
+
+ lBuffer[0] = '\0';
+ fgets(lBuffer, sizeof(lBuffer), lIn);
+ fclose(lIn);
+
+ tinyfd_messageBox("your password as it was saved", lBuffer, "ok", "info", 1);
+
+ lTheSelectFolderName = tinyfd_selectFolderDialog(
+ "let us just select a directory", "../../");
+
+ if (!lTheSelectFolderName)
+ {
+ tinyfd_messageBox(
+ "Error",
+ "Select folder name is NULL",
+ "ok",
+ "error",
+ 1);
+ return 1;
+ }
+
+ tinyfd_messageBox("The selected folder is", lTheSelectFolderName, "ok", "info", 1);
+
+ lTheHexColor = tinyfd_colorChooser(
+ "choose a nice color",
+ "#FF0077",
+ lRgbColor,
+ lRgbColor);
+
+ if (!lTheHexColor)
+ {
+ tinyfd_messageBox(
+ "Error",
+ "hexcolor is NULL",
+ "ok",
+ "error",
+ 1);
+ return 1;
+ }
+
+ tinyfd_messageBox("The selected hexcolor is", lTheHexColor, "ok", "info", 1);
+
+ tinyfd_messageBox("your read password was", lPassword, "ok", "info", 1);
+
+ return 0;
+}
+
+#ifdef _MSC_VER
+#pragma warning(default:4996)
+#endif
+
+/*
+OSX :
+$ clang -o hello.app hello.c tinyfiledialogs.c
+( or gcc )
+
+UNIX :
+$ gcc -o hello hello.c tinyfiledialogs.c
+( or clang tcc owcc cc CC )
+
+Windows :
+ MinGW needs gcc >= v4.9 otherwise some headers are incomplete
+ > gcc -o hello.exe hello.c tinyfiledialogs.c -LC:/mingw/lib -lcomdlg32 -lole32
+
+ TinyCC needs >= v0.9.27 (+ tweaks - contact me) otherwise some headers are missing
+ > tcc -o hello.exe hello.c tinyfiledialogs.c ^
+ -isystem C:\tcc\winapi-full-for-0.9.27\include\winapi ^
+ -lcomdlg32 -lole32 -luser32 -lshell32
+
+ Borland C: > bcc32c -o hello.exe hello.c tinyfiledialogs.c
+ OpenWatcom v2: create a character-mode executable project.
+
+ VisualStudio :
+ Create a console application project,
+ it links against comdlg32.lib & ole32.lib.
+
+ VisualStudio command line :
+ > cl hello.c tinyfiledialogs.c comdlg32.lib ole32.lib user32.lib shell32.lib /W4
+*/
diff --git a/ext/tinyfiledialogs/hello_wchar_t.c b/ext/tinyfiledialogs/hello_wchar_t.c
new file mode 100644
index 0000000..e05f3f5
--- /dev/null
+++ b/ext/tinyfiledialogs/hello_wchar_t.c
@@ -0,0 +1,241 @@
+/* SPDX-License-Identifier: Zlib
+Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
+ ________________________________________________________________
+ | |
+ | 100% compatible C C++ -> You can rename this .c file as .cpp |
+ |________________________________________________________________|
+
+********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
+ _________
+ / \ hello_wchar_t.c v3.21.3 [Feb 12, 2026]
+ |tiny file| Hello WCHAR_T windows only file created [November 9, 2014]
+ | dialogs |
+ \____ ___/ http://tinyfiledialogs.sourceforge.net
+ \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+ ____________________________________________
+ | |
+ | email: tinyfiledialogs at ysengrin.com |
+ |____________________________________________|
+ ________________________________________________________________
+ | |
+ | this file is for windows only it uses wchar_t UTF-16 functions |
+ |________________________________________________________________|
+
+If you like tinyfiledialogs, please upvote my stackoverflow answer
+https://stackoverflow.com/a/47651444
+
+- License -
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+ See compilation instructions at the end of this file
+
+ __________________________________________
+ | ______________________________________ |
+ | | | |
+ | | DO NOT USE USER INPUT IN THE DIALOGS | |
+ | |______________________________________| |
+ |__________________________________________|
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "tinyfiledialogs.h"
+
+#ifdef _MSC_VER
+#pragma warning(disable:4996) /* silences warning about wcscpy*/
+#endif
+
+int main(void) /* WINDOWS ONLY */
+{
+ wchar_t * lPassword;
+ wchar_t * lTheSaveFileName;
+ wchar_t * lTheOpenFileName;
+ wchar_t * lTheSelectFolderName;
+ wchar_t * lTheHexColor;
+ wchar_t * lWillBeGraphicMode;
+ unsigned char lRgbColor[3];
+ FILE * lIn;
+ wchar_t lWcharBuff[1024];
+ wchar_t lBuffer[1024];
+ wchar_t const * lFilterPatterns[2] = { L"*.txt", L"*.text" };
+
+ tinyfd_beep();
+
+ lWillBeGraphicMode = tinyfd_inputBoxW(L"tinyfd_query", NULL, NULL);
+
+ wcscpy(lBuffer, L"v");
+ mbstowcs(lWcharBuff, tinyfd_version, strlen(tinyfd_version) + 1);
+ wcscat(lBuffer, lWcharBuff);
+ if (lWillBeGraphicMode)
+ {
+ wcscat(lBuffer, L"\ngraphic mode: ");
+ }
+ else
+ {
+ wcscat(lBuffer, L"\nconsole mode: ");
+ }
+ mbstowcs(lWcharBuff, tinyfd_response, strlen(tinyfd_response)+1);
+ wcscat(lBuffer, lWcharBuff);
+ wcscat(lBuffer, L"\n");
+ mbstowcs(lWcharBuff, tinyfd_needs + 78, strlen(tinyfd_needs + 78) + 1);
+ wcscat(lBuffer, lWcharBuff);
+
+ tinyfd_messageBoxW(L"hello", lBuffer, L"ok", L"info", 0);
+
+ tinyfd_notifyPopupW(L"the title", L"the message\n\tfrom outer-space", L"info");
+
+ lPassword = tinyfd_inputBoxW(
+ L"a password box", L"your password will be revealed later", NULL);
+
+ if (!lPassword) return 1;
+
+ lTheSaveFileName = tinyfd_saveFileDialogW(
+ L"let us save this password",
+ L"passwordFile.txt",
+ 2,
+ lFilterPatterns,
+ NULL);
+
+ if (! lTheSaveFileName)
+ {
+ tinyfd_messageBoxW(
+ L"Error",
+ L"Save file name is NULL",
+ L"ok",
+ L"error",
+ 1);
+ return 1 ;
+ }
+
+ lIn = _wfopen(lTheSaveFileName, L"wt, ccs=UNICODE");
+ if (!lIn)
+ {
+ tinyfd_messageBoxW(
+ L"Error",
+ L"Can not open this file in write mode",
+ L"ok",
+ L"error",
+ 1);
+ return 1 ;
+ }
+ fputws(lPassword, lIn);
+ fclose(lIn);
+
+ lTheOpenFileName = tinyfd_openFileDialogW(
+ L"let us read the password back",
+ L"",
+ 2,
+ lFilterPatterns,
+ NULL,
+ 0);
+
+ if (! lTheOpenFileName)
+ {
+ tinyfd_messageBoxW(
+ L"Error",
+ L"Open file name is NULL",
+ L"ok",
+ L"error",
+ 1);
+ return 1 ;
+ }
+
+ lIn = _wfopen(lTheOpenFileName, L"rt, ccs=UNICODE");
+
+ if (!lIn)
+ {
+ tinyfd_messageBoxW(
+ L"Error",
+ L"Can not open this file in read mode",
+ L"ok",
+ L"error",
+ 1);
+ return(1);
+ }
+ lBuffer[0] = '\0';
+ fgetws(lBuffer, sizeof(lBuffer), lIn);
+ fclose(lIn);
+
+ tinyfd_messageBoxW(L"your password is",
+ lBuffer, L"ok", L"info", 1);
+
+ lTheSelectFolderName = tinyfd_selectFolderDialogW(
+ L"let us just select a directory", L"C:\\");
+
+ if (!lTheSelectFolderName)
+ {
+ tinyfd_messageBoxW(
+ L"Error",
+ L"Select folder name is NULL",
+ L"ok",
+ L"error",
+ 1);
+ return 1;
+ }
+
+ tinyfd_messageBoxW(L"The selected folder is",
+ lTheSelectFolderName, L"ok", L"info", 1);
+
+ lTheHexColor = tinyfd_colorChooserW(
+ L"choose a nice color",
+ L"#FF0077",
+ lRgbColor,
+ lRgbColor);
+
+ if (!lTheHexColor)
+ {
+ tinyfd_messageBoxW(
+ L"Error",
+ L"hexcolor is NULL",
+ L"ok",
+ L"error",
+ 1);
+ return 1;
+ }
+
+ tinyfd_messageBoxW(L"The selected hexcolor is",
+ lTheHexColor, L"ok", L"info", 1);
+
+ tinyfd_messageBoxW(L"your password was", lPassword, L"ok", L"info", 1);
+
+ return 0;
+}
+
+#ifdef _MSC_VER
+#pragma warning(default:4996)
+#endif
+
+
+/*
+MinGW needs gcc >= v4.9 otherwise some headers are incomplete
+> gcc -o hello.exe hello.c tinyfiledialogs.c -LC:/mingw/lib -lcomdlg32 -lole32
+
+TinyCC needs >= v0.9.27 (+ tweaks - contact me) otherwise some headers are missing
+> tcc -o hello.exe hello.c tinyfiledialogs.c ^
+ -isystem C:\tcc\winapi-full-for-0.9.27\include\winapi ^
+ -lcomdlg32 -lole32 -luser32 -lshell32
+
+Borland C: > bcc32c -o hello.exe hello.c tinyfiledialogs.c
+OpenWatcom v2: create a character-mode executable project.
+
+VisualStudio :
+ Create a console application project,
+ it links against comdlg32.lib & ole32.lib.
+
+VisualStudio command line :
+ > cl hello.c tinyfiledialogs.c comdlg32.lib ole32.lib user32.lib shell32.lib /W4
+*/
diff --git a/ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.c b/ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.c
new file mode 100644
index 0000000..bc2960f
--- /dev/null
+++ b/ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.c
@@ -0,0 +1,259 @@
+/* SPDX-License-Identifier: ZLIB
+Copyright (c) 2014 - 2023 Guillaume Vareille http://ysengrin.com
+ _________
+ / \ tinyfiledialogs v3.9.0 [Nov 3, 2022] zlib licence
+ |tiny file|
+ | dialogs |
+ \____ ___/ http://tinyfiledialogs.sourceforge.net
+ \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+
+If you like tinyfiledialogs, please upvote my stackoverflow answer
+https://stackoverflow.com/a/47651444
+
+- License -
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef __sun
+#ifndef _POSIX_C_SOURCE
+#define _POSIX_C_SOURCE 2 /* to accept POSIX 2 in old ANSI C standards */
+#endif
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../tinyfiledialogs.h"
+
+#define MAX_PATH_OR_CMD 1024 /* _MAX_PATH or MAX_PATH */
+int tfd_quoteDetected(char const * aString);
+void tfd_replaceSubStr( char const * aSource ,char const * aOldSubStr ,
+ char const * aNewSubStr ,char * aoDestination );
+#ifndef _WIN32
+int tfd_isDarwin(void);
+int tfd_kdialogPresent(void);
+int tfd_matedialogPresent(void);
+int tfd_qarmaPresent(void);
+int tfd_shellementaryPresent(void);
+int tfd_xpropPresent(void);
+int tfd_zenityPresent(void);
+int tfd_zenity3Present(void);
+#endif /*_WIN32 */
+
+
+/* not cross platform - unix zenity only */
+/* contributed by Attila Dusnoki */
+#ifndef _WIN32
+char * tinyfd_arrayDialog(
+ char const * aTitle , /* "" */
+ int aNumOfColumns , /* 2 */
+ char const * const * aColumns , /* {"Column 1","Column 2"} */
+ int aNumOfRows , /* 2 */
+ char const * const * aCells )
+ /* {"Row1 Col1","Row1 Col2","Row2 Col1","Row2 Col2"} */
+{
+ static char lBuff [MAX_PATH_OR_CMD] ;
+ char lDialogString [MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ int i ;
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_arrayDialog("INVALID TITLE WITH QUOTES", aNumOfColumns, aColumns, aNumOfRows, aCells);
+ for (i = 0; i < aNumOfColumns; i++)
+ {
+ if (tfd_quoteDetected(aColumns[i])) return tinyfd_arrayDialog("INVALID COLUMNS WITH QUOTES", 0, NULL, 0, NULL);
+ }
+ for (i = 0; i < aNumOfRows; i++)
+ {
+ if (tfd_quoteDetected(aCells[i])) return tinyfd_arrayDialog("INVALID ROWS WITH QUOTES", 0, NULL, 0, NULL);
+ }
+
+ lBuff[0]='\0';
+
+ if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
+ strcpy( lDialogString , "zenity" ) ;
+ if ( (tfd_zenity3Present() >= 4) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_matedialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
+ strcpy( lDialogString , "matedialog" ) ;
+ }
+ else if ( tfd_shellementaryPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
+ strcpy( lDialogString , "shellementary" ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
+ strcpy( lDialogString , "qarma" ) ;
+ if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ strcat( lDialogString , " --list --print-column=ALL" ) ;
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, " --title=\"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+ }
+
+ if ( aColumns && (aNumOfColumns > 0) )
+ {
+ for ( i = 0 ; i < aNumOfColumns ; i ++ )
+ {
+ strcat( lDialogString , " --column=\"" ) ;
+ strcat( lDialogString , aColumns [i] ) ;
+ strcat( lDialogString , "\"" ) ;
+ }
+ }
+
+ if ( aCells && (aNumOfRows > 0) )
+ {
+ strcat( lDialogString , " " ) ;
+ for ( i = 0 ; i < aNumOfRows*aNumOfColumns ; i ++ )
+ {
+ strcat( lDialogString , "\"" ) ;
+ strcat( lDialogString , aCells [i] ) ;
+ strcat( lDialogString , "\" " ) ;
+ }
+ }
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"");return (char *)0;}
+ return NULL ;
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+ if ( ! ( lIn = popen( lDialogString , "r" ) ) )
+ {
+ return NULL ;
+ }
+ while ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {}
+ pclose( lIn ) ;
+ if ( lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ /* printf( "lBuff: %s\n" , lBuff ) ; */
+ if ( ! strlen( lBuff ) )
+ {
+ return NULL ;
+ }
+ return lBuff ;
+}
+#endif /*_WIN32 */
+
+
+/* not cross platform - UNIX and OSX only */
+/* contributed by srikanth http://sourceforge.net/u/cr1vct/profile */
+#ifndef _WIN32
+char *tinyfd_checklistDialog(
+ char const *aTitle,
+ int aNumOfOptions,
+ char const *const *aOptions)
+{
+ static char lBuff[MAX_PATH_OR_CMD];
+ static char dest[MAX_PATH_OR_CMD];
+
+ char lDialogString[MAX_PATH_OR_CMD];
+ FILE *lIn;
+ int i ;
+ char *target = lDialogString;
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_checklistDialog("INVALID TITLE WITH QUOTES", aNumOfOptions, aOptions);
+ for (i = 0; i < aNumOfOptions; i++)
+ {
+ if (tfd_quoteDetected(aOptions[i])) return tinyfd_checklistDialog("INVALID COLUMNS WITH QUOTES", 0, NULL);
+ }
+
+ lBuff[0] = '\0';
+ if (tfd_isDarwin())
+ {
+ target += sprintf(target, "osascript -e \'set Choices to {");
+ for (i = 0; i < aNumOfOptions; i++)
+ {
+ if (i != aNumOfOptions - 1)
+ target += sprintf(target, "\"%s\", ", aOptions[i]);
+ else
+ target += sprintf(target, "\"%s\"", aOptions[i]);
+ }
+ target += sprintf(target, "}\' -e \'set Choice to choose from list Choices with prompt \"%s\" with multiple selections allowed\' -e \'Choice\'", aTitle);
+ }
+
+ else if (tfd_kdialogPresent())
+ {
+ target += sprintf(target, "kdialog --checklist \'%s\' ", aTitle);
+ for (i = 0; i < aNumOfOptions; i++)
+ {
+ target += sprintf(target, "\'%s\' \'%s\' OFF ", aOptions[i], aOptions[i]);
+ }
+ }
+ else if (tfd_zenityPresent())
+ {
+ target += sprintf(target, "zenity --list --column= --column= --checklist --title=\'%s\' ", aTitle);
+ for (i = 0; i < aNumOfOptions; i++)
+ {
+ target += sprintf(target, "\'\' \'%s\' ", aOptions[i]);
+ }
+ }
+ if (tinyfd_verbose)
+ printf("lDialogString: %s\n", lDialogString);
+ if (!(lIn = popen(lDialogString, "r")))
+ {
+ return NULL;
+ }
+ while (fgets(lBuff, sizeof(lBuff), lIn) != NULL)
+ {
+ }
+ pclose(lIn);
+ if (lBuff[strlen(lBuff) - 1] == '\n')
+ {
+ lBuff[strlen(lBuff) - 1] = '\0';
+ }
+ /* printf( "lBuff: %s\n" , lBuff ) ; */
+ if (!strlen(lBuff))
+ {
+ return NULL;
+ }
+ if (tfd_kdialogPresent())
+ {
+ tfd_replaceSubStr(lBuff, "\" \"", "|", dest);
+ dest[strlen(dest) - 2] = '\0';
+ return dest + 1;
+ }
+ if (tfd_isDarwin())
+ {
+ tfd_replaceSubStr(lBuff, "\", \"", "|", dest);
+ dest[strlen(dest) - 2] = '\0';
+ dest[strlen(dest) - 3] = '\0';
+ return dest + 2;
+ }
+ return lBuff;
+}
+#endif /*_WIN32 */
diff --git a/ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.h b/ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.h
new file mode 100644
index 0000000..07bfea8
--- /dev/null
+++ b/ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: ZLIB
+Copyright (c) 2014 - 2023 Guillaume Vareille http://ysengrin.com
+ _________
+ / \ tinyfiledialogs v3.9.0 [Nov 3, 2022]
+ |tiny file|
+ | dialogs |
+ \____ ___/ http://tinyfiledialogs.sourceforge.net
+ \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+
+If you like tinyfiledialogs, please upvote my stackoverflow answer
+https://stackoverflow.com/a/47651444
+
+ - License -
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* not cross platform - unix zenity only */
+/* contributed by Attila Dusnoki */
+#ifndef _WIN32
+char * tinyfd_arrayDialog(
+ char const * aTitle , /* NULL or "" */
+ int aNumOfColumns , /* 2 */
+ char const * const * aColumns, /* {"Column 1","Column 2"} */
+ int aNumOfRows, /* 2 */
+ char const * const * aCells);
+ /* {"Row1 Col1","Row1 Col2","Row2 Col1","Row2 Col2"} */
+#endif /*_WIN32 */
+
+/* not cross platform - UNIX and OSX only */
+/* contributed by srikanth http://sourceforge.net/u/cr1vct/profile */
+#ifndef _WIN32
+char * tinyfd_checklistDialog(
+ char const * aTitle ,
+ int aNumOfOptions ,
+ char const * const * aOptions);
+#endif /*_WIN32 */
diff --git a/ext/tinyfiledialogs/tinyfiledialogs.c b/ext/tinyfiledialogs/tinyfiledialogs.c
new file mode 100644
index 0000000..44bd6aa
--- /dev/null
+++ b/ext/tinyfiledialogs/tinyfiledialogs.c
@@ -0,0 +1,8401 @@
+/* SPDX-License-Identifier: Zlib
+Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
+ ________________________________________________________________
+ | |
+ | 100% compatible C C++ -> You can rename this .c file as .cpp |
+ | 100% compatible C C++ -> You can rename this .c file as .cpp |
+ | 100% compatible C C++ -> You can rename this .c file as .cpp |
+ | 100% compatible C C++ -> You can rename this .c file as .cpp |
+ | 100% compatible C C++ -> You can rename this .c file as .cpp |
+ |________________________________________________________________|
+
+********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
+ _________
+ / \ tinyfiledialogs.c v3.21.3 [Feb 12, 2026] zlib licence
+ |tiny file| Unique code file created [November 9, 2014]
+ | dialogs |
+ \____ ___/ http://tinyfiledialogs.sourceforge.net
+ \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+ ____________________________________________
+ | |
+ | email: tinyfiledialogs at ysengrin.com |
+ |____________________________________________|
+ _________________________________________________________________________________
+ | |
+ | the windows only wchar_t UTF-16 prototypes are at the bottom of the header file |
+ |_________________________________________________________________________________|
+ _________________________________________________________
+ | |
+ | on windows: - since v3.6 char is UTF-8 by default |
+ | - if you want MBCS set tinyfd_winUtf8 to 0 |
+ | - functions like fopen expect MBCS |
+ |_________________________________________________________|
+
+If you like tinyfiledialogs, please upvote my stackoverflow answer
+https://stackoverflow.com/a/47651444
+
+- License -
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+ __________________________________________
+ | ______________________________________ |
+ | | | |
+ | | DO NOT USE USER INPUT IN THE DIALOGS | |
+ | |______________________________________| |
+ |__________________________________________|
+*/
+
+
+#if defined(__GNUC__) || defined(__clang__)
+#ifndef _GNU_SOURCE
+ #define _GNU_SOURCE /* used only to resolve symbolic links. Can be commented out */
+ #ifndef _POSIX_C_SOURCE
+ #ifdef __NetBSD__
+ #define _POSIX_C_SOURCE 200112L
+ #define _XOPEN_SOURCE 600 /* https://pubs.opengroup.org/onlinepubs/007904875/functions/xsh_chap02_02.html */
+ #elif defined(__FreeBSD__)
+ #define _POSIX_C_SOURCE 199506L /* 199506L is enough for freebsd for realpath() */
+ #elif defined(__illumos__) || defined(__solaris__)
+ #define _POSIX_C_SOURCE 200112L /* illumos/solaris needs 200112L for realpath() */
+ #else
+ #define _POSIX_C_SOURCE 2 /* to accept POSIX 2 in old ANSI C standards */
+ #endif
+ #endif
+#endif
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <sys/stat.h>
+
+#ifdef _WIN32
+ #ifdef __BORLANDC__
+ #define _getch getch
+ #endif
+ #ifndef _WIN32_WINNT
+ #define _WIN32_WINNT 0x0500
+ #endif
+ #include <windows.h>
+ #include <commdlg.h>
+ #include <shlobj.h>
+ #include <conio.h>
+ #include <direct.h>
+ #define TINYFD_NOCCSUNICODE
+ #define TINYFD_SLASH "\\"
+#else
+ #include <limits.h>
+ #include <unistd.h>
+ #include <dirent.h> /* on old systems try <sys/dir.h> instead */
+ #include <termios.h>
+ #include <sys/utsname.h>
+ #include <signal.h> /* on old systems try <sys/signal.h> instead */
+ #define TINYFD_SLASH "/"
+#endif /* _WIN32 */
+
+#include "tinyfiledialogs.h"
+
+#define MAX_PATH_OR_CMD 1024 /* _MAX_PATH or MAX_PATH */
+
+#ifndef MAX_MULTIPLE_FILES
+#define MAX_MULTIPLE_FILES 1024
+#endif
+#define LOW_MULTIPLE_FILES 32
+
+char tinyfd_version[8] = "3.21.3";
+
+/******************************************************************************************************/
+/**************************************** UTF-8 on Windows ********************************************/
+/******************************************************************************************************/
+#ifdef _WIN32
+/* if you want to use UTF-8 ( instead of the UTF-16/wchar_t functions at the end of tinyfiledialogs.h )
+Make sure your code is really prepared for UTF-8 (on windows, functions like fopen() expect MBCS and not UTF-8) */
+int tinyfd_winUtf8 = 1; /* on windows char strings can be 1:UTF-8(default) or 0:MBCS */
+/* for MBCS change this to 0, here or in your code */
+#endif
+/******************************************************************************************************/
+/******************************************************************************************************/
+/******************************************************************************************************/
+
+int tinyfd_verbose = 0 ; /* on unix: prints the command line calls */
+int tinyfd_silent = 1 ; /* 1 (default) or 0 : on unix, hide errors and warnings from called dialogs */
+
+/* Curses dialogs are difficult to use, on windows they are only ascii and uses the unix backslah */
+int tinyfd_allowCursesDialogs = 0 ; /* 0 (default) or 1 */
+int tinyfd_forceConsole = 0 ; /* 0 (default) or 1 */
+/* for unix & windows: 0 (graphic mode) or 1 (console mode).
+0: try to use a graphic solution, if it fails then it uses console mode.
+1: forces all dialogs into console mode even when the X server is present.
+ it can use the package dialog or dialog.exe.
+ on windows it only make sense for console applications */
+
+/* int tinyfd_assumeGraphicDisplay = 0; */ /* 0 (default) or 1 */
+/* some systems don't set the environment variable DISPLAY even when a graphic display is present.
+set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display */
+
+
+char tinyfd_response[1024];
+/* if you pass "tinyfd_query" as aTitle,
+the functions will not display the dialogs
+but return 0 for console mode, 1 for graphic mode.
+tinyfd_response is then filled with the retain solution.
+possible values for tinyfd_response are (all lowercase)
+for graphic mode:
+ windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
+ shellementary qarma shanty boxer python2-tkinter python3-tkinter python-dbus
+ perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
+for console mode:
+ dialog whiptail basicinput no_solution */
+
+static int gWarningDisplayed = 0 ;
+static char gTitle[]="missing software! (we will try basic console input)";
+
+#ifdef _WIN32
+char tinyfd_needs[] = "\
+ ___________\n\
+/ \\ \n\
+| tiny file |\n\
+| dialogs |\n\
+\\_____ ____/\n\
+ \\|\
+\ntiny file dialogs on Windows needs:\
+\n a graphic display\
+\nor dialog.exe (curses console mode ** Disabled by default **)\
+\nor a console for basic input";
+#else
+char tinyfd_needs[] = "\
+ ___________\n\
+/ \\ \n\
+| tiny file |\n\
+| dialogs |\n\
+\\_____ ____/\n\
+ \\|\
+\ntiny file dialogs on UNIX needs:\
+\n applescript or kdialog or yad or Xdialog\
+\nor zenity (or matedialog, shellementary, qarma, shanty, boxer)\
+\nor python (2 or 3) + tkinter + python-dbus (optional)\
+\nor dialog (opens console if needed) ** Disabled by default **\
+\nor xterm + bash (opens console for basic input)\
+\nor existing console for basic input.";
+
+#endif
+
+
+#ifdef _MSC_VER
+#pragma warning(disable:4996) /* allows usage of strncpy, strcpy, strcat, sprintf, fopen */
+#pragma warning(disable:4100) /* allows usage of strncpy, strcpy, strcat, sprintf, fopen */
+#pragma warning(disable:4706) /* allows usage of strncpy, strcpy, strcat, sprintf, fopen */
+#endif
+
+
+static int getenvDISPLAY(void)
+{
+ /* return tinyfd_assumeGraphicDisplay || getenv("DISPLAY") || getenv("WAYLAND_DISPLAY") ; */
+ static int lReturnValue = -1 ;
+
+ if ( lReturnValue < 0 )
+ {
+ lReturnValue = 0 ;
+ if ( getenv("DISPLAY") ) lReturnValue += 1 ;
+ if ( getenv("WAYLAND_DISPLAY") ) lReturnValue += 2 ;
+ }
+
+ return lReturnValue ;
+}
+
+
+static char * getCurDir(void)
+{
+ static char lCurDir[MAX_PATH_OR_CMD];
+ return getcwd(lCurDir, sizeof(lCurDir));
+}
+
+
+static char * getPathWithoutFinalSlash(
+ char * aoDestination, /* make sure it is allocated, use _MAX_PATH */
+ char const * aSource) /* aoDestination and aSource can be the same */
+{
+ char const * lTmp ;
+ if ( aSource )
+ {
+ lTmp = strrchr(aSource, '/');
+ if (!lTmp)
+ {
+ lTmp = strrchr(aSource, '\\');
+ }
+ if (lTmp)
+ {
+ strncpy(aoDestination, aSource, lTmp - aSource );
+ aoDestination[lTmp - aSource] = '\0';
+ }
+ else
+ {
+ * aoDestination = '\0';
+ }
+ }
+ else
+ {
+ * aoDestination = '\0';
+ }
+ return aoDestination;
+}
+
+
+static char * getLastName(
+ char * aoDestination, /* make sure it is allocated */
+ char const * aSource)
+{
+ /* copy the last name after '/' or '\' */
+ char const * lTmp ;
+ if ( aSource )
+ {
+ lTmp = strrchr(aSource, '/');
+ if (!lTmp)
+ {
+ lTmp = strrchr(aSource, '\\');
+ }
+ if (lTmp)
+ {
+ strcpy(aoDestination, lTmp + 1);
+ }
+ else
+ {
+ strcpy(aoDestination, aSource);
+ }
+ }
+ else
+ {
+ * aoDestination = '\0';
+ }
+ return aoDestination;
+}
+
+
+static void ensureFinalSlash( char * aioString )
+{
+ if ( aioString && strlen( aioString ) )
+ {
+ char * lastcar = aioString + strlen( aioString ) - 1 ;
+ if ( strncmp( lastcar , TINYFD_SLASH , 1 ) )
+ {
+ strcat( lastcar , TINYFD_SLASH ) ;
+ }
+ }
+}
+
+
+static void Hex2RGB( char const aHexRGB[8] , unsigned char aoResultRGB[3] )
+{
+ char lColorChannel[8] ;
+ if ( aoResultRGB )
+ {
+ if ( aHexRGB )
+ {
+ strcpy(lColorChannel, aHexRGB ) ;
+ aoResultRGB[2] = (unsigned char)strtoul(lColorChannel+5,NULL,16);
+ lColorChannel[5] = '\0';
+ aoResultRGB[1] = (unsigned char)strtoul(lColorChannel+3,NULL,16);
+ lColorChannel[3] = '\0';
+ aoResultRGB[0] = (unsigned char)strtoul(lColorChannel+1,NULL,16);
+/* printf("%d %d %d\n", aoResultRGB[0], aoResultRGB[1], aoResultRGB[2]); */
+ }
+ else
+ {
+ aoResultRGB[0]=0;
+ aoResultRGB[1]=0;
+ aoResultRGB[2]=0;
+ }
+ }
+}
+
+
+static void RGB2Hex( unsigned char const aRGB[3], char aoResultHexRGB[8] )
+{
+ if ( aoResultHexRGB )
+ {
+ if ( aRGB )
+ {
+#if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
+ sprintf(aoResultHexRGB, "#%02hhx%02hhx%02hhx", aRGB[0], aRGB[1], aRGB[2]);
+#else
+ sprintf(aoResultHexRGB, "#%02hx%02hx%02hx", aRGB[0], aRGB[1], aRGB[2]);
+#endif
+ /*printf("aoResultHexRGB %s\n", aoResultHexRGB);*/
+ }
+ else
+ {
+ aoResultHexRGB[0]=0;
+ aoResultHexRGB[1]=0;
+ aoResultHexRGB[2]=0;
+ }
+ }
+}
+
+
+void tfd_replaceSubStr( char const * aSource, char const * aOldSubStr,
+ char const * aNewSubStr, char * aoDestination )
+{
+ char const * pOccurence ;
+ char const * p ;
+ char const * lNewSubStr = "" ;
+ size_t lOldSubLen = strlen( aOldSubStr ) ;
+
+ if ( ! aSource )
+ {
+ * aoDestination = '\0' ;
+ return ;
+ }
+ if ( ! aOldSubStr )
+ {
+ strcpy( aoDestination , aSource ) ;
+ return ;
+ }
+ if ( aNewSubStr )
+ {
+ lNewSubStr = aNewSubStr ;
+ }
+ p = aSource ;
+ * aoDestination = '\0' ;
+ while ( ( pOccurence = strstr( p , aOldSubStr ) ) != NULL )
+ {
+ strncat( aoDestination , p , pOccurence - p ) ;
+ strcat( aoDestination , lNewSubStr ) ;
+ p = pOccurence + lOldSubLen ;
+ }
+ strcat( aoDestination , p ) ;
+}
+
+
+static int filenameValid( char const * aFileNameWithoutPath )
+{
+ if ( ! aFileNameWithoutPath
+ || ! strlen(aFileNameWithoutPath)
+ || strpbrk(aFileNameWithoutPath , "\\/:*?\"<>|") )
+ {
+ return 0 ;
+ }
+ return 1 ;
+}
+
+#ifndef _WIN32
+
+static int fileExists( char const * aFilePathAndName )
+{
+ FILE * lIn ;
+ if ( ! aFilePathAndName || ! strlen(aFilePathAndName) )
+ {
+ return 0 ;
+ }
+ lIn = ( FILE * ) fopen( aFilePathAndName , "r" ) ;
+ if ( ! lIn )
+ {
+ return 0 ;
+ }
+ fclose( lIn ) ;
+ return 1 ;
+}
+
+#endif
+
+
+static void wipefile(char const * aFilename)
+{
+ int i;
+ struct stat st;
+ FILE * lIn;
+
+ if (stat(aFilename, &st) == 0)
+ {
+ lIn = ( FILE * ) fopen(aFilename, "w") ;
+ if ( lIn )
+ {
+ for (i = 0; i < st.st_size; i++)
+ {
+ fputc('A', lIn);
+ }
+ fclose(lIn);
+ }
+ }
+}
+
+
+int tfd_quoteDetected(char const * aString)
+{
+ char const * p;
+
+ if (!aString) return 0;
+
+ p = aString;
+ if ( strchr(p, '\''))
+ {
+ return 1;
+ }
+
+ if ( strchr(p, '\"'))
+ {
+ return 1;
+ }
+
+ if ( strchr(p, '`'))
+ {
+ return 1;
+ }
+
+/* p = aString;
+ while ((p = strchr(p, '$')))
+ {
+ p ++ ;
+ if ( ( * p == '(' ) || ( * p == '_' ) || isalpha( * p) ) return 1 ;
+ }*/
+
+ return 0;
+}
+
+
+char const * tinyfd_getGlobalChar(char const * aCharVariableName) /* to be called from C# (you don't need this in C or C++) */
+{
+ if (!aCharVariableName || !strlen(aCharVariableName)) return NULL;
+ else if (!strcmp(aCharVariableName, "tinyfd_version")) return tinyfd_version;
+ else if (!strcmp(aCharVariableName, "tinyfd_needs")) return tinyfd_needs;
+ else if (!strcmp(aCharVariableName, "tinyfd_response")) return tinyfd_response;
+ else return NULL ;
+}
+
+
+int tinyfd_getGlobalInt(char const * aIntVariableName) /* to be called from C# (you don't need this in C or C++) */
+{
+ if ( !aIntVariableName || !strlen(aIntVariableName) ) return -1 ;
+ else if ( !strcmp(aIntVariableName, "tinyfd_verbose") ) return tinyfd_verbose ;
+ else if ( !strcmp(aIntVariableName, "tinyfd_silent") ) return tinyfd_silent ;
+ else if ( !strcmp(aIntVariableName, "tinyfd_allowCursesDialogs") ) return tinyfd_allowCursesDialogs ;
+ else if ( !strcmp(aIntVariableName, "tinyfd_forceConsole") ) return tinyfd_forceConsole ;
+ /* else if ( !strcmp(aIntVariableName, "tinyfd_assumeGraphicDisplay") ) return tinyfd_assumeGraphicDisplay ; */
+#ifdef _WIN32
+ else if ( !strcmp(aIntVariableName, "tinyfd_winUtf8") ) return tinyfd_winUtf8 ;
+#endif
+ else return -1;
+}
+
+
+int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue) /* to be called from C# (you don't need this in C or C++) */
+{
+ if (!aIntVariableName || !strlen(aIntVariableName)) return -1 ;
+ else if (!strcmp(aIntVariableName, "tinyfd_verbose")) { tinyfd_verbose = aValue; return tinyfd_verbose; }
+ else if (!strcmp(aIntVariableName, "tinyfd_silent")) { tinyfd_silent = aValue; return tinyfd_silent; }
+ else if (!strcmp(aIntVariableName, "tinyfd_allowCursesDialogs")) { tinyfd_allowCursesDialogs = aValue; return tinyfd_allowCursesDialogs; }
+ else if (!strcmp(aIntVariableName, "tinyfd_forceConsole")) { tinyfd_forceConsole = aValue; return tinyfd_forceConsole; }
+ /* else if (!strcmp(aIntVariableName, "tinyfd_assumeGraphicDisplay")) { tinyfd_assumeGraphicDisplay = aValue; return tinyfd_assumeGraphicDisplay; } */
+#ifdef _WIN32
+ else if (!strcmp(aIntVariableName, "tinyfd_winUtf8")) { tinyfd_winUtf8 = aValue; return tinyfd_winUtf8; }
+#endif
+ else return -1;
+}
+
+
+#ifdef _WIN32
+
+static int powershellPresent(void)
+{ /*only on vista and above (or installed on xp)*/
+ static int lPowershellPresent = -1;
+ char lBuff[MAX_PATH_OR_CMD];
+ FILE* lIn;
+ char const* lString = "powershell.exe";
+
+ if (lPowershellPresent < 0)
+ {
+ lIn = ( FILE * ) _popen("where powershell.exe", "r") ;
+ if (! lIn )
+ {
+ lPowershellPresent = 0;
+ return 0;
+ }
+ while (fgets(lBuff, sizeof(lBuff), lIn) != NULL)
+ {
+ }
+ _pclose(lIn);
+ if (lBuff[strlen(lBuff) - 1] == '\n')
+ {
+ lBuff[strlen(lBuff) - 1] = '\0';
+ }
+ if (strcmp(lBuff + strlen(lBuff) - strlen(lString), lString))
+ {
+ lPowershellPresent = 0;
+ }
+ else
+ {
+ lPowershellPresent = 1;
+ }
+ }
+ return lPowershellPresent;
+}
+
+static int windowsVersion(void)
+{
+#if ( !defined(__MINGW32__) || defined(__MINGW64_VERSION_MAJOR) ) && !defined(__DMC__)
+ typedef LONG NTSTATUS ;
+ typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
+ HMODULE hMod;
+ RtlGetVersionPtr lFxPtr;
+ RTL_OSVERSIONINFOW lRovi = { 0 };
+
+ hMod = GetModuleHandleW(L"ntdll.dll");
+ if (hMod) {
+ lFxPtr = (RtlGetVersionPtr)GetProcAddress(hMod, "RtlGetVersion");
+ if (lFxPtr)
+ {
+ lRovi.dwOSVersionInfoSize = sizeof(lRovi);
+ if (!lFxPtr(&lRovi))
+ {
+ return lRovi.dwMajorVersion;
+ }
+ }
+ }
+#endif
+ if (powershellPresent()) return 6; /*minimum is vista or installed on xp*/
+ return 0;
+}
+
+
+static void replaceChr(char * aString, char aOldChr, char aNewChr)
+{
+ char * p;
+
+ if (!aString) return;
+ if (aOldChr == aNewChr) return;
+
+ p = aString;
+ while ((p = strchr(p, aOldChr)))
+ {
+ *p = aNewChr;
+ p++;
+ }
+ return;
+}
+
+
+#if !defined(WC_ERR_INVALID_CHARS)
+/* undefined prior to Vista, so not yet in MINGW header file */
+#define WC_ERR_INVALID_CHARS 0x00000000 /* 0x00000080 for MINGW maybe ? */
+#endif
+
+static int sizeUtf16From8(char const * aUtf8string)
+{
+ return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
+ aUtf8string, -1, NULL, 0);
+}
+
+
+static int sizeUtf16FromMbcs(char const * aMbcsString)
+{
+ return MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS,
+ aMbcsString, -1, NULL, 0);
+}
+
+
+static int sizeUtf8(wchar_t const * aUtf16string)
+{
+ return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
+ aUtf16string, -1, NULL, 0, NULL, NULL);
+}
+
+
+static int sizeMbcs(wchar_t const * aMbcsString)
+{
+ int lRes = WideCharToMultiByte(CP_ACP, 0,
+ aMbcsString, -1, NULL, 0, NULL, NULL);
+ /* DWORD licic = GetLastError(); */
+ return lRes;
+}
+
+
+wchar_t* tinyfd_mbcsTo16(char const* aMbcsString)
+{
+ static wchar_t* lMbcsString = NULL;
+ int lSize;
+
+ free(lMbcsString);
+ if (!aMbcsString) { lMbcsString = NULL; return NULL; }
+ lSize = sizeUtf16FromMbcs(aMbcsString);
+ if (lSize)
+ {
+ lMbcsString = (wchar_t*) malloc(lSize * sizeof(wchar_t));
+ lSize = MultiByteToWideChar(CP_ACP, 0, aMbcsString, -1, lMbcsString, lSize);
+ }
+ else wcscpy(lMbcsString, L"");
+ return lMbcsString;
+}
+
+
+wchar_t * tinyfd_utf8to16(char const * aUtf8string)
+{
+ static wchar_t * lUtf16string = NULL;
+ int lSize;
+
+ free(lUtf16string);
+ if (!aUtf8string) {lUtf16string = NULL; return NULL;}
+ lSize = sizeUtf16From8(aUtf8string);
+ if (lSize)
+ {
+ lUtf16string = (wchar_t*) malloc(lSize * sizeof(wchar_t));
+ lSize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
+ aUtf8string, -1, lUtf16string, lSize);
+ return lUtf16string;
+ }
+ else
+ {
+ /* let's try mbcs anyway */
+ lUtf16string = NULL;
+ return tinyfd_mbcsTo16(aUtf8string);
+ }
+}
+
+
+char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string)
+{
+ static char * lMbcsString = NULL;
+ int lSize;
+
+ free(lMbcsString);
+ if (!aUtf16string) { lMbcsString = NULL; return NULL; }
+ lSize = sizeMbcs(aUtf16string);
+ if (lSize)
+ {
+ lMbcsString = (char*) malloc(lSize);
+ lSize = WideCharToMultiByte(CP_ACP, 0, aUtf16string, -1, lMbcsString, lSize, NULL, NULL);
+ }
+ else strcpy(lMbcsString, "");
+ return lMbcsString;
+}
+
+
+char * tinyfd_utf8toMbcs(char const * aUtf8string)
+{
+ wchar_t const * lUtf16string;
+ lUtf16string = tinyfd_utf8to16(aUtf8string);
+ return tinyfd_utf16toMbcs(lUtf16string);
+}
+
+
+char * tinyfd_utf16to8(wchar_t const * aUtf16string)
+{
+ static char * lUtf8string = NULL;
+ int lSize;
+
+ free(lUtf8string);
+ if (!aUtf16string) { lUtf8string = NULL; return NULL; }
+ lSize = sizeUtf8(aUtf16string);
+ if (lSize)
+ {
+ lUtf8string = (char*) malloc(lSize);
+ lSize = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, aUtf16string, -1, lUtf8string, lSize, NULL, NULL);
+ }
+ else strcpy(lUtf8string, "");
+ return lUtf8string;
+}
+
+
+char * tinyfd_mbcsTo8(char const * aMbcsString)
+{
+ wchar_t const * lUtf16string;
+ lUtf16string = tinyfd_mbcsTo16(aMbcsString);
+ return tinyfd_utf16to8(lUtf16string);
+}
+
+
+void tinyfd_beep(void)
+{
+ if (windowsVersion() > 5) Beep(440, 300);
+ else MessageBeep(MB_OK);
+}
+
+
+static void wipefileW(wchar_t const * aFilename)
+{
+ int i;
+ FILE * lIn;
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__) || defined(__DMC__)
+ struct _stat st;
+ if (_wstat(aFilename, &st) == 0)
+#else
+ struct __stat64 st;
+ if (_wstat64(aFilename, &st) == 0)
+#endif
+ {
+ lIn = ( FILE * ) _wfopen(aFilename, L"w");
+ if ( lIn )
+ {
+ for (i = 0; i < st.st_size; i++)
+ {
+ fputc('A', lIn);
+ }
+ fclose(lIn);
+ }
+ }
+}
+
+
+static wchar_t * getPathWithoutFinalSlashW(
+ wchar_t * aoDestination, /* make sure it is allocated, use _MAX_PATH */
+ wchar_t const * aSource) /* aoDestination and aSource can be the same */
+{
+ wchar_t const * lTmp;
+ if (aSource)
+ {
+ lTmp = wcsrchr(aSource, L'/');
+ if (!lTmp)
+ {
+ lTmp = wcsrchr(aSource, L'\\');
+ }
+ if (lTmp)
+ {
+ wcsncpy(aoDestination, aSource, lTmp - aSource);
+ aoDestination[lTmp - aSource] = L'\0';
+ }
+ else
+ {
+ *aoDestination = L'\0';
+ }
+ }
+ else
+ {
+ *aoDestination = L'\0';
+ }
+ return aoDestination;
+}
+
+
+static wchar_t * getLastNameW(
+ wchar_t * aoDestination, /* make sure it is allocated */
+ wchar_t const * aSource)
+{
+ /* copy the last name after '/' or '\' */
+ wchar_t const * lTmp;
+ if (aSource)
+ {
+ lTmp = wcsrchr(aSource, L'/');
+ if (!lTmp)
+ {
+ lTmp = wcsrchr(aSource, L'\\');
+ }
+ if (lTmp)
+ {
+ wcscpy(aoDestination, lTmp + 1);
+ }
+ else
+ {
+ wcscpy(aoDestination, aSource);
+ }
+ }
+ else
+ {
+ *aoDestination = L'\0';
+ }
+ return aoDestination;
+}
+
+
+static void Hex2RGBW(wchar_t const aHexRGB[8], unsigned char aoResultRGB[3])
+{
+ wchar_t lColorChannel[8];
+ if (aoResultRGB)
+ {
+ if (aHexRGB)
+ {
+ wcscpy(lColorChannel, aHexRGB);
+ aoResultRGB[2] = (unsigned char)wcstoul(lColorChannel + 5, NULL, 16);
+ lColorChannel[5] = '\0';
+ aoResultRGB[1] = (unsigned char)wcstoul(lColorChannel + 3, NULL, 16);
+ lColorChannel[3] = '\0';
+ aoResultRGB[0] = (unsigned char)wcstoul(lColorChannel + 1, NULL, 16);
+ /* printf("%d %d %d\n", aoResultRGB[0], aoResultRGB[1], aoResultRGB[2]); */
+ }
+ else
+ {
+ aoResultRGB[0] = 0;
+ aoResultRGB[1] = 0;
+ aoResultRGB[2] = 0;
+ }
+ }
+}
+
+
+static void RGB2HexW( unsigned char const aRGB[3], wchar_t aoResultHexRGB[8])
+{
+#if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
+ wchar_t const * const lPrintFormat = L"#%02hhx%02hhx%02hhx";
+#else
+ wchar_t const * const lPrintFormat = L"#%02hx%02hx%02hx";
+#endif
+
+ if (aoResultHexRGB)
+ {
+ if (aRGB)
+ {
+ /* wprintf(L"aoResultHexRGB %s\n", aoResultHexRGB); */
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ swprintf(aoResultHexRGB, 8, lPrintFormat, aRGB[0], aRGB[1], aRGB[2]);
+#else
+ swprintf(aoResultHexRGB, lPrintFormat, aRGB[0], aRGB[1], aRGB[2]);
+#endif
+ }
+ else
+ {
+ aoResultHexRGB[0] = 0;
+ aoResultHexRGB[1] = 0;
+ aoResultHexRGB[2] = 0;
+ }
+ }
+}
+
+
+static int dirExists(char const * aDirPath)
+{
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__) || defined(__DMC__)
+ struct _stat lInfo;
+#else
+ struct __stat64 lInfo;
+#endif
+ wchar_t * lTmpWChar;
+ int lStatRet;
+ size_t lDirLen;
+
+ if (!aDirPath)
+ return 0;
+ lDirLen = strlen(aDirPath);
+ if (!lDirLen)
+ return 1;
+ if ( (lDirLen == 2) && (aDirPath[1] == ':') )
+ return 1;
+
+ if (tinyfd_winUtf8)
+ {
+ lTmpWChar = tinyfd_utf8to16(aDirPath);
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__)
+ lStatRet = _wstat(lTmpWChar, &lInfo);
+#else
+ lStatRet = _wstat64(lTmpWChar, &lInfo);
+#endif
+ if (lStatRet != 0)
+ return 0;
+ else if (lInfo.st_mode & S_IFDIR)
+ return 1;
+ else
+ return 0;
+ }
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__)
+ else if (_stat(aDirPath, &lInfo) != 0)
+#else
+ else if (_stat64(aDirPath, &lInfo) != 0)
+#endif
+ return 0;
+ else if (lInfo.st_mode & S_IFDIR)
+ return 1;
+ else
+ return 0;
+}
+
+
+static int fileExists(char const * aFilePathAndName)
+{
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__) || defined(__DMC__)
+ struct _stat lInfo;
+#else
+ struct __stat64 lInfo;
+#endif
+ wchar_t * lTmpWChar;
+ int lStatRet;
+ FILE * lIn;
+
+ if (!aFilePathAndName || !strlen(aFilePathAndName))
+ {
+ return 0;
+ }
+
+ if (tinyfd_winUtf8)
+ {
+ lTmpWChar = tinyfd_utf8to16(aFilePathAndName);
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__)
+ lStatRet = _wstat(lTmpWChar, &lInfo);
+#else
+ lStatRet = _wstat64(lTmpWChar, &lInfo);
+#endif
+
+ if (lStatRet != 0)
+ return 0;
+ else if (lInfo.st_mode & _S_IFREG)
+ return 1;
+ else
+ return 0;
+ }
+ else
+ {
+ lIn = ( FILE * ) fopen(aFilePathAndName, "r");
+ if (!lIn)
+ {
+ return 0;
+ }
+ fclose(lIn);
+ return 1;
+ }
+}
+
+static void replaceWchar(wchar_t * aString,
+ wchar_t aOldChr,
+ wchar_t aNewChr)
+{
+ wchar_t * p;
+
+ if (!aString)
+ {
+ return ;
+ }
+
+ if (aOldChr == aNewChr)
+ {
+ return ;
+ }
+
+ p = aString;
+ while ((p = wcsrchr(p, aOldChr)))
+ {
+ *p = aNewChr;
+#ifdef TINYFD_NOCCSUNICODE
+ p++;
+#endif
+ p++;
+ }
+ return ;
+}
+
+
+static int quoteDetectedW(wchar_t const * aString)
+{
+ wchar_t const * p;
+
+ if (!aString) return 0;
+
+ p = aString;
+ while ((p = wcsrchr(p, L'\'')))
+ {
+ return 1;
+ }
+
+ p = aString;
+ while ((p = wcsrchr(p, L'\"')))
+ {
+ return 1;
+ }
+
+ return 0;
+}
+
+#endif /* _WIN32 */
+
+/* source and destination can be the same or ovelap*/
+static char * ensureFilesExist(char * aDestination,
+ char const * aSourcePathsAndNames)
+{
+ char * lDestination = aDestination;
+ char const * p;
+ char const * p2;
+ size_t lLen;
+
+ if (!aSourcePathsAndNames)
+ {
+ return NULL;
+ }
+ lLen = strlen(aSourcePathsAndNames);
+ if (!lLen)
+ {
+ return NULL;
+ }
+
+ p = aSourcePathsAndNames;
+ while ((p2 = strchr(p, '|')) != NULL)
+ {
+ lLen = p2 - p;
+ memmove(lDestination, p, lLen);
+ lDestination[lLen] = '\0';
+ if (fileExists(lDestination))
+ {
+ lDestination += lLen;
+ *lDestination = '|';
+ lDestination++;
+ }
+ p = p2 + 1;
+ }
+ if (fileExists(p))
+ {
+ lLen = strlen(p);
+ memmove(lDestination, p, lLen);
+ lDestination[lLen] = '\0';
+ }
+ else
+ {
+ *(lDestination - 1) = '\0';
+ }
+ return aDestination;
+}
+
+#ifdef _WIN32
+
+static int __stdcall EnumThreadWndProc(HWND hwnd, LPARAM lParam)
+{
+ wchar_t lTitleName[MAX_PATH];
+ wchar_t const* lDialogTitle = (wchar_t const *) lParam;
+
+ GetWindowTextW(hwnd, lTitleName, MAX_PATH);
+ /* wprintf(L"lTitleName %ls \n", lTitleName); */
+
+ if (wcscmp(lDialogTitle, lTitleName) == 0)
+ {
+ SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
+ return 0;
+ }
+ return 1;
+}
+
+
+static void hiddenConsoleW(wchar_t const * aString, wchar_t const * aDialogTitle, int aInFront)
+{
+ STARTUPINFOW StartupInfo;
+ PROCESS_INFORMATION ProcessInfo;
+
+ if (!aString || !wcslen(aString) ) return;
+
+ memset(&StartupInfo, 0, sizeof(StartupInfo));
+ StartupInfo.cb = sizeof(STARTUPINFOW);
+ StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
+ StartupInfo.wShowWindow = SW_HIDE;
+
+ if (!CreateProcessW(NULL, (LPWSTR)aString, NULL, NULL, FALSE,
+ CREATE_NEW_CONSOLE, NULL, NULL,
+ &StartupInfo, &ProcessInfo))
+ {
+ return; /* GetLastError(); */
+ }
+
+ WaitForInputIdle(ProcessInfo.hProcess, INFINITE);
+ if (aInFront)
+ {
+ while (EnumWindows(EnumThreadWndProc, (LPARAM)aDialogTitle)) {}
+ }
+ WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
+ CloseHandle(ProcessInfo.hThread);
+ CloseHandle(ProcessInfo.hProcess);
+}
+
+
+int tinyfd_messageBoxW(
+ wchar_t const * aTitle, /* NULL or "" */
+ wchar_t const * aMessage, /* NULL or "" may contain \n and \t */
+ wchar_t const * aDialogType, /* "ok" "okcancel" "yesno" "yesnocancel" */
+ wchar_t const * aIconType, /* "info" "warning" "error" "question" */
+ int aDefaultButton) /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
+{
+ int lBoxReturnValue;
+ UINT aCode;
+
+ if (aTitle&&!wcscmp(aTitle, L"tinyfd_query")){ strcpy(tinyfd_response, "windows_wchar"); return 1; }
+
+ /*if (quoteDetectedW(aTitle)) return tinyfd_messageBoxW(L"INVALID TITLE WITH QUOTES", aMessage, aDialogType, aIconType, aDefaultButton);
+ if (quoteDetectedW(aMessage)) return tinyfd_messageBoxW(aTitle, L"INVALID MESSAGE WITH QUOTES", aDialogType, aIconType, aDefaultButton);*/
+
+ if (aIconType && !wcscmp(L"warning", aIconType))
+ {
+ aCode = MB_ICONWARNING;
+ }
+ else if (aIconType && !wcscmp(L"error", aIconType))
+ {
+ aCode = MB_ICONERROR;
+ }
+ else if (aIconType && !wcscmp(L"question", aIconType))
+ {
+ aCode = MB_ICONQUESTION;
+ }
+ else
+ {
+ aCode = MB_ICONINFORMATION;
+ }
+
+ if (aDialogType && !wcscmp(L"okcancel", aDialogType))
+ {
+ aCode += MB_OKCANCEL;
+ if (!aDefaultButton)
+ {
+ aCode += MB_DEFBUTTON2;
+ }
+ }
+ else if (aDialogType && !wcscmp(L"yesno", aDialogType))
+ {
+ aCode += MB_YESNO;
+ if (!aDefaultButton)
+ {
+ aCode += MB_DEFBUTTON2;
+ }
+ }
+ else if (aDialogType && !wcscmp(L"yesnocancel", aDialogType))
+ {
+ aCode += MB_YESNOCANCEL;
+ if (aDefaultButton == 1)
+ {
+ aCode += MB_DEFBUTTON1;
+ }
+ else if (aDefaultButton == 2)
+ {
+ aCode += MB_DEFBUTTON2;
+ }
+ else
+ {
+ aCode += MB_DEFBUTTON3;
+ }
+ }
+ else
+ {
+ aCode += MB_OK;
+ }
+
+ aCode += MB_TOPMOST;
+
+ lBoxReturnValue = MessageBoxW(GetForegroundWindow(), aMessage, aTitle, aCode);
+
+ if ( (lBoxReturnValue == IDNO) && (aDialogType && !wcscmp(L"yesnocancel", aDialogType)) )
+ {
+ return 2;
+ }
+ else if ( (lBoxReturnValue == IDOK) || (lBoxReturnValue == IDYES) )
+ {
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+
+/* int tinyfd_notifyPopupW_ORIGINAL(
+ wchar_t const * aTitle,
+ wchar_t const * aMessage,
+ wchar_t const * aIconType)
+{
+ wchar_t * lDialogString;
+ size_t lTitleLen;
+ size_t lMessageLen;
+ size_t lDialogStringLen;
+
+ if (aTitle && !wcscmp(aTitle, L"tinyfd_query")) { strcpy(tinyfd_response, "windows_wchar"); return 1; }
+
+ if (quoteDetectedW(aTitle)) return tinyfd_notifyPopupW(L"INVALID TITLE WITH QUOTES", aMessage, aIconType);
+ if (quoteDetectedW(aMessage)) return tinyfd_notifyPopupW(aTitle, L"INVALID MESSAGE WITH QUOTES", aIconType);
+
+ lTitleLen = aTitle ? wcslen(aTitle) : 0;
+ lMessageLen = aMessage ? wcslen(aMessage) : 0;
+ lDialogStringLen = 3 * MAX_PATH_OR_CMD + lTitleLen + lMessageLen;
+ lDialogString = (wchar_t *) malloc(2 * lDialogStringLen);
+ if (!lDialogString) return 0;
+
+ wcscpy(lDialogString, L"powershell.exe -executionpolicy bypass -command \"\
+function Show-BalloonTip {\
+[cmdletbinding()] \
+param( \
+[string]$Title = ' ', \
+[string]$Message = ' ', \
+[ValidateSet('info', 'warning', 'error')] \
+[string]$IconType = 'info');\
+[system.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null ; \
+$balloon = New-Object System.Windows.Forms.NotifyIcon ; \
+$path = Get-Process -id $pid | Select-Object -ExpandProperty Path ; \
+$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path) ;");
+
+ wcscat(lDialogString, L"\
+$balloon.Icon = $icon ; \
+$balloon.BalloonTipIcon = $IconType ; \
+$balloon.BalloonTipText = $Message ; \
+$balloon.BalloonTipTitle = $Title ; \
+$balloon.Text = 'tinyfiledialogs' ; \
+$balloon.Visible = $true ; \
+$balloon.ShowBalloonTip(5000)};\
+Show-BalloonTip");
+
+ if (aTitle && wcslen(aTitle))
+ {
+ wcscat(lDialogString, L" -Title '");
+ wcscat(lDialogString, aTitle);
+ wcscat(lDialogString, L"'");
+ }
+ if (aMessage && wcslen(aMessage))
+ {
+ wcscat(lDialogString, L" -Message '");
+ wcscat(lDialogString, aMessage);
+ wcscat(lDialogString, L"'");
+ }
+ if (aMessage && wcslen(aIconType))
+ {
+ wcscat(lDialogString, L" -IconType '");
+ wcscat(lDialogString, aIconType);
+ wcscat(lDialogString, L"'");
+ }
+ wcscat(lDialogString, L"\"");
+
+ hiddenConsoleW(lDialogString, aTitle, 0);
+ free(lDialogString);
+ return 1;
+}*/
+
+
+/* return has only meaning for tinyfd_query */
+int tinyfd_notifyPopupW(
+ wchar_t const* aTitle, /* NULL or L"" */
+ wchar_t const* aMessage, /* NULL or L"" may contain \n \t */
+ wchar_t const* aIconType) /* L"info" L"warning" L"error" */
+{
+ wchar_t* lDialogString;
+ size_t lTitleLen;
+ size_t lMessageLen;
+ size_t lDialogStringLen;
+
+ FILE* lIn;
+
+ if (aTitle && !wcscmp(aTitle, L"tinyfd_query")) { strcpy(tinyfd_response, "windows_wchar"); return 1; }
+
+ if (quoteDetectedW(aTitle)) return tinyfd_notifyPopupW(L"INVALID TITLE WITH QUOTES", aMessage, aIconType);
+ if (quoteDetectedW(aMessage)) return tinyfd_notifyPopupW(aTitle, L"INVALID MESSAGE WITH QUOTES", aIconType);
+
+ lTitleLen = aTitle ? wcslen(aTitle) : 0;
+ lMessageLen = aMessage ? wcslen(aMessage) : 0;
+ lDialogStringLen = 3 * MAX_PATH_OR_CMD + lTitleLen + lMessageLen;
+ lDialogString = (wchar_t*)malloc(2 * lDialogStringLen);
+ if (!lDialogString) return 0;
+
+ swprintf(lDialogString,
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ lDialogStringLen,
+#endif
+ L"%ls\\tinyfd.hta", _wgetenv(L"TEMP"));
+
+ lIn = ( FILE * ) _wfopen(lDialogString, L"w");
+ if (!lIn)
+ {
+ free(lDialogString);
+ return 0;
+ }
+
+ wcscpy(lDialogString, L"\n\
+<html>\n\
+<head>\n\
+<title>");
+ if ( aTitle && wcslen(aTitle) ) wcscat(lDialogString, aTitle);
+ wcscat(lDialogString, L"</title>\n\
+</head>\n\
+<HTA:APPLICATION\n\
+SysMenu = 'no'\n\
+ID = 'tinyfdHTA'\n\
+APPLICATIONNAME = 'tinyfd_notifyPopup'\n\
+MINIMIZEBUTTON = 'no'\n\
+MAXIMIZEBUTTON = 'no'\n\
+BORDER = 'dialog'\n\
+SCROLL = 'no'\n\
+SINGLEINSTANCE = 'yes'\n\
+WINDOWSTATE = 'hidden'>\n\
+<script language = 'VBScript'>\n\
+intWidth = Screen.Width/4\n\
+intHeight = Screen.Height/10\n\
+ResizeTo intWidth, intHeight\n\
+MoveTo Screen.Width * .7, Screen.Height * .8\n\
+result = 0\n\
+Sub Window_onLoad\n\
+idTimer = window.setTimeout(\"PausedSection\", 3000, \"VBScript\")\n\
+End Sub\n");
+
+ wcscat(lDialogString, L"\n\
+Sub PausedSection\n\
+window.Close\n\
+End Sub\n\
+</script>\n\
+<body style = 'background-color:#EEEEEE' onkeypress = 'vbs:Default_Buttons' align = 'top'>\n\
+<table width = '100%' height = '80%' align = 'center' border = '0'>\n\
+<tr border = '0'>\n\
+<td align = 'left' valign = 'middle' style='Font-Family:Arial'>\n");
+
+ wcscat(lDialogString, aMessage ? aMessage : L"");
+
+ wcscat(lDialogString, L"\n\
+</body>\n\
+</html>\n\
+");
+
+ fputws(lDialogString, lIn);
+ fclose(lIn);
+
+ if (aTitle && wcslen(aTitle))
+ {
+ wcscat(lDialogString, L" -Title '");
+ wcscat(lDialogString, aTitle);
+ wcscat(lDialogString, L"'");
+ }
+ if (aMessage && wcslen(aMessage))
+ {
+ wcscat(lDialogString, L" -Message '");
+ wcscat(lDialogString, aMessage);
+ wcscat(lDialogString, L"'");
+ }
+ if (aMessage && wcslen(aIconType))
+ {
+ wcscat(lDialogString, L" -IconType '");
+ wcscat(lDialogString, aIconType);
+ wcscat(lDialogString, L"'");
+ }
+ wcscat(lDialogString, L"\"");
+
+ /* wprintf ( L"lDialogString: %ls\n" , lDialogString ) ; */
+ wcscpy(lDialogString,
+ L"cmd.exe /c mshta.exe \"%TEMP%\\tinyfd.hta\"");
+
+ hiddenConsoleW(lDialogString, aTitle, 0);
+ free(lDialogString);
+ return 1;
+}
+
+
+wchar_t * tinyfd_inputBoxW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aMessage, /* NULL or L"" (\n and \t have no effect) */
+ wchar_t const * aDefaultInput) /* L"" , if NULL it's a passwordBox */
+{
+ static wchar_t lBuff[MAX_PATH_OR_CMD];
+ wchar_t * lDialogString;
+ FILE * lIn;
+ FILE * lFile;
+ int lResult;
+ size_t lTitleLen;
+ size_t lMessageLen;
+ size_t lDialogStringLen;
+
+ if (aTitle&&!wcscmp(aTitle, L"tinyfd_query")){ strcpy(tinyfd_response, "windows_wchar"); return (wchar_t *)1; }
+
+ if (quoteDetectedW(aTitle)) return tinyfd_inputBoxW(L"INVALID TITLE WITH QUOTES", aMessage, aDefaultInput);
+ if (quoteDetectedW(aMessage)) return tinyfd_inputBoxW(aTitle, L"INVALID MESSAGE WITH QUOTES", aDefaultInput);
+ if (quoteDetectedW(aDefaultInput)) return tinyfd_inputBoxW(aTitle, aMessage, L"INVALID DEFAULT_INPUT WITH QUOTES: use the GRAVE ACCENT \\x60 instead.");
+
+ lTitleLen = aTitle ? wcslen(aTitle) : 0 ;
+ lMessageLen = aMessage ? wcslen(aMessage) : 0 ;
+ lDialogStringLen = 3 * MAX_PATH_OR_CMD + lTitleLen + lMessageLen;
+ lDialogString = (wchar_t *) malloc(2 * lDialogStringLen);
+
+ if (aDefaultInput)
+ {
+ swprintf(lDialogString,
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ lDialogStringLen,
+#endif
+ L"%ls\\tinyfd.vbs", _wgetenv(L"TEMP"));
+ }
+ else
+ {
+ swprintf(lDialogString,
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ lDialogStringLen,
+#endif
+ L"%ls\\tinyfd.hta", _wgetenv(L"TEMP"));
+ }
+ lIn = ( FILE * ) _wfopen(lDialogString, L"w");
+ if (!lIn)
+ {
+ free(lDialogString);
+ return NULL;
+ }
+
+ if ( aDefaultInput )
+ {
+ wcscpy(lDialogString, L"Dim result:result=InputBox(\"");
+ if (aMessage && wcslen(aMessage))
+ {
+ wcscpy(lBuff, aMessage);
+ replaceWchar(lBuff, L'\n', L' ');
+ wcscat(lDialogString, lBuff);
+ }
+ wcscat(lDialogString, L"\",\"");
+ if (aTitle) wcscat(lDialogString, aTitle);
+ wcscat(lDialogString, L"\",\"");
+
+ if (aDefaultInput && wcslen(aDefaultInput))
+ {
+ wcscpy(lBuff, aDefaultInput);
+ replaceWchar(lBuff, L'\n', L' ');
+ wcscat(lDialogString, lBuff);
+ }
+ wcscat(lDialogString, L"\"):If IsEmpty(result) then:WScript.Echo 0");
+ wcscat(lDialogString, L":Else: WScript.Echo \"1\" & result : End If");
+ }
+ else
+ {
+ wcscpy(lDialogString, L"\n\
+<html>\n\
+<head>\n\
+<title>");
+ if (aTitle) wcscat(lDialogString, aTitle);
+ wcscat(lDialogString, L"</title>\n\
+</head>\n\
+<HTA:APPLICATION\n\
+ID = 'tinyfdHTA'\n\
+APPLICATIONNAME = 'tinyfd_inputBox'\n\
+MINIMIZEBUTTON = 'no'\n\
+MAXIMIZEBUTTON = 'no'\n\
+BORDER = 'dialog'\n\
+SCROLL = 'no'\n\
+SINGLEINSTANCE = 'yes'\n\
+WINDOWSTATE = 'hidden'>\n\
+\n\
+<script language = 'VBScript'>\n\
+\n\
+intWidth = Screen.Width/4\n\
+intHeight = Screen.Height/6\n\
+ResizeTo intWidth, intHeight\n\
+MoveTo((Screen.Width/2)-(intWidth/2)),((Screen.Height/2)-(intHeight/2))\n\
+result = 0\n\
+\n\
+Sub Window_onLoad\n\
+txt_input.Focus\n\
+End Sub\n\
+\n");
+
+ wcscat(lDialogString, L"\
+Sub Window_onUnload\n\
+Set objFSO = CreateObject(\"Scripting.FileSystemObject\")\n\
+Set oShell = CreateObject(\"WScript.Shell\")\n\
+strTempFolder = oShell.ExpandEnvironmentStrings(\"%TEMP%\")\n\
+Set objFile = objFSO.CreateTextFile(strTempFolder & \"\\tinyfd.txt\",True,True)\n\
+If result = 1 Then\n\
+objFile.Write 1 & txt_input.Value\n\
+Else\n\
+objFile.Write 0\n\
+End If\n\
+objFile.Close\n\
+End Sub\n\
+\n\
+Sub Run_ProgramOK\n\
+result = 1\n\
+window.Close\n\
+End Sub\n\
+\n\
+Sub Run_ProgramCancel\n\
+window.Close\n\
+End Sub\n\
+\n");
+
+ wcscat(lDialogString, L"Sub Default_Buttons\n\
+If Window.Event.KeyCode = 13 Then\n\
+btn_OK.Click\n\
+ElseIf Window.Event.KeyCode = 27 Then\n\
+btn_Cancel.Click\n\
+End If\n\
+End Sub\n\
+\n\
+</script>\n\
+<body style = 'background-color:#EEEEEE' onkeypress = 'vbs:Default_Buttons' align = 'top'>\n\
+<table width = '100%' height = '80%' align = 'center' border = '0'>\n\
+<tr border = '0'>\n\
+<td align = 'left' valign = 'middle' style='Font-Family:Arial'>\n");
+
+ wcscat(lDialogString, aMessage ? aMessage : L"");
+
+ wcscat(lDialogString, L"\n\
+</td>\n\
+<td align = 'right' valign = 'middle' style = 'margin-top: 0em'>\n\
+<table align = 'right' style = 'margin-right: 0em;'>\n\
+<tr align = 'right' style = 'margin-top: 5em;'>\n\
+<input type = 'button' value = 'OK' name = 'btn_OK' onClick = 'vbs:Run_ProgramOK' style = 'width: 5em; margin-top: 2em;'><br>\n\
+<input type = 'button' value = 'Cancel' name = 'btn_Cancel' onClick = 'vbs:Run_ProgramCancel' style = 'width: 5em;'><br><br>\n\
+</tr>\n\
+</table>\n\
+</td>\n\
+</tr>\n\
+</table>\n");
+
+ wcscat(lDialogString, L"<table width = '100%' height = '100%' align = 'center' border = '0'>\n\
+<tr>\n\
+<td align = 'left' valign = 'top'>\n\
+<input type = 'password' id = 'txt_input'\n\
+name = 'txt_input' value = '' style = 'float:left;width:100%' ><BR>\n\
+</td>\n\
+</tr>\n\
+</table>\n\
+</body>\n\
+</html>\n\
+" ) ;
+ }
+ fputws(lDialogString, lIn);
+ fclose(lIn);
+
+ if (aDefaultInput)
+ {
+ swprintf(lDialogString,
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ lDialogStringLen,
+#endif
+ L"%ls\\tinyfd.txt",_wgetenv(L"TEMP"));
+
+#ifdef TINYFD_NOCCSUNICODE
+ lFile = _wfopen(lDialogString, L"w");
+ fputc(0xFF, lFile);
+ fputc(0xFE, lFile);
+#else
+ lFile = _wfopen(lDialogString, L"wt, ccs=UNICODE"); /*or ccs=UTF-16LE*/
+#endif
+ fclose(lFile);
+
+ wcscpy(lDialogString, L"cmd.exe /c cscript.exe //U //Nologo ");
+ wcscat(lDialogString, L"\"%TEMP%\\tinyfd.vbs\" ");
+ wcscat(lDialogString, L">> \"%TEMP%\\tinyfd.txt\"");
+ }
+ else
+ {
+ wcscpy(lDialogString,
+ L"cmd.exe /c mshta.exe \"%TEMP%\\tinyfd.hta\"");
+ }
+
+ /* wprintf ( "lDialogString: %ls\n" , lDialogString ) ; */
+
+ hiddenConsoleW(lDialogString, aTitle, 1);
+
+ swprintf(lDialogString,
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ lDialogStringLen,
+#endif
+ L"%ls\\tinyfd.txt", _wgetenv(L"TEMP"));
+ /* wprintf(L"lDialogString: %ls\n", lDialogString); */
+#ifdef TINYFD_NOCCSUNICODE
+ lIn = ( FILE * ) _wfopen(lDialogString, L"r") ;
+#else
+ lIn = ( FILE * ) _wfopen(lDialogString, L"rt, ccs=UNICODE") /*or ccs=UTF-16LE*/
+#endif
+ if ( ! lIn )
+ {
+ _wremove(lDialogString);
+ free(lDialogString);
+ return NULL;
+ }
+
+ memset(lBuff, 0, MAX_PATH_OR_CMD * sizeof(wchar_t) );
+
+#ifdef TINYFD_NOCCSUNICODE
+ fgets((char *)lBuff, 2*MAX_PATH_OR_CMD, lIn);
+#else
+ fgetws(lBuff, MAX_PATH_OR_CMD, lIn);
+#endif
+ fclose(lIn);
+ wipefileW(lDialogString);
+ _wremove(lDialogString);
+
+ if (aDefaultInput)
+ {
+ swprintf(lDialogString,
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ lDialogStringLen,
+#endif
+ L"%ls\\tinyfd.vbs", _wgetenv(L"TEMP"));
+ }
+ else
+ {
+ swprintf(lDialogString,
+#if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
+ lDialogStringLen,
+#endif
+ L"%ls\\tinyfd.hta", _wgetenv(L"TEMP"));
+ }
+ _wremove(lDialogString);
+ free(lDialogString);
+ /* wprintf( L"lBuff: %ls\n" , lBuff ) ; */
+#ifdef TINYFD_NOCCSUNICODE
+ lResult = !wcsncmp(lBuff+1, L"1", 1);
+#else
+ lResult = !wcsncmp(lBuff, L"1", 1);
+#endif
+
+ /* printf( "lResult: %d \n" , lResult ) ; */
+ if (!lResult)
+ {
+ return NULL ;
+ }
+
+ /* wprintf( "lBuff+1: %ls\n" , lBuff+1 ) ; */
+
+#ifdef TINYFD_NOCCSUNICODE
+ if (aDefaultInput)
+ {
+ lDialogStringLen = wcslen(lBuff) ;
+ lBuff[lDialogStringLen - 1] = L'\0';
+ lBuff[lDialogStringLen - 2] = L'\0';
+ }
+ return lBuff + 2;
+#else
+ if (aDefaultInput) lBuff[wcslen(lBuff) - 1] = L'\0';
+ return lBuff + 1;
+#endif
+}
+
+
+wchar_t * tinyfd_saveFileDialogW(
+ wchar_t const * aTitle, /* NULL or "" */
+ wchar_t const * aDefaultPathAndOrFile, /* NULL or "" */
+ int aNumOfFilterPatterns, /* 0 */
+ wchar_t const * const * aFilterPatterns, /* NULL or {"*.jpg","*.png"} */
+ wchar_t const * aSingleFilterDescription) /* NULL or "image files" */
+{
+ static wchar_t lBuff[MAX_PATH_OR_CMD];
+ wchar_t lDirname[MAX_PATH_OR_CMD];
+ wchar_t lDialogString[MAX_PATH_OR_CMD];
+ wchar_t lFilterPatterns[MAX_PATH_OR_CMD] = L"";
+ wchar_t * p;
+ wchar_t * lRetval;
+ wchar_t const * ldefExt = NULL;
+ int i;
+ HRESULT lHResult;
+ OPENFILENAMEW ofn = {0};
+
+ if (aTitle&&!wcscmp(aTitle, L"tinyfd_query")){ strcpy(tinyfd_response, "windows_wchar"); return (wchar_t *)1; }
+
+ /*if (quoteDetectedW(aTitle)) return tinyfd_saveFileDialogW(L"INVALID TITLE WITH QUOTES", aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription);
+ if (quoteDetectedW(aDefaultPathAndOrFile)) return tinyfd_saveFileDialogW(aTitle, L"INVALID DEFAULT_PATH WITH QUOTES", aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription);
+ if (quoteDetectedW(aSingleFilterDescription)) return tinyfd_saveFileDialogW(aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, L"INVALID FILTER_DESCRIPTION WITH QUOTES");
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (quoteDetectedW(aFilterPatterns[i])) return tinyfd_saveFileDialogW(L"INVALID FILTER_PATTERN WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultPathAndOrFile, 0, NULL, NULL);
+ }*/
+
+ lHResult = CoInitializeEx(NULL, 0);
+
+ getPathWithoutFinalSlashW(lDirname, aDefaultPathAndOrFile);
+ getLastNameW(lBuff, aDefaultPathAndOrFile);
+
+ if (aNumOfFilterPatterns > 0)
+ {
+ ldefExt = aFilterPatterns[0];
+
+ if (aSingleFilterDescription && wcslen(aSingleFilterDescription))
+ {
+ wcscpy(lFilterPatterns, aSingleFilterDescription);
+ wcscat(lFilterPatterns, L"\n");
+ }
+ wcscat(lFilterPatterns, aFilterPatterns[0]);
+ for (i = 1; i < aNumOfFilterPatterns; i++)
+ {
+ wcscat(lFilterPatterns, L";");
+ wcscat(lFilterPatterns, aFilterPatterns[i]);
+ }
+ wcscat(lFilterPatterns, L"\n");
+ if (!(aSingleFilterDescription && wcslen(aSingleFilterDescription)))
+ {
+ wcscpy(lDialogString, lFilterPatterns);
+ wcscat(lFilterPatterns, lDialogString);
+ }
+ wcscat(lFilterPatterns, L"All Files\n*.*\n");
+ p = lFilterPatterns;
+ while ((p = wcschr(p, L'\n')) != NULL)
+ {
+ *p = L'\0';
+ p++;
+ }
+ }
+
+ ofn.lStructSize = sizeof(OPENFILENAMEW);
+ ofn.hwndOwner = GetForegroundWindow();
+ ofn.hInstance = 0;
+ ofn.lpstrFilter = wcslen(lFilterPatterns) ? lFilterPatterns : NULL;
+ ofn.lpstrCustomFilter = NULL;
+ ofn.nMaxCustFilter = 0;
+ ofn.nFilterIndex = 1;
+ ofn.lpstrFile = lBuff;
+
+ ofn.nMaxFile = MAX_PATH_OR_CMD;
+ ofn.lpstrFileTitle = NULL;
+ ofn.nMaxFileTitle = MAX_PATH_OR_CMD/2;
+ ofn.lpstrInitialDir = wcslen(lDirname) ? lDirname : NULL;
+ ofn.lpstrTitle = aTitle && wcslen(aTitle) ? aTitle : NULL;
+ ofn.Flags = OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST ;
+ ofn.nFileOffset = 0;
+ ofn.nFileExtension = 0;
+ ofn.lpstrDefExt = ldefExt;
+ ofn.lCustData = 0L;
+ ofn.lpfnHook = NULL;
+ ofn.lpTemplateName = NULL;
+
+ if (GetSaveFileNameW(&ofn) == 0)
+ {
+ lRetval = NULL;
+ }
+ else
+ {
+ lRetval = lBuff;
+ }
+
+ if (lHResult == S_OK || lHResult == S_FALSE)
+ {
+ CoUninitialize();
+ }
+ return lRetval;
+}
+
+
+wchar_t * tinyfd_openFileDialogW(
+ wchar_t const * aTitle, /* NULL or "" */
+ wchar_t const * aDefaultPathAndOrFile, /* NULL or "" */
+ int aNumOfFilterPatterns, /* 0 */
+ wchar_t const * const * aFilterPatterns, /* NULL or {"*.jpg","*.png"} */
+ wchar_t const * aSingleFilterDescription, /* NULL or "image files" */
+ int aAllowMultipleSelects) /* 0 or 1 ; -1 to free allocated memory and return */
+{
+ size_t lLengths[MAX_MULTIPLE_FILES];
+ wchar_t lDirname[MAX_PATH_OR_CMD];
+ wchar_t lFilterPatterns[MAX_PATH_OR_CMD] = L"";
+ wchar_t lDialogString[MAX_PATH_OR_CMD];
+ wchar_t * lPointers[MAX_MULTIPLE_FILES+1];
+ wchar_t * p;
+ int i, j;
+ size_t lBuffLen;
+ DWORD lFullBuffLen;
+ HRESULT lHResult;
+ OPENFILENAMEW ofn = { 0 };
+ static wchar_t * lBuff = NULL;
+
+ free(lBuff);
+ lBuff = NULL;
+ if (aAllowMultipleSelects < 0) return (wchar_t *)0;
+
+ if (aTitle&&!wcscmp(aTitle, L"tinyfd_query")){ strcpy(tinyfd_response, "windows_wchar"); return (wchar_t *)1; }
+
+ /*if (quoteDetectedW(aTitle)) return tinyfd_openFileDialogW(L"INVALID TITLE WITH QUOTES", aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription, aAllowMultipleSelects);
+ if (quoteDetectedW(aDefaultPathAndOrFile)) return tinyfd_openFileDialogW(aTitle, L"INVALID DEFAULT_PATH WITH QUOTES", aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription, aAllowMultipleSelects);
+ if (quoteDetectedW(aSingleFilterDescription)) return tinyfd_openFileDialogW(aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, L"INVALID FILTER_DESCRIPTION WITH QUOTES", aAllowMultipleSelects);
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (quoteDetectedW(aFilterPatterns[i])) return tinyfd_openFileDialogW(L"INVALID FILTER_PATTERN WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultPathAndOrFile, 0, NULL, NULL, aAllowMultipleSelects);
+ }*/
+
+ if (aAllowMultipleSelects)
+ {
+ lFullBuffLen = MAX_MULTIPLE_FILES * MAX_PATH_OR_CMD + 1;
+ lBuff = (wchar_t*) malloc(lFullBuffLen * sizeof(wchar_t));
+ if (!lBuff)
+ {
+ lFullBuffLen = LOW_MULTIPLE_FILES * MAX_PATH_OR_CMD + 1;
+ lBuff = (wchar_t*) malloc( lFullBuffLen * sizeof(wchar_t));
+ }
+ }
+ else
+ {
+ lFullBuffLen = MAX_PATH_OR_CMD + 1;
+ lBuff = (wchar_t*) malloc(lFullBuffLen * sizeof(wchar_t));
+ }
+ if (!lBuff) return NULL;
+
+ lHResult = CoInitializeEx(NULL, 0);
+
+ getPathWithoutFinalSlashW(lDirname, aDefaultPathAndOrFile);
+ getLastNameW(lBuff, aDefaultPathAndOrFile);
+
+ if (aNumOfFilterPatterns > 0)
+ {
+ if (aSingleFilterDescription && wcslen(aSingleFilterDescription))
+ {
+ wcscpy(lFilterPatterns, aSingleFilterDescription);
+ wcscat(lFilterPatterns, L"\n");
+ }
+ wcscat(lFilterPatterns, aFilterPatterns[0]);
+ for (i = 1; i < aNumOfFilterPatterns; i++)
+ {
+ wcscat(lFilterPatterns, L";");
+ wcscat(lFilterPatterns, aFilterPatterns[i]);
+ }
+ wcscat(lFilterPatterns, L"\n");
+ if (!(aSingleFilterDescription && wcslen(aSingleFilterDescription)))
+ {
+ wcscpy(lDialogString, lFilterPatterns);
+ wcscat(lFilterPatterns, lDialogString);
+ }
+ wcscat(lFilterPatterns, L"All Files\n*.*\n");
+ p = lFilterPatterns;
+ while ((p = wcschr(p, L'\n')) != NULL)
+ {
+ *p = L'\0';
+ p++;
+ }
+ }
+
+ ofn.lStructSize = sizeof(OPENFILENAME);
+ ofn.hwndOwner = GetForegroundWindow();
+ ofn.hInstance = 0;
+ ofn.lpstrFilter = wcslen(lFilterPatterns) ? lFilterPatterns : NULL;
+ ofn.lpstrCustomFilter = NULL;
+ ofn.nMaxCustFilter = 0;
+ ofn.nFilterIndex = 1;
+ ofn.lpstrFile = lBuff;
+ ofn.nMaxFile = lFullBuffLen;
+ ofn.lpstrFileTitle = NULL;
+ ofn.nMaxFileTitle = MAX_PATH_OR_CMD / 2;
+ ofn.lpstrInitialDir = wcslen(lDirname) ? lDirname : NULL;
+ ofn.lpstrTitle = aTitle && wcslen(aTitle) ? aTitle : NULL;
+ ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
+ ofn.nFileOffset = 0;
+ ofn.nFileExtension = 0;
+ ofn.lpstrDefExt = NULL;
+ ofn.lCustData = 0L;
+ ofn.lpfnHook = NULL;
+ ofn.lpTemplateName = NULL;
+
+ if (aAllowMultipleSelects)
+ {
+ ofn.Flags |= OFN_ALLOWMULTISELECT;
+ }
+
+ if (GetOpenFileNameW(&ofn) == 0)
+ {
+ free(lBuff);
+ lBuff = NULL;
+ }
+ else
+ {
+ lBuffLen = wcslen(lBuff);
+ lPointers[0] = lBuff + lBuffLen + 1;
+ if (aAllowMultipleSelects && (lPointers[0][0] != L'\0'))
+ {
+ i = 0;
+ do
+ {
+ lLengths[i] = wcslen(lPointers[i]);
+ lPointers[i + 1] = lPointers[i] + lLengths[i] + 1;
+ i++;
+ } while (lPointers[i][0] != L'\0' && i < MAX_MULTIPLE_FILES );
+
+ if (i > MAX_MULTIPLE_FILES)
+ {
+ free(lBuff);
+ lBuff = NULL;
+ }
+ else
+ {
+ i--;
+ p = lBuff + lFullBuffLen - 1;
+ *p = L'\0';
+ for (j = i; j >= 0; j--)
+ {
+ p -= lLengths[j];
+ memmove(p, lPointers[j], lLengths[j] * sizeof(wchar_t));
+ p--;
+ *p = L'\\';
+ p -= lBuffLen;
+ memmove(p, lBuff, lBuffLen*sizeof(wchar_t));
+ p--;
+ *p = L'|';
+ }
+ p++;
+ wcscpy(lBuff, p);
+ lBuffLen = wcslen(lBuff);
+ }
+ }
+ if (lBuff) lBuff = (wchar_t*)(realloc(lBuff, (lBuffLen + 1) * sizeof(wchar_t)));
+ }
+
+ if (lHResult == S_OK || lHResult == S_FALSE)
+ {
+ CoUninitialize();
+ }
+
+ return lBuff;
+}
+
+
+BOOL CALLBACK BrowseCallbackProcW_enum(HWND hWndChild, LPARAM lParam)
+{
+ wchar_t buf[255];
+ (void)lParam;
+ GetClassNameW(hWndChild, buf, sizeof(buf));
+ if (wcscmp(buf, L"SysTreeView32") == 0)
+ {
+ HTREEITEM hNode = TreeView_GetSelection(hWndChild);
+ TreeView_EnsureVisible(hWndChild, hNode);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+
+static int __stdcall BrowseCallbackProcW(HWND hwnd, UINT uMsg, LPARAM lp, LPARAM pData)
+{
+ (void)lp;
+ switch (uMsg)
+ {
+ case BFFM_INITIALIZED:
+ SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)pData);
+ break;
+ case BFFM_SELCHANGED:
+ EnumChildWindows(hwnd, BrowseCallbackProcW_enum, 0);
+ }
+ return 0;
+}
+
+wchar_t * tinyfd_selectFolderDialogW(
+ wchar_t const * aTitle, /* NULL or "" */
+ wchar_t const * aDefaultPath) /* NULL or "" */
+{
+ static wchar_t lBuff[MAX_PATH_OR_CMD];
+ wchar_t * lRetval;
+
+ LPITEMIDLIST lpItem;
+ HRESULT lHResult;
+ BROWSEINFOW bInfo = { 0 } ;
+
+ if (aTitle&&!wcscmp(aTitle, L"tinyfd_query")){ strcpy(tinyfd_response, "windows_wchar"); return (wchar_t *)1; }
+
+ /*if (quoteDetectedW(aTitle)) return tinyfd_selectFolderDialogW(L"INVALID TITLE WITH QUOTES", aDefaultPath);
+ if (quoteDetectedW(aDefaultPath)) return tinyfd_selectFolderDialogW(aTitle, L"INVALID DEFAULT_PATH WITH QUOTES");*/
+
+ lHResult = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
+
+ bInfo.hwndOwner = GetForegroundWindow();
+ bInfo.pidlRoot = NULL;
+ bInfo.pszDisplayName = lBuff;
+ bInfo.lpszTitle = aTitle && wcslen(aTitle) ? aTitle : NULL;
+ if (lHResult == S_OK || lHResult == S_FALSE)
+ {
+#ifndef __DMC__
+ bInfo.ulFlags = BIF_USENEWUI;
+#endif
+ }
+ bInfo.lpfn = BrowseCallbackProcW;
+ bInfo.lParam = (LPARAM)aDefaultPath;
+ bInfo.iImage = -1;
+
+ lpItem = SHBrowseForFolderW(&bInfo);
+ if (!lpItem)
+ {
+ lRetval = NULL;
+ }
+ else
+ {
+ SHGetPathFromIDListW(lpItem, lBuff);
+ lRetval = lBuff ;
+ }
+
+ if (lHResult == S_OK || lHResult == S_FALSE)
+ {
+ CoUninitialize();
+ }
+ return lRetval;
+}
+
+
+wchar_t * tinyfd_colorChooserW(
+ wchar_t const * aTitle, /* NULL or "" */
+ wchar_t const * aDefaultHexRGB, /* NULL or "#FF0000"*/
+ unsigned char const aDefaultRGB[3], /* { 0 , 255 , 255 } */
+ unsigned char aoResultRGB[3]) /* { 0 , 0 , 0 } */
+{
+ static wchar_t lResultHexRGB[8];
+ CHOOSECOLORW cc;
+ COLORREF crCustColors[16];
+ unsigned char lDefaultRGB[3];
+ int lRet;
+
+ HRESULT lHResult;
+
+ if (aTitle&&!wcscmp(aTitle, L"tinyfd_query")){ strcpy(tinyfd_response, "windows_wchar"); return (wchar_t *)1; }
+
+ /*if (quoteDetectedW(aTitle)) return tinyfd_colorChooserW(L"INVALID TITLE WITH QUOTES", aDefaultHexRGB, aDefaultRGB, aoResultRGB);
+ if (quoteDetectedW(aDefaultHexRGB)) return tinyfd_colorChooserW(aTitle, L"INVALID DEFAULT_HEX_RGB WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultRGB, aoResultRGB);*/
+
+ lHResult = CoInitializeEx(NULL, 0);
+
+ if ( aDefaultHexRGB && wcslen(aDefaultHexRGB) )
+ {
+ Hex2RGBW(aDefaultHexRGB, lDefaultRGB);
+ }
+ else
+ {
+ lDefaultRGB[0] = aDefaultRGB[0];
+ lDefaultRGB[1] = aDefaultRGB[1];
+ lDefaultRGB[2] = aDefaultRGB[2];
+ }
+
+ /* we can't use aTitle */
+ cc.lStructSize = sizeof(CHOOSECOLOR);
+ cc.hwndOwner = GetForegroundWindow();
+ cc.hInstance = NULL;
+ cc.rgbResult = RGB(lDefaultRGB[0], lDefaultRGB[1], lDefaultRGB[2]);
+ cc.lpCustColors = crCustColors;
+ cc.Flags = CC_RGBINIT | CC_FULLOPEN | CC_ANYCOLOR ;
+ cc.lCustData = 0;
+ cc.lpfnHook = NULL;
+ cc.lpTemplateName = NULL;
+
+ lRet = ChooseColorW(&cc);
+
+ if (!lRet)
+ {
+ return NULL;
+ }
+
+ aoResultRGB[0] = GetRValue(cc.rgbResult);
+ aoResultRGB[1] = GetGValue(cc.rgbResult);
+ aoResultRGB[2] = GetBValue(cc.rgbResult);
+
+ RGB2HexW(aoResultRGB, lResultHexRGB);
+
+ if (lHResult == S_OK || lHResult == S_FALSE)
+ {
+ CoUninitialize();
+ }
+
+ return lResultHexRGB;
+}
+
+
+static int messageBoxWinGui(
+ char const * aTitle, /* NULL or "" */
+ char const * aMessage, /* NULL or "" may contain \n and \t */
+ char const * aDialogType, /* "ok" "okcancel" "yesno" "yesnocancel" */
+ char const * aIconType, /* "info" "warning" "error" "question" */
+ int aDefaultButton) /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
+{
+ int lIntRetVal;
+ wchar_t lTitle[128] = L"";
+ wchar_t * lMessage = NULL;
+ wchar_t lDialogType[16] = L"";
+ wchar_t lIconType[16] = L"";
+ wchar_t * lTmpWChar;
+
+ if (aTitle)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aTitle);
+ else lTmpWChar = tinyfd_mbcsTo16(aTitle);
+ wcscpy(lTitle, lTmpWChar);
+ }
+ if (aMessage)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aMessage);
+ else lTmpWChar = tinyfd_mbcsTo16(aMessage);
+ lMessage = (wchar_t *) malloc((wcslen(lTmpWChar) + 1)* sizeof(wchar_t));
+ if (lMessage) wcscpy(lMessage, lTmpWChar);
+ }
+ if (aDialogType)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aDialogType);
+ else lTmpWChar = tinyfd_mbcsTo16(aDialogType);
+ wcscpy(lDialogType, lTmpWChar);
+ }
+ if (aIconType)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aIconType);
+ else lTmpWChar = tinyfd_mbcsTo16(aIconType);
+ wcscpy(lIconType, lTmpWChar);
+ }
+
+ lIntRetVal = tinyfd_messageBoxW(lTitle, lMessage, lDialogType, lIconType, aDefaultButton);
+
+ free(lMessage);
+
+ return lIntRetVal;
+}
+
+
+static int notifyWinGui(
+ char const * aTitle, /* NULL or "" */
+ char const * aMessage, /* NULL or "" may NOT contain \n nor \t */
+ char const * aIconType)
+{
+ wchar_t lTitle[128] = L"";
+ wchar_t * lMessage = NULL;
+ wchar_t lIconType[16] = L"";
+ wchar_t * lTmpWChar;
+
+ if (aTitle)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aTitle);
+ else lTmpWChar = tinyfd_mbcsTo16(aTitle);
+ wcscpy(lTitle, lTmpWChar);
+ }
+ if (aMessage)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aMessage);
+ else lTmpWChar = tinyfd_mbcsTo16(aMessage);
+ lMessage = (wchar_t *) malloc((wcslen(lTmpWChar) + 1)* sizeof(wchar_t));
+ if (lMessage) wcscpy(lMessage, lTmpWChar);
+ }
+ if (aIconType)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aIconType);
+ else lTmpWChar = tinyfd_mbcsTo16(aIconType);
+ wcscpy(lIconType, lTmpWChar);
+ }
+
+ tinyfd_notifyPopupW(lTitle, lMessage, lIconType);
+
+ free(lMessage);
+
+ return 1;
+}
+
+
+static int inputBoxWinGui(
+ char * aoBuff,
+ char const * aTitle, /* NULL or "" */
+ char const * aMessage, /* NULL or "" may NOT contain \n nor \t */
+ char const * aDefaultInput) /* "" , if NULL it's a passwordBox */
+{
+ wchar_t lTitle[128] = L"";
+ wchar_t * lMessage = NULL;
+ wchar_t lDefaultInput[MAX_PATH_OR_CMD] = L"";
+ wchar_t * lTmpWChar;
+ char * lTmpChar;
+
+ if (aTitle)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aTitle);
+ else lTmpWChar = tinyfd_mbcsTo16(aTitle);
+ wcscpy(lTitle, lTmpWChar);
+ }
+ if (aMessage)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aMessage);
+ else lTmpWChar = tinyfd_mbcsTo16(aMessage);
+ lMessage = (wchar_t *) malloc((wcslen(lTmpWChar) + 1)* sizeof(wchar_t));
+ if (lMessage) wcscpy(lMessage, lTmpWChar);
+ }
+ if (aDefaultInput)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aDefaultInput);
+ else lTmpWChar = tinyfd_mbcsTo16(aDefaultInput);
+ wcscpy(lDefaultInput, lTmpWChar);
+ lTmpWChar = tinyfd_inputBoxW(lTitle, lMessage, lDefaultInput);
+ }
+ else lTmpWChar = tinyfd_inputBoxW(lTitle, lMessage, NULL);
+
+ free(lMessage);
+
+ if (!lTmpWChar)
+ {
+ aoBuff[0] = '\0';
+ return 0;
+ }
+
+ if (tinyfd_winUtf8) lTmpChar = tinyfd_utf16to8(lTmpWChar);
+ else lTmpChar = tinyfd_utf16toMbcs(lTmpWChar);
+
+ strcpy(aoBuff, lTmpChar);
+
+ return 1;
+}
+
+
+static char * saveFileDialogWinGui(
+ char * aoBuff,
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultPathAndOrFile, /* NULL or "" */
+ int aNumOfFilterPatterns, /* 0 */
+ char const * const * aFilterPatterns, /* NULL or {"*.jpg","*.png"} */
+ char const * aSingleFilterDescription) /* NULL or "image files" */
+{
+ wchar_t lTitle[128] = L"";
+ wchar_t lDefaultPathAndFile[MAX_PATH_OR_CMD] = L"";
+ wchar_t lSingleFilterDescription[128] = L"";
+ wchar_t * * lFilterPatterns;
+ wchar_t * lTmpWChar;
+ char * lTmpChar;
+ int i;
+
+ lFilterPatterns = (wchar_t **) malloc(aNumOfFilterPatterns*sizeof(wchar_t *));
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aFilterPatterns[i]);
+ else lTmpWChar = tinyfd_mbcsTo16(aFilterPatterns[i]);
+ lFilterPatterns[i] = (wchar_t *) malloc((wcslen(lTmpWChar) + 1) * sizeof(wchar_t *));
+ if (lFilterPatterns[i]) wcscpy(lFilterPatterns[i], lTmpWChar);
+ }
+
+ if (aTitle)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aTitle);
+ else lTmpWChar = tinyfd_mbcsTo16(aTitle);
+ wcscpy(lTitle, lTmpWChar);
+ }
+ if (aDefaultPathAndOrFile)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aDefaultPathAndOrFile);
+ else lTmpWChar = tinyfd_mbcsTo16(aDefaultPathAndOrFile);
+ wcscpy(lDefaultPathAndFile, lTmpWChar);
+ }
+ if (aSingleFilterDescription)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aSingleFilterDescription);
+ else lTmpWChar = tinyfd_mbcsTo16(aSingleFilterDescription);
+ wcscpy(lSingleFilterDescription, lTmpWChar);
+ }
+
+ lTmpWChar = tinyfd_saveFileDialogW(
+ lTitle,
+ lDefaultPathAndFile,
+ aNumOfFilterPatterns,
+ (wchar_t const**) lFilterPatterns, /*stupid cast for gcc*/
+ lSingleFilterDescription);
+
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ free(lFilterPatterns[i]);
+ }
+ free(lFilterPatterns);
+
+ if (!lTmpWChar)
+ {
+ return NULL;
+ }
+
+ if (tinyfd_winUtf8) lTmpChar = tinyfd_utf16to8(lTmpWChar);
+ else lTmpChar = tinyfd_utf16toMbcs(lTmpWChar);
+ strcpy(aoBuff, lTmpChar);
+ if (tinyfd_winUtf8) (void)tinyfd_utf16to8(NULL);
+ else (void)tinyfd_utf16toMbcs(NULL);
+
+ return aoBuff;
+}
+
+
+static char * openFileDialogWinGui(
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultPathAndOrFile, /* NULL or "" */
+ int aNumOfFilterPatterns, /* 0 */
+ char const * const * aFilterPatterns, /* NULL or {"*.jpg","*.png"} */
+ char const * aSingleFilterDescription, /* NULL or "image files" */
+ int aAllowMultipleSelects) /* 0 or 1 */
+{
+ wchar_t lTitle[128] = L"";
+ wchar_t lDefaultPathAndFile[MAX_PATH_OR_CMD] = L"";
+ wchar_t lSingleFilterDescription[128] = L"";
+ wchar_t * * lFilterPatterns;
+ wchar_t * lTmpWChar;
+ char * lTmpChar;
+ int i;
+
+ lFilterPatterns = (wchar_t * *) malloc(aNumOfFilterPatterns*sizeof(wchar_t *));
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aFilterPatterns[i]);
+ else lTmpWChar = tinyfd_mbcsTo16(aFilterPatterns[i]);
+ lFilterPatterns[i] = (wchar_t *) malloc((wcslen(lTmpWChar) + 1)*sizeof(wchar_t *));
+ if (lFilterPatterns[i]) wcscpy(lFilterPatterns[i], lTmpWChar);
+ }
+
+ if (aTitle)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aTitle);
+ else lTmpWChar = tinyfd_mbcsTo16(aTitle);
+ wcscpy(lTitle, lTmpWChar);
+ }
+ if (aDefaultPathAndOrFile)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aDefaultPathAndOrFile);
+ else lTmpWChar = tinyfd_mbcsTo16(aDefaultPathAndOrFile);
+ wcscpy(lDefaultPathAndFile, lTmpWChar);
+ }
+ if (aSingleFilterDescription)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aSingleFilterDescription);
+ else lTmpWChar = tinyfd_mbcsTo16(aSingleFilterDescription);
+ wcscpy(lSingleFilterDescription, lTmpWChar);
+ }
+
+ lTmpWChar = tinyfd_openFileDialogW(
+ lTitle,
+ lDefaultPathAndFile,
+ aNumOfFilterPatterns,
+ (wchar_t const**) lFilterPatterns, /*stupid cast for gcc*/
+ lSingleFilterDescription,
+ aAllowMultipleSelects);
+
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ free(lFilterPatterns[i]);
+ }
+ free(lFilterPatterns);
+
+ if (!lTmpWChar) return NULL;
+
+ if (tinyfd_winUtf8) lTmpChar = tinyfd_utf16to8(lTmpWChar);
+ else lTmpChar = tinyfd_utf16toMbcs(lTmpWChar);
+ (void)tinyfd_openFileDialogW(NULL, NULL, 0, NULL, NULL, -1);
+
+ return lTmpChar;
+}
+
+
+static char * selectFolderDialogWinGui(
+ char * aoBuff,
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultPath) /* NULL or "" */
+{
+ wchar_t lTitle[128] = L"";
+ wchar_t lDefaultPath[MAX_PATH_OR_CMD] = L"";
+ wchar_t * lTmpWChar;
+ char * lTmpChar;
+
+ if (aTitle)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aTitle);
+ else lTmpWChar = tinyfd_mbcsTo16(aTitle);
+ wcscpy(lTitle, lTmpWChar);
+ }
+ if (aDefaultPath)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aDefaultPath);
+ else lTmpWChar = tinyfd_mbcsTo16(aDefaultPath);
+ wcscpy(lDefaultPath, lTmpWChar);
+ }
+
+ lTmpWChar = tinyfd_selectFolderDialogW(
+ lTitle,
+ lDefaultPath);
+
+ if (!lTmpWChar)
+ {
+ return NULL;
+ }
+
+ if (tinyfd_winUtf8) lTmpChar = tinyfd_utf16to8(lTmpWChar);
+ else lTmpChar = tinyfd_utf16toMbcs(lTmpWChar);
+ strcpy(aoBuff, lTmpChar);
+
+ return aoBuff;
+}
+
+
+static char * colorChooserWinGui(
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultHexRGB, /* NULL or "#FF0000"*/
+ unsigned char const aDefaultRGB[3], /* { 0 , 255 , 255 } */
+ unsigned char aoResultRGB[3]) /* { 0 , 0 , 0 } */
+{
+ static char lResultHexRGB[8];
+
+ wchar_t lTitle[128];
+ wchar_t * lTmpWChar;
+ char * lTmpChar;
+ wchar_t lDefaultHexRGB[16] = L"";
+
+ if (aTitle)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aTitle);
+ else lTmpWChar = tinyfd_mbcsTo16(aTitle);
+ wcscpy(lTitle, lTmpWChar);
+ }
+ if (aDefaultHexRGB)
+ {
+ if (tinyfd_winUtf8) lTmpWChar = tinyfd_utf8to16(aDefaultHexRGB);
+ else lTmpWChar = tinyfd_mbcsTo16(aDefaultHexRGB);
+ wcscpy(lDefaultHexRGB, lTmpWChar);
+ }
+
+ lTmpWChar = tinyfd_colorChooserW(
+ lTitle,
+ lDefaultHexRGB,
+ aDefaultRGB,
+ aoResultRGB );
+
+ if (!lTmpWChar)
+ {
+ return NULL;
+ }
+
+ if (tinyfd_winUtf8) lTmpChar = tinyfd_utf16to8(lTmpWChar);
+ else lTmpChar = tinyfd_utf16toMbcs(lTmpWChar);
+ strcpy(lResultHexRGB, lTmpChar);
+
+ return lResultHexRGB;
+}
+
+
+static int dialogPresent(void)
+{
+ static int lDialogPresent = -1 ;
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ char const * lString = "dialog.exe";
+ if (!tinyfd_allowCursesDialogs) return 0;
+ if (lDialogPresent < 0)
+ {
+ lIn = ( FILE * ) _popen("where dialog.exe", "r");
+ if ( ! lIn )
+ {
+ lDialogPresent = 0 ;
+ return 0 ;
+ }
+ while ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {}
+ _pclose( lIn ) ;
+ if ( lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ if ( strcmp(lBuff+strlen(lBuff)-strlen(lString),lString) )
+ {
+ lDialogPresent = 0 ;
+ }
+ else
+ {
+ lDialogPresent = 1 ;
+ }
+ }
+ return lDialogPresent;
+}
+
+
+static int messageBoxWinConsole(
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" may contain \n and \t */
+ char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */
+ char const * aIconType , /* "info" "warning" "error" "question" */
+ int aDefaultButton ) /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
+{
+ char lDialogString[MAX_PATH_OR_CMD];
+ char lDialogFile[MAX_PATH_OR_CMD];
+ FILE * lIn;
+ char lBuff[MAX_PATH_OR_CMD] = "";
+ (void)aIconType;
+
+ strcpy(lDialogString, "dialog ");
+ if (aTitle && strlen(aTitle))
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( aDialogType && ( !strcmp( "okcancel" , aDialogType )
+ || !strcmp("yesno", aDialogType) || !strcmp("yesnocancel", aDialogType) ) )
+ {
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString, "tab: move focus") ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+ {
+ if ( ! aDefaultButton )
+ {
+ strcat( lDialogString , "--defaultno " ) ;
+ }
+ strcat( lDialogString ,
+ "--yes-label \"Ok\" --no-label \"Cancel\" --yesno " ) ;
+ }
+ else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+ {
+ if ( ! aDefaultButton )
+ {
+ strcat( lDialogString , "--defaultno " ) ;
+ }
+ strcat( lDialogString , "--yesno " ) ;
+ }
+ else if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ if (!aDefaultButton)
+ {
+ strcat(lDialogString, "--defaultno ");
+ }
+ strcat(lDialogString, "--menu ");
+ }
+ else
+ {
+ strcat( lDialogString , "--msgbox " ) ;
+ }
+
+ strcat( lDialogString , "\"" ) ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ tfd_replaceSubStr( aMessage , "\n" , "\\n" , lBuff ) ;
+ strcat(lDialogString, lBuff) ;
+ lBuff[0]='\0';
+ }
+ strcat(lDialogString, "\" ");
+
+ if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ strcat(lDialogString, "0 60 0 Yes \"\" No \"\"");
+ strcat(lDialogString, "2>>");
+ }
+ else
+ {
+ strcat(lDialogString, "10 60");
+ strcat(lDialogString, " && echo 1 > ");
+ }
+
+ strcpy(lDialogFile, getenv("TEMP"));
+ strcat(lDialogFile, "\\tinyfd.txt");
+ strcat(lDialogString, lDialogFile);
+
+ /*if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;*/
+ system( lDialogString ) ;
+
+ lIn = ( FILE * ) fopen(lDialogFile, "r");
+ if ( ! lIn )
+ {
+ remove(lDialogFile);
+ return 0 ;
+ }
+ while (fgets(lBuff, sizeof(lBuff), lIn) != NULL)
+ {}
+ fclose(lIn);
+ remove(lDialogFile);
+ if ( lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+
+ /* if (tinyfd_verbose) printf("lBuff: %s\n", lBuff); */
+ if ( ! strlen(lBuff) )
+ {
+ return 0;
+ }
+
+ if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ if (lBuff[0] == 'Y') return 1;
+ else return 2;
+ }
+
+ return 1;
+}
+
+
+static int inputBoxWinConsole(
+ char * aoBuff ,
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" may NOT contain \n nor \t */
+ char const * aDefaultInput ) /* "" , if NULL it's a passwordBox */
+{
+ char lDialogString[MAX_PATH_OR_CMD];
+ char lDialogFile[MAX_PATH_OR_CMD];
+ FILE * lIn;
+ int lResult;
+
+ strcpy(lDialogFile, getenv("TEMP"));
+ strcat(lDialogFile, "\\tinyfd.txt");
+ strcpy(lDialogString , "echo|set /p=1 >" ) ;
+ strcat(lDialogString, lDialogFile);
+ strcat( lDialogString , " & " ) ;
+
+ strcat( lDialogString , "dialog " ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString, "tab: move focus") ;
+ if ( ! aDefaultInput )
+ {
+ strcat(lDialogString, " (sometimes nothing, no blink nor star, is shown in text field)") ;
+ }
+
+ strcat(lDialogString, "\" ") ;
+
+ if ( ! aDefaultInput )
+ {
+ strcat( lDialogString , "--insecure --passwordbox" ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "--inputbox" ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage) ;
+ }
+ strcat(lDialogString,"\" 10 60 ") ;
+ if ( aDefaultInput && strlen(aDefaultInput) )
+ {
+ strcat(lDialogString, "\"") ;
+ strcat(lDialogString, aDefaultInput) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat(lDialogString, "2>>");
+ strcpy(lDialogFile, getenv("TEMP"));
+ strcat(lDialogFile, "\\tinyfd.txt");
+ strcat(lDialogString, lDialogFile);
+ strcat(lDialogString, " || echo 0 > ");
+ strcat(lDialogString, lDialogFile);
+
+ /* printf( "lDialogString: %s\n" , lDialogString ) ; */
+ system( lDialogString ) ;
+
+ lIn = ( FILE * ) fopen(lDialogFile, "r");
+ if ( ! lIn )
+ {
+ remove(lDialogFile);
+ aoBuff[0] = '\0';
+ return 0;
+ }
+ while (fgets(aoBuff, MAX_PATH_OR_CMD, lIn) != NULL)
+ {}
+ fclose(lIn);
+
+ wipefile(lDialogFile);
+ remove(lDialogFile);
+ if ( aoBuff[strlen( aoBuff ) -1] == '\n' )
+ {
+ aoBuff[strlen( aoBuff ) -1] = '\0' ;
+ }
+ /* printf( "aoBuff: %s\n" , aoBuff ) ; */
+
+ /* printf( "aoBuff: %s len: %lu \n" , aoBuff , strlen(aoBuff) ) ; */
+ lResult = strncmp( aoBuff , "1" , 1) ? 0 : 1 ;
+ /* printf( "lResult: %d \n" , lResult ) ; */
+ if ( ! lResult )
+ {
+ aoBuff[0] = '\0';
+ return 0 ;
+ }
+ /* printf( "aoBuff+1: %s\n" , aoBuff+1 ) ; */
+ strcpy(aoBuff, aoBuff+3);
+ return 1;
+}
+
+
+static char * saveFileDialogWinConsole(
+ char * aoBuff ,
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPathAndOrFile ) /* NULL or "" */
+{
+ char lDialogString[MAX_PATH_OR_CMD];
+ char lPathAndFile[MAX_PATH_OR_CMD] = "";
+ FILE * lIn;
+
+ strcpy( lDialogString , "dialog " ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString,
+ "tab: focus | /: populate | spacebar: fill text field | ok: TEXT FIELD ONLY") ;
+ strcat(lDialogString, "\" ") ;
+
+ strcat( lDialogString , "--fselect \"" ) ;
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ /* dialog.exe uses unix separators even on windows */
+ strcpy(lPathAndFile, aDefaultPathAndOrFile);
+ replaceChr( lPathAndFile , '\\' , '/' ) ;
+ }
+
+ /* dialog.exe needs at least one separator */
+ if ( ! strchr(lPathAndFile, '/') )
+ {
+ strcat(lDialogString, "./") ;
+ }
+ strcat(lDialogString, lPathAndFile) ;
+ strcat(lDialogString, "\" 0 60 2>");
+ strcpy(lPathAndFile, getenv("TEMP"));
+ strcat(lPathAndFile, "\\tinyfd.txt");
+ strcat(lDialogString, lPathAndFile);
+
+ /* printf( "lDialogString: %s\n" , lDialogString ) ; */
+ system( lDialogString ) ;
+
+ lIn = ( FILE * ) fopen(lPathAndFile, "r");
+ if ( ! lIn )
+ {
+ remove(lPathAndFile);
+ return NULL;
+ }
+ while (fgets(aoBuff, MAX_PATH_OR_CMD, lIn) != NULL)
+ {}
+ fclose(lIn);
+ remove(lPathAndFile);
+ replaceChr( aoBuff , '/' , '\\' ) ;
+ /* printf( "aoBuff: %s\n" , aoBuff ) ; */
+ getLastName(lDialogString,aoBuff);
+ if ( ! strlen(lDialogString) )
+ {
+ return NULL;
+ }
+ return aoBuff;
+}
+
+
+static char * openFileDialogWinConsole(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPathAndOrFile ) /* NULL or "" */
+{
+ char lFilterPatterns[MAX_PATH_OR_CMD] = "";
+ char lDialogString[MAX_PATH_OR_CMD] ;
+ FILE * lIn;
+
+ static char aoBuff[MAX_PATH_OR_CMD];
+
+ strcpy( lDialogString , "dialog " ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString,
+ "tab: focus | /: populate | spacebar: fill text field | ok: TEXT FIELD ONLY") ;
+ strcat(lDialogString, "\" ") ;
+
+ strcat( lDialogString , "--fselect \"" ) ;
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ /* dialog.exe uses unix separators even on windows */
+ strcpy(lFilterPatterns, aDefaultPathAndOrFile);
+ replaceChr( lFilterPatterns , '\\' , '/' ) ;
+ }
+
+ /* dialog.exe needs at least one separator */
+ if ( ! strchr(lFilterPatterns, '/') )
+ {
+ strcat(lDialogString, "./") ;
+ }
+ strcat(lDialogString, lFilterPatterns) ;
+ strcat(lDialogString, "\" 0 60 2>");
+ strcpy(lFilterPatterns, getenv("TEMP"));
+ strcat(lFilterPatterns, "\\tinyfd.txt");
+ strcat(lDialogString, lFilterPatterns);
+
+ /* printf( "lDialogString: %s\n" , lDialogString ) ; */
+ system( lDialogString ) ;
+
+ lIn = ( FILE * ) fopen(lFilterPatterns, "r");
+ if ( ! lIn )
+ {
+ remove(lFilterPatterns);
+ return NULL;
+ }
+ while (fgets(aoBuff, MAX_PATH_OR_CMD, lIn) != NULL)
+ {}
+ fclose(lIn);
+ remove(lFilterPatterns);
+ replaceChr( aoBuff , '/' , '\\' ) ;
+ /* printf( "aoBuff: %s\n" , aoBuff ) ; */
+ return aoBuff;
+}
+
+
+static char * selectFolderDialogWinConsole(
+ char * aoBuff ,
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPath ) /* NULL or "" */
+{
+ char lDialogString[MAX_PATH_OR_CMD] ;
+ char lString[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+
+ strcpy( lDialogString , "dialog " ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString,
+ "tab: focus | /: populate | spacebar: fill text field | ok: TEXT FIELD ONLY") ;
+ strcat(lDialogString, "\" ") ;
+
+ strcat( lDialogString , "--dselect \"" ) ;
+ if ( aDefaultPath && strlen(aDefaultPath) )
+ {
+ /* dialog.exe uses unix separators even on windows */
+ strcpy(lString, aDefaultPath) ;
+ ensureFinalSlash(lString);
+ replaceChr( lString , '\\' , '/' ) ;
+ strcat(lDialogString, lString) ;
+ }
+ else
+ {
+ /* dialog.exe needs at least one separator */
+ strcat(lDialogString, "./") ;
+ }
+ strcat(lDialogString, "\" 0 60 2>");
+ strcpy(lString, getenv("TEMP"));
+ strcat(lString, "\\tinyfd.txt");
+ strcat(lDialogString, lString);
+
+ /* printf( "lDialogString: %s\n" , lDialogString ) ; */
+ system( lDialogString ) ;
+
+ lIn = ( FILE * ) fopen(lString, "r") ;
+ if ( ! lIn )
+ {
+ remove(lString);
+ return NULL;
+ }
+ while (fgets(aoBuff, MAX_PATH_OR_CMD, lIn) != NULL)
+ {}
+ fclose(lIn);
+ remove(lString);
+ replaceChr( aoBuff , '/' , '\\' ) ;
+ /* printf( "aoBuff: %s\n" , aoBuff ) ; */
+ return aoBuff;
+}
+
+static void writeUtf8( char const * aUtf8String )
+{
+ unsigned long lNum;
+ void * lConsoleHandle;
+ wchar_t * lTmpWChar;
+
+ lConsoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
+ lTmpWChar = tinyfd_utf8to16(aUtf8String);
+ (void)WriteConsoleW(lConsoleHandle, lTmpWChar, (DWORD) wcslen(lTmpWChar), &lNum, NULL);
+}
+
+
+int tinyfd_messageBox(
+ char const * aTitle, /* NULL or "" */
+ char const * aMessage, /* NULL or "" may contain \n and \t */
+ char const * aDialogType, /* "ok" "okcancel" "yesno" "yesnocancel" */
+ char const * aIconType, /* "info" "warning" "error" "question" */
+ int aDefaultButton) /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
+{
+ char lChar;
+ UINT lOriginalCP = 0;
+ UINT lOriginalOutputCP = 0;
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_messageBox("INVALID TITLE WITH QUOTES", aMessage, aDialogType, aIconType, aDefaultButton);
+ if (tfd_quoteDetected(aMessage)) return tinyfd_messageBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDialogType, aIconType, aDefaultButton);
+
+ if ((!tinyfd_forceConsole || !(GetConsoleWindow() || dialogPresent()))
+ && (!getenv("SSH_CLIENT") || getenvDISPLAY()))
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "windows"); return 1; }
+ return messageBoxWinGui(aTitle, aMessage, aDialogType, aIconType, aDefaultButton);
+ }
+ else if (dialogPresent())
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "dialog"); return 0; }
+ return messageBoxWinConsole(
+ aTitle, aMessage, aDialogType, aIconType, aDefaultButton);
+ }
+ else
+ {
+ if (!tinyfd_winUtf8)
+ {
+ lOriginalCP = GetConsoleCP();
+ lOriginalOutputCP = GetConsoleOutputCP();
+ (void)SetConsoleCP(GetACP());
+ (void)SetConsoleOutputCP(GetACP());
+ }
+
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "basicinput"); return 0; }
+ if (!gWarningDisplayed && !tinyfd_forceConsole)
+ {
+ gWarningDisplayed = 1;
+ printf("\n\n%s\n", gTitle);
+ printf("%s\n\n", tinyfd_needs);
+ }
+
+ if (aTitle && strlen(aTitle))
+ {
+ printf("\n");
+ if (tinyfd_winUtf8) writeUtf8(aTitle);
+ else printf("%s", aTitle);
+ printf("\n\n");
+ }
+ if (aDialogType && !strcmp("yesno", aDialogType))
+ {
+ do
+ {
+ if (aMessage && strlen(aMessage))
+ {
+ if (tinyfd_winUtf8) writeUtf8(aMessage);
+ else printf("%s", aMessage);
+ printf("\n");
+ }
+ printf("y/n: ");
+ lChar = (char)tolower(_getch());
+ printf("\n\n");
+ } while (lChar != 'y' && lChar != 'n');
+ if (!tinyfd_winUtf8) { (void)SetConsoleCP(lOriginalCP); (void)SetConsoleOutputCP(lOriginalOutputCP); }
+ return lChar == 'y' ? 1 : 0;
+ }
+ else if (aDialogType && !strcmp("okcancel", aDialogType))
+ {
+ do
+ {
+ if (aMessage && strlen(aMessage))
+ {
+ if (tinyfd_winUtf8) writeUtf8(aMessage);
+ else printf("%s", aMessage);
+ printf("\n");
+ }
+ printf("[O]kay/[C]ancel: ");
+ lChar = (char)tolower(_getch());
+ printf("\n\n");
+ } while (lChar != 'o' && lChar != 'c');
+ if (!tinyfd_winUtf8) { (void)SetConsoleCP(lOriginalCP); (void)SetConsoleOutputCP(lOriginalOutputCP); }
+ return lChar == 'o' ? 1 : 0;
+ }
+ else if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ do
+ {
+ if (aMessage && strlen(aMessage))
+ {
+ if (tinyfd_winUtf8) writeUtf8(aMessage);
+ else printf("%s", aMessage);
+ printf("\n");
+ }
+ printf("[Y]es/[N]o/[C]ancel: ");
+ lChar = (char)tolower(_getch());
+ printf("\n\n");
+ } while (lChar != 'y' && lChar != 'n' && lChar != 'c');
+ if (!tinyfd_winUtf8) { (void)SetConsoleCP(lOriginalCP); (void)SetConsoleOutputCP(lOriginalOutputCP); }
+ return (lChar == 'y') ? 1 : (lChar == 'n') ? 2 : 0;
+ }
+ else
+ {
+ if (aMessage && strlen(aMessage))
+ {
+ if (tinyfd_winUtf8) writeUtf8(aMessage);
+ else printf("%s", aMessage);
+ printf("\n\n");
+ }
+ printf("press enter to continue "); fflush(stdout);
+ lChar = (char)_getch();
+ printf("\n\n");
+ if (!tinyfd_winUtf8) { (void)SetConsoleCP(lOriginalCP); (void)SetConsoleOutputCP(lOriginalOutputCP); }
+ return 1;
+ }
+ }
+}
+
+
+/* return has only meaning for tinyfd_query */
+int tinyfd_notifyPopup(
+ char const * aTitle, /* NULL or "" */
+ char const * aMessage , /* NULL or "" may contain \n \t */
+ char const * aIconType ) /* "info" "warning" "error" */
+{
+ if (tfd_quoteDetected(aTitle)) return tinyfd_notifyPopup("INVALID TITLE WITH QUOTES", aMessage, aIconType);
+ if (tfd_quoteDetected(aMessage)) return tinyfd_notifyPopup(aTitle, "INVALID MESSAGE WITH QUOTES", aIconType);
+
+ if ( powershellPresent()
+ && ( !tinyfd_forceConsole || !(GetConsoleWindow() || dialogPresent()) )
+ && (!getenv("SSH_CLIENT") || getenvDISPLAY()) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return 1;}
+ return notifyWinGui(aTitle, aMessage, aIconType);
+ }
+ else return tinyfd_messageBox(aTitle, aMessage, "ok" , aIconType, 0);
+}
+
+
+/* returns NULL on cancel */
+char * tinyfd_inputBox(
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" (\n and \t have no effect) */
+ char const * aDefaultInput ) /* "" , if NULL it's a passwordBox */
+{
+ static char lBuff[MAX_PATH_OR_CMD] = "";
+ char * lEOF;
+
+ DWORD mode = 0;
+ HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
+
+ unsigned long lNum;
+ void * lConsoleHandle;
+ char * lTmpChar;
+ wchar_t lBuffW[1024];
+
+ UINT lOriginalCP = 0;
+ UINT lOriginalOutputCP = 0;
+
+ if (!aTitle && !aMessage && !aDefaultInput) return lBuff; /* now I can fill lBuff from outside */
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_inputBox("INVALID TITLE WITH QUOTES", aMessage, aDefaultInput);
+ if (tfd_quoteDetected(aMessage)) return tinyfd_inputBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDefaultInput);
+ if (tfd_quoteDetected(aDefaultInput)) return tinyfd_inputBox(aTitle, aMessage, "INVALID DEFAULT_INPUT WITH QUOTES: use the GRAVE ACCENT \\x60 instead.");
+
+ mode = 0;
+ hStdin = GetStdHandle(STD_INPUT_HANDLE);
+
+ if ((!tinyfd_forceConsole || !(
+ GetConsoleWindow() ||
+ dialogPresent()))
+ && (!getenv("SSH_CLIENT") || getenvDISPLAY()))
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return (char *)1;}
+ lBuff[0]='\0';
+ if (inputBoxWinGui(lBuff, aTitle, aMessage, aDefaultInput)) return lBuff;
+ else return NULL;
+ }
+ else if ( dialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ lBuff[0]='\0';
+ if (inputBoxWinConsole(lBuff, aTitle, aMessage, aDefaultInput) ) return lBuff;
+ else return NULL;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"basicinput");return (char *)0;}
+ lBuff[0]='\0';
+ if (!gWarningDisplayed && !tinyfd_forceConsole)
+ {
+ gWarningDisplayed = 1 ;
+ printf("\n\n%s\n", gTitle);
+ printf("%s\n\n", tinyfd_needs);
+ }
+
+ if (!tinyfd_winUtf8)
+ {
+ lOriginalCP = GetConsoleCP();
+ lOriginalOutputCP = GetConsoleOutputCP();
+ (void)SetConsoleCP(GetACP());
+ (void)SetConsoleOutputCP(GetACP());
+ }
+
+ if (aTitle && strlen(aTitle))
+ {
+ printf("\n");
+ if (tinyfd_winUtf8) writeUtf8(aTitle);
+ else printf("%s", aTitle);
+ printf("\n\n");
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ if (tinyfd_winUtf8) writeUtf8(aMessage);
+ else printf("%s", aMessage);
+ printf("\n");
+ }
+ printf("(ctrl-Z + enter to cancel): "); fflush(stdout);
+ if ( ! aDefaultInput )
+ {
+ (void) GetConsoleMode(hStdin, &mode);
+ (void) SetConsoleMode(hStdin, mode & (~ENABLE_ECHO_INPUT));
+ }
+ if (tinyfd_winUtf8)
+ {
+ lConsoleHandle = GetStdHandle(STD_INPUT_HANDLE);
+ (void) ReadConsoleW(lConsoleHandle, lBuffW, MAX_PATH_OR_CMD, &lNum, NULL);
+ if (!aDefaultInput)
+ {
+ (void)SetConsoleMode(hStdin, mode);
+ printf("\n");
+ }
+ lBuffW[lNum] = '\0';
+ if (lBuffW[wcslen(lBuffW) - 1] == '\n') lBuffW[wcslen(lBuffW) - 1] = '\0';
+ if (lBuffW[wcslen(lBuffW) - 1] == '\r') lBuffW[wcslen(lBuffW) - 1] = '\0';
+ lTmpChar = tinyfd_utf16to8(lBuffW);
+ if (lTmpChar)
+ {
+ strcpy(lBuff, lTmpChar);
+ return lBuff;
+ }
+ else
+ return NULL;
+ }
+ else
+ {
+ lEOF = fgets(lBuff, MAX_PATH_OR_CMD, stdin);
+ if (!aDefaultInput)
+ {
+ (void)SetConsoleMode(hStdin, mode);
+ printf("\n");
+ }
+
+ if (!tinyfd_winUtf8)
+ {
+ (void)SetConsoleCP(lOriginalCP);
+ (void)SetConsoleOutputCP(lOriginalOutputCP);
+ }
+
+ if (!lEOF)
+ {
+ return NULL;
+ }
+ printf("\n");
+ if (strchr(lBuff, 27))
+ {
+ return NULL;
+ }
+ if (lBuff[strlen(lBuff) - 1] == '\n')
+ {
+ lBuff[strlen(lBuff) - 1] = '\0';
+ }
+ return lBuff;
+ }
+ }
+}
+
+
+char * tinyfd_saveFileDialog(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPathAndOrFile , /* NULL or "" */
+ int aNumOfFilterPatterns , /* 0 */
+ char const * const * aFilterPatterns , /* NULL or {"*.jpg","*.png"} */
+ char const * aSingleFilterDescription ) /* NULL or "image files" */
+{
+ static char lBuff[MAX_PATH_OR_CMD] ;
+ char lString[MAX_PATH_OR_CMD] ;
+ char * p ;
+ char * lPointerInputBox;
+ int i;
+
+ lBuff[0]='\0';
+
+ if ( ! aFilterPatterns ) aNumOfFilterPatterns = 0 ;
+ if (tfd_quoteDetected(aTitle)) return tinyfd_saveFileDialog("INVALID TITLE WITH QUOTES", aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription);
+ if (tfd_quoteDetected(aDefaultPathAndOrFile)) return tinyfd_saveFileDialog(aTitle, "INVALID DEFAULT_PATH WITH QUOTES", aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription);
+ if (tfd_quoteDetected(aSingleFilterDescription)) return tinyfd_saveFileDialog(aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, "INVALID FILTER_DESCRIPTION WITH QUOTES");
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (tfd_quoteDetected(aFilterPatterns[i])) return tinyfd_saveFileDialog("INVALID FILTER_PATTERN WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultPathAndOrFile, 0, NULL, NULL);
+ }
+
+
+ if ( ( !tinyfd_forceConsole || !( GetConsoleWindow() || dialogPresent() ) )
+ && (!getenv("SSH_CLIENT") || getenvDISPLAY()))
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return (char *)1;}
+ p = saveFileDialogWinGui(lBuff,
+ aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns, (char const * const *)aFilterPatterns, aSingleFilterDescription);
+ }
+ else if (dialogPresent())
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "dialog"); return (char *)0; }
+ p = saveFileDialogWinConsole(lBuff, aTitle, aDefaultPathAndOrFile);
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "basicinput"); return (char *)0; }
+ strcpy(lBuff, "Save file in ");
+ strcat(lBuff, getCurDir());
+
+ lPointerInputBox = tinyfd_inputBox(NULL,NULL,NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, lBuff, "");
+ if (p) strcpy(lBuff, p); else lBuff[0] = '\0';
+ if (lPointerInputBox) strcpy(lPointerInputBox, lString); /* restore its previous content to tinyfd_inputBox */
+ p = lBuff;
+ }
+
+ if ( ! p || ! strlen( p ) )
+ {
+ return NULL;
+ }
+ getPathWithoutFinalSlash( lString , p ) ;
+ if ( strlen( lString ) && ! dirExists( lString ) )
+ {
+ return NULL ;
+ }
+ getLastName(lString,p);
+ if ( ! filenameValid(lString) )
+ {
+ return NULL;
+ }
+ return p ;
+}
+
+
+/* in case of multiple files, the separator is | */
+char * tinyfd_openFileDialog(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPathAndOrFile, /* NULL or "" */
+ int aNumOfFilterPatterns , /* 0 */
+ char const * const * aFilterPatterns, /* NULL or {"*.jpg","*.png"} */
+ char const * aSingleFilterDescription, /* NULL or "image files" */
+ int aAllowMultipleSelects ) /* 0 or 1 */
+{
+ static char lBuff[MAX_PATH_OR_CMD];
+ char lString[MAX_PATH_OR_CMD];
+ char * p;
+ char * lPointerInputBox;
+ int i;
+
+ if ( ! aFilterPatterns ) aNumOfFilterPatterns = 0 ;
+ if (tfd_quoteDetected(aTitle)) return tinyfd_openFileDialog("INVALID TITLE WITH QUOTES", aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription, aAllowMultipleSelects);
+ if (tfd_quoteDetected(aDefaultPathAndOrFile)) return tinyfd_openFileDialog(aTitle, "INVALID DEFAULT_PATH WITH QUOTES", aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription, aAllowMultipleSelects);
+ if (tfd_quoteDetected(aSingleFilterDescription)) return tinyfd_openFileDialog(aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, "INVALID FILTER_DESCRIPTION WITH QUOTES", aAllowMultipleSelects);
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (tfd_quoteDetected(aFilterPatterns[i])) return tinyfd_openFileDialog("INVALID FILTER_PATTERN WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultPathAndOrFile, 0, NULL, NULL, aAllowMultipleSelects);
+ }
+
+ if ( ( !tinyfd_forceConsole || !( GetConsoleWindow() || dialogPresent() ) )
+ && (!getenv("SSH_CLIENT") || getenvDISPLAY()))
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return (char *)1;}
+ p = openFileDialogWinGui( aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns,
+ (char const * const *)aFilterPatterns, aSingleFilterDescription, aAllowMultipleSelects);
+ }
+ else if (dialogPresent())
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "dialog"); return (char *)0; }
+ p = openFileDialogWinConsole(aTitle, aDefaultPathAndOrFile);
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "basicinput"); return (char *)0; }
+ strcpy(lBuff, "Open file from ");
+ strcat(lBuff, getCurDir());
+ lPointerInputBox = tinyfd_inputBox(NULL, NULL, NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, lBuff, "");
+ if (p) strcpy(lBuff, p); else lBuff[0] = '\0';
+ if (lPointerInputBox) strcpy(lPointerInputBox, lString); /* restore its previous content to tinyfd_inputBox */
+ p = lBuff;
+ }
+
+ if ( ! p || ! strlen( p ) )
+ {
+ return NULL;
+ }
+ if ( aAllowMultipleSelects && strchr(p, '|') )
+ {
+ p = ensureFilesExist( (char *) p , p ) ;
+ }
+ else if ( ! fileExists(p) )
+ {
+ return NULL ;
+ }
+ /* printf( "lBuff3: %s\n" , p ) ; */
+ return p ;
+}
+
+
+char * tinyfd_selectFolderDialog(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPath ) /* NULL or "" */
+{
+ static char lBuff[MAX_PATH_OR_CMD];
+ char * p;
+ char * lPointerInputBox;
+ char lString[MAX_PATH_OR_CMD];
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_selectFolderDialog("INVALID TITLE WITH QUOTES", aDefaultPath);
+ if (tfd_quoteDetected(aDefaultPath)) return tinyfd_selectFolderDialog(aTitle, "INVALID DEFAULT_PATH WITH QUOTES");
+
+ if ( ( !tinyfd_forceConsole || !( GetConsoleWindow() || dialogPresent() ) )
+ && (!getenv("SSH_CLIENT") || getenvDISPLAY()))
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return (char *)1;}
+ p = selectFolderDialogWinGui(lBuff, aTitle, aDefaultPath);
+ }
+ else
+ if (dialogPresent())
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "dialog"); return (char *)0; }
+ p = selectFolderDialogWinConsole(lBuff, aTitle, aDefaultPath);
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "basicinput"); return (char *)0; }
+ strcpy(lBuff, "Select folder from ");
+ strcat(lBuff, getCurDir());
+ lPointerInputBox = tinyfd_inputBox(NULL, NULL, NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, lBuff, "");
+ if (p) strcpy(lBuff, p); else lBuff[0] = '\0';
+ if (lPointerInputBox) strcpy(lPointerInputBox, lString); /* restore its previous content to tinyfd_inputBox */
+ p = lBuff;
+ }
+
+ if ( ! p || ! strlen( p ) || ! dirExists( p ) )
+ {
+ return NULL ;
+ }
+ return p ;
+}
+
+
+/* aDefaultRGB is used only if aDefaultHexRGB is absent */
+/* aDefaultRGB and aoResultRGB can be the same array */
+/* returns NULL on cancel */
+/* returns the hexcolor as a string "#FF0000" */
+/* aoResultRGB also contains the result */
+char * tinyfd_colorChooser(
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultHexRGB, /* NULL or "" or "#FF0000"*/
+ unsigned char const aDefaultRGB[3], /* { 0 , 255 , 255 } */
+ unsigned char aoResultRGB[3]) /* { 0 , 0 , 0 } */
+{
+ static char lDefaultHexRGB[16];
+ int i;
+ char * p ;
+ char * lPointerInputBox;
+ char lString[MAX_PATH_OR_CMD];
+
+ lDefaultHexRGB[0] = '\0';
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_colorChooser("INVALID TITLE WITH QUOTES", aDefaultHexRGB, aDefaultRGB, aoResultRGB);
+ if (tfd_quoteDetected(aDefaultHexRGB)) return tinyfd_colorChooser(aTitle, "INVALID DEFAULT_HEX_RGB WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultRGB, aoResultRGB);
+
+ if ( (!tinyfd_forceConsole || !( GetConsoleWindow() || dialogPresent()) )
+ && (!getenv("SSH_CLIENT") || getenvDISPLAY()))
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return (char *)1;}
+ p = colorChooserWinGui(aTitle, aDefaultHexRGB, aDefaultRGB, aoResultRGB);
+ if (p)
+ {
+ strcpy(lDefaultHexRGB, p);
+ return lDefaultHexRGB;
+ }
+ return NULL;
+ }
+ else if (dialogPresent())
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "dialog"); return (char *)0; }
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle, "tinyfd_query")){ strcpy(tinyfd_response, "basicinput"); return (char *)0; }
+ }
+
+ if (aDefaultHexRGB && (strlen(aDefaultHexRGB)==7) )
+ {
+ strncpy(lDefaultHexRGB, aDefaultHexRGB,7);
+ lDefaultHexRGB[7]='\0';
+ }
+ else
+ {
+ RGB2Hex(aDefaultRGB, lDefaultHexRGB);
+ }
+
+ lPointerInputBox = tinyfd_inputBox(NULL, NULL, NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, "Enter hex rgb color (i.e. #f5ca20)", lDefaultHexRGB);
+
+ if ( !p || (strlen(p) != 7) || (p[0] != '#') )
+ {
+ return NULL ;
+ }
+ for ( i = 1 ; i < 7 ; i ++ )
+ {
+ if ( ! isxdigit( (int) p[i] ) )
+ {
+ return NULL ;
+ }
+ }
+ Hex2RGB(p,aoResultRGB);
+
+ strcpy(lDefaultHexRGB, p);
+
+ if (lPointerInputBox) strcpy(lPointerInputBox, lString); /* restore its previous content to tinyfd_inputBox */
+
+ return lDefaultHexRGB;
+}
+
+
+#else /* unix */
+
+static char gPython2Name[16];
+static char gPython3Name[16];
+static char gPythonName[16];
+
+int tfd_isDarwin(void)
+{
+ static int lIsDarwin = -1 ;
+ struct utsname lUtsname ;
+ if ( lIsDarwin < 0 ) lIsDarwin = !uname(&lUtsname) && !strcmp(lUtsname.sysname,"Darwin") ;
+ return lIsDarwin ;
+}
+
+
+int tfd_isHaiku(void)
+{
+ static int lIsHaiku = -1 ;
+ struct utsname lUtsname ;
+ if ( lIsHaiku < 0 ) lIsHaiku = !uname(&lUtsname) && !strcmp(lUtsname.sysname,"Haiku") ;
+ return lIsHaiku ;
+}
+
+
+static int dirExists( char const * aDirPath )
+{
+ DIR * lDir ;
+ if ( ! aDirPath || ! strlen( aDirPath ) )
+ return 0 ;
+
+ lDir = opendir( aDirPath ) ;
+ if ( ! lDir )
+ return 0 ;
+
+ closedir( lDir ) ;
+
+ return 1 ;
+}
+
+
+static int detectPresence( char const * aExecutable )
+{
+ char lBuff[MAX_PATH_OR_CMD] ;
+ char lTestedString[MAX_PATH_OR_CMD] = "command -v " ;
+ FILE * lIn ;
+#ifdef _GNU_SOURCE
+ char* lAllocatedCharString;
+ int lSubstringUndetected;
+#endif
+
+ strcat( lTestedString , aExecutable ) ;
+ strcat( lTestedString, " 2>/dev/null ");
+ lIn = ( FILE * ) popen( lTestedString , "r" ) ;
+ if ( ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ && ( ! strchr( lBuff , ':' ) ) && ( strncmp(lBuff, "no ", 3) ) )
+ { /* present */
+ pclose( lIn ) ;
+
+#ifdef _GNU_SOURCE /*to bypass this, just comment out "#define _GNU_SOURCE" at the top of the file*/
+ if ( lBuff[strlen( lBuff ) -1] == '\n' ) lBuff[strlen( lBuff ) -1] = '\0' ;
+ lAllocatedCharString = realpath(lBuff,NULL); /*same as canonicalize_file_name*/
+ lSubstringUndetected = ! strstr(lAllocatedCharString, aExecutable);
+ free(lAllocatedCharString);
+ if (lSubstringUndetected)
+ {
+ if (tinyfd_verbose) printf("detectPresence %s %d\n", aExecutable, 0);
+ return 0;
+ }
+#endif /*_GNU_SOURCE*/
+
+ if (tinyfd_verbose) printf("detectPresence %s %d\n", aExecutable, 1);
+ return 1 ;
+ }
+ else
+ {
+ pclose( lIn ) ;
+ if (tinyfd_verbose) printf("detectPresence %s %d\n", aExecutable, 0);
+ return 0 ;
+ }
+}
+
+
+static char * getVersion( char const * aExecutable ) /*version must be first numeral*/
+{
+ static char lBuff[MAX_PATH_OR_CMD] ;
+ char lTestedString[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ char * lTmp ;
+
+ strcpy( lTestedString , aExecutable ) ;
+ strcat( lTestedString , " --version" ) ;
+
+ lIn = ( FILE * ) popen( lTestedString , "r" ) ;
+ lTmp = fgets( lBuff , sizeof( lBuff ) , lIn ) ;
+ pclose( lIn ) ;
+
+ lTmp += strcspn(lTmp,"0123456789");
+ /* printf("lTmp:%s\n", lTmp); */
+ return lTmp ;
+}
+
+
+static int * getMajorMinorPatch( char const * aExecutable )
+{
+ static int lArray[3] ;
+ char * lTmp ;
+
+ lTmp = (char *) getVersion(aExecutable);
+ lArray[0] = atoi( strtok(lTmp," ,.-") ) ;
+ /* printf("lArray0 %d\n", lArray[0]); */
+ lArray[1] = atoi( strtok(0," ,.-") ) ;
+ /* printf("lArray1 %d\n", lArray[1]); */
+ lArray[2] = atoi( strtok(0," ,.-") ) ;
+ /* printf("lArray2 %d\n", lArray[2]); */
+
+ if ( !lArray[0] && !lArray[1] && !lArray[2] ) return NULL;
+ return lArray ;
+}
+
+
+static int tryCommand( char const * aCommand )
+{
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+
+ lIn = ( FILE * ) popen( aCommand , "r" ) ;
+ if ( fgets( lBuff , sizeof( lBuff ) , lIn ) == NULL )
+ { /* present */
+ pclose( lIn ) ;
+ return 1 ;
+ }
+ else
+ {
+ pclose( lIn ) ;
+ return 0 ;
+ }
+
+}
+
+
+static int isTerminalRunning(void)
+{
+ static int lIsTerminalRunning = -1 ;
+ if ( lIsTerminalRunning < 0 )
+ {
+ lIsTerminalRunning = isatty(1);
+ if (tinyfd_verbose) printf("isTerminalRunning %d\n", lIsTerminalRunning );
+ }
+ return lIsTerminalRunning;
+}
+
+
+static char * dialogNameOnly(void)
+{
+ static char lDialogName[128] = "*" ;
+ if ( lDialogName[0] == '*' )
+ {
+ if (!tinyfd_allowCursesDialogs)
+ {
+ strcpy(lDialogName , "" );
+ }
+ else if ( tfd_isDarwin() && * strcpy(lDialogName , "/opt/local/bin/dialog" )
+ && detectPresence( lDialogName ) )
+ {}
+ else if ( * strcpy(lDialogName , "dialog" )
+ && detectPresence( lDialogName ) )
+ {}
+ else
+ {
+ strcpy(lDialogName , "" );
+ }
+ }
+ return lDialogName ;
+}
+
+
+int isDialogVersionBetter09b(void)
+{
+ char const * lDialogName ;
+ char * lVersion ;
+ int lMajor ;
+ int lMinor ;
+ int lDate ;
+ int lResult ;
+ char * lMinorP ;
+ char * lLetter ;
+ char lBuff[128] ;
+
+ /*char lTest[128] = " 0.9b-20031126" ;*/
+
+ lDialogName = dialogNameOnly() ;
+ if ( ! strlen(lDialogName) || !(lVersion = (char *) getVersion(lDialogName)) ) return 0 ;
+ /*lVersion = lTest ;*/
+ /*printf("lVersion %s\n", lVersion);*/
+ strcpy(lBuff,lVersion);
+ lMajor = atoi( strtok(lVersion," ,.-") ) ;
+ /*printf("lMajor %d\n", lMajor);*/
+ lMinorP = strtok(0," ,.-abcdefghijklmnopqrstuvxyz");
+ lMinor = atoi( lMinorP ) ;
+ /*printf("lMinor %d\n", lMinor );*/
+ lDate = atoi( strtok(0," ,.-") ) ;
+ if (lDate<0) lDate = - lDate;
+ /*printf("lDate %d\n", lDate);*/
+ lLetter = lMinorP + strlen(lMinorP) ;
+ strcpy(lVersion,lBuff);
+ strtok(lLetter," ,.-");
+ /*printf("lLetter %s\n", lLetter);*/
+ lResult = (lMajor > 0) || ( ( lMinor == 9 ) && (*lLetter == 'b') && (lDate >= 20031126) );
+ /*printf("lResult %d\n", lResult);*/
+ return lResult;
+}
+
+
+static int whiptailPresentOnly(void)
+{
+ static int lWhiptailPresent = -1 ;
+ if (!tinyfd_allowCursesDialogs) return 0;
+ if ( lWhiptailPresent < 0 )
+ {
+ lWhiptailPresent = detectPresence( "whiptail" ) ;
+ }
+ return lWhiptailPresent ;
+}
+
+
+static char * terminalName(void)
+{
+ static char lTerminalName[128] = "*" ;
+ char lShellName[64] = "*" ;
+ int * lArray;
+
+ if ( lTerminalName[0] == '*' )
+ {
+ if ( detectPresence( "bash" ) )
+ {
+ strcpy(lShellName , "bash -c " ) ; /*good for basic input*/
+ }
+ else if ( strlen(dialogNameOnly()) || whiptailPresentOnly() )
+ {
+ strcpy(lShellName , "sh -c " ) ; /*good enough for dialog & whiptail*/
+ }
+ else
+ {
+ strcpy(lTerminalName , "" ) ;
+ return NULL ;
+ }
+
+ if ( tfd_isDarwin() )
+ {
+ if ( * strcpy(lTerminalName , "/opt/X11/bin/xterm" )
+ && detectPresence( lTerminalName ) )
+ {
+ strcat(lTerminalName , " -fa 'DejaVu Sans Mono' -fs 10 -title tinyfiledialogs -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else
+ {
+ strcpy(lTerminalName , "" ) ;
+ }
+ }
+ else if ( * strcpy(lTerminalName,"xterm") /*good (small without parameters)*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -fa 'DejaVu Sans Mono' -fs 10 -title tinyfiledialogs -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"terminator") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -x " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"lxterminal") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"konsole") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"kterm") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"tilix") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"xfce4-terminal") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -x " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"mate-terminal") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -x " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"Eterm") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"evilvte") /*good*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"pterm") /*good (only letters)*/
+ && detectPresence(lTerminalName) )
+ {
+ strcat(lTerminalName , " -e " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else if ( * strcpy(lTerminalName,"gnome-terminal")
+ && detectPresence(lTerminalName) && (lArray = getMajorMinorPatch(lTerminalName))
+ && ((lArray[0]<3) || (lArray[0]==3 && lArray[1]<=6)) )
+ {
+ strcat(lTerminalName , " --disable-factory -x " ) ;
+ strcat(lTerminalName , lShellName ) ;
+ }
+ else
+ {
+ strcpy(lTerminalName , "" ) ;
+ }
+ /* bad: koi rxterm guake tilda vala-terminal qterminal kgx
+ aterm Terminal terminology sakura lilyterm weston-terminal
+ roxterm termit xvt rxvt mrxvt urxvt */
+ }
+ if ( strlen(lTerminalName) )
+ {
+ return lTerminalName ;
+ }
+ else
+ {
+ return NULL ;
+ }
+}
+
+
+static char * dialogName(void)
+{
+ char * lDialogName ;
+ lDialogName = dialogNameOnly( ) ;
+ if ( strlen(lDialogName) && ( isTerminalRunning() || terminalName() ) )
+ {
+ return lDialogName ;
+ }
+ else
+ {
+ return NULL ;
+ }
+}
+
+
+static int whiptailPresent(void)
+{
+ int lWhiptailPresent ;
+ lWhiptailPresent = whiptailPresentOnly( ) ;
+ if ( lWhiptailPresent && ( isTerminalRunning() || terminalName() ) )
+ {
+ return lWhiptailPresent ;
+ }
+ else
+ {
+ return 0 ;
+ }
+}
+
+
+static int graphicMode(void)
+{
+ static int lGraphicMode = -1 ;
+
+ if ( lGraphicMode < 0 )
+ {
+ if ( getenvDISPLAY() ) lGraphicMode = 1 ;
+ else if ( tfd_isDarwin() || tfd_isHaiku() )
+ {
+ if ( getenv("SSH_TTY") ) lGraphicMode = 0 ;
+ else lGraphicMode = 1 ;
+ }
+ else lGraphicMode = 0 ;
+ }
+ /*printf("lGraphicMode %d %d\n", lGraphicMode);*/
+ if ( tinyfd_forceConsole && ( isTerminalRunning() || terminalName() ) ) return 0 ;
+
+ return lGraphicMode ;
+}
+
+
+static int ffplayPresent(void)
+{
+ static int lFFplayPresent = -1;
+ if (lFFplayPresent < 0)
+ {
+ lFFplayPresent = detectPresence("ffplay");
+ }
+ return lFFplayPresent;
+}
+
+
+static int pactlPresent( void )
+{
+ static int lPactlPresent = -1 ;
+ char lBuff [256] ;
+ FILE * lIn ;
+
+ if ( lPactlPresent < 0 )
+ {
+ lPactlPresent = detectPresence("pactl") ;
+ if ( lPactlPresent )
+ {
+ lIn = ( FILE * ) popen( "pactl info | grep -iF pulseaudio" , "r" ) ;
+ if ( ! (fgets( lBuff , sizeof( lBuff ) , lIn ) && ! strstr(lBuff, "PipeWire") ) )
+ {
+ lPactlPresent = 0 ;
+ }
+ pclose( lIn ) ;
+ if (tinyfd_verbose) printf("is pactl valid ? %d\n", lPactlPresent);
+ }
+ }
+ return lPactlPresent ;
+}
+
+
+static int speakertestPresent(void)
+{
+ static int lSpeakertestPresent = -1 ;
+ if ( lSpeakertestPresent < 0 )
+ {
+ lSpeakertestPresent = detectPresence("speaker-test") ;
+ }
+ return lSpeakertestPresent ;
+}
+
+
+static int playPresent(void) /* play is part of sox */
+{
+ static int lPlayPresent = -1;
+ if (lPlayPresent < 0)
+ {
+ lPlayPresent = detectPresence("sox"); /*if sox is present, play is ready*/
+ }
+ return lPlayPresent;
+}
+
+
+static int beepexePresent(void)
+{
+ static int lBeepexePresent = -1;
+ if (lBeepexePresent < 0)
+ {
+ lBeepexePresent = detectPresence("beep.exe");
+ }
+ return lBeepexePresent;
+}
+
+
+/*static int beepPresent(void)
+{
+ static int lBeepPresent = -1 ;
+ if ( lBeepPresent < 0 )
+ {
+ lBeepPresent = detectPresence("beep") ;
+ }
+ return lBeepPresent ;
+}*/
+
+
+static int playsoundPresent(void) /* playsound is part of pipewire */
+{
+ static int lPlaysoundPresent = -1 ;
+ if (lPlaysoundPresent < 0)
+ {
+ lPlaysoundPresent = detectPresence("playsound_simple");
+ if ( lPlaysoundPresent && ! fileExists("/usr/share/sounds/freedesktop/stereo/bell.oga") )
+ {
+ lPlaysoundPresent = 0 ;
+ }
+ }
+ return lPlaysoundPresent;
+}
+
+
+static int paplayPresent(void) /* playsound is part of pipewire */
+{
+ static int lPaplayPresent = -1 ;
+ if (lPaplayPresent < 0)
+ {
+ lPaplayPresent = detectPresence("paplay");
+ if ( lPaplayPresent && ! fileExists("/usr/share/sounds/freedesktop/stereo/bell.oga") )
+ {
+ lPaplayPresent = 0 ;
+ }
+ }
+ return lPaplayPresent;
+}
+
+
+static int xmessagePresent(void)
+{
+ static int lXmessagePresent = -1 ;
+ if ( lXmessagePresent < 0 )
+ {
+ lXmessagePresent = detectPresence("xmessage");/*if not tty,not on osxpath*/
+ }
+ return lXmessagePresent && graphicMode( ) ;
+}
+
+
+static int gxmessagePresent(void)
+{
+ static int lGxmessagePresent = -1 ;
+ if ( lGxmessagePresent < 0 )
+ {
+ lGxmessagePresent = detectPresence("gxmessage") ;
+ }
+ return lGxmessagePresent && graphicMode( ) ;
+}
+
+
+static int gmessagePresent(void)
+{
+ static int lGmessagePresent = -1 ;
+ if ( lGmessagePresent < 0 )
+ {
+ lGmessagePresent = detectPresence("gmessage") ;
+ }
+ return lGmessagePresent && graphicMode( ) ;
+}
+
+
+static int notifysendPresent(void)
+{
+ static int lNotifysendPresent = -1 ;
+ if ( lNotifysendPresent < 0 )
+ {
+ lNotifysendPresent = detectPresence("notify-send") ;
+ }
+ return lNotifysendPresent && graphicMode( ) ;
+}
+
+
+static int notifyPresent(void)
+{
+ static int lNotifyPresent = -1 ;
+ if ( lNotifyPresent < 0 )
+ {
+ lNotifyPresent = detectPresence("notify") ;
+ }
+ return lNotifyPresent && graphicMode( ) ;
+}
+
+
+static int perlPresent(void)
+{
+ static int lPerlPresent = -1 ;
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+
+ if ( lPerlPresent < 0 )
+ {
+ lPerlPresent = detectPresence("perl") ;
+ if (lPerlPresent)
+ {
+ lIn = ( FILE * ) popen("perl -MNet::DBus -e \"Net::DBus->session->get_service('org.freedesktop.Notifications')\" 2>&1", "r");
+ if (fgets(lBuff, sizeof(lBuff), lIn) == NULL)
+ {
+ lPerlPresent = 2;
+ }
+ pclose(lIn);
+ if (tinyfd_verbose) printf("perl-dbus %d\n", lPerlPresent);
+ }
+ }
+ return graphicMode() ? lPerlPresent : 0 ;
+}
+
+
+static int afplayPresent(void)
+{
+ static int lAfplayPresent = -1 ;
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+
+ if ( lAfplayPresent < 0 )
+ {
+ lAfplayPresent = detectPresence("afplay") ;
+ if ( lAfplayPresent )
+ {
+ lIn = ( FILE * ) popen( "test -e /System/Library/Sounds/Ping.aiff || echo Ping" , "r" ) ;
+ if ( fgets( lBuff , sizeof( lBuff ) , lIn ) == NULL )
+ {
+ lAfplayPresent = 2 ;
+ }
+ pclose( lIn ) ;
+ if (tinyfd_verbose) printf("afplay %d\n", lAfplayPresent);
+ }
+ }
+ return graphicMode() ? lAfplayPresent : 0 ;
+}
+
+
+static int xdialogPresent(void)
+{
+ static int lXdialogPresent = -1 ;
+ if ( lXdialogPresent < 0 )
+ {
+ lXdialogPresent = detectPresence("Xdialog") ;
+ }
+ return lXdialogPresent && graphicMode( ) ;
+}
+
+
+static int gdialogPresent(void)
+{
+ static int lGdialoglPresent = -1 ;
+ if ( lGdialoglPresent < 0 )
+ {
+ lGdialoglPresent = detectPresence( "gdialog" ) ;
+ }
+ return lGdialoglPresent && graphicMode( ) ;
+}
+
+
+static int osascriptPresent(void)
+{
+ static int lOsascriptPresent = -1 ;
+ if ( lOsascriptPresent < 0 )
+ {
+ gWarningDisplayed |= !!getenv("SSH_TTY");
+ lOsascriptPresent = detectPresence( "osascript" ) ;
+ }
+ return lOsascriptPresent && graphicMode() && !getenv("SSH_TTY") ;
+}
+
+
+static int dunstifyPresent(void)
+{
+ static int lDunstifyPresent = -1 ;
+ static char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ char * lTmp ;
+
+ if ( lDunstifyPresent < 0 )
+ {
+ lDunstifyPresent = detectPresence( "dunstify" ) ;
+ if ( lDunstifyPresent )
+ {
+ lIn = ( FILE * ) popen( "dunstify -s" , "r" ) ;
+ lTmp = fgets( lBuff , sizeof( lBuff ) , lIn ) ;
+ pclose( lIn ) ;
+ /* printf("lTmp:%s\n", lTmp); */
+ lDunstifyPresent = strstr(lTmp,"name:dunst\n") ? 1 : 0 ;
+ if (tinyfd_verbose) printf("lDunstifyPresent %d\n", lDunstifyPresent);
+ }
+ }
+ return lDunstifyPresent && graphicMode( ) ;
+}
+
+
+static int dunstPresent(void)
+{
+ static int lDunstPresent = -1 ;
+ static char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ char * lTmp ;
+
+ if ( lDunstPresent < 0 )
+ {
+ lDunstPresent = detectPresence( "dunst" ) ;
+ if ( lDunstPresent )
+ {
+ lIn = ( FILE * ) popen( "ps -e | grep dunst | grep -v grep" , "r" ) ; /* add "| wc -l" to receive the number of lines */
+ lTmp = fgets( lBuff , sizeof( lBuff ) , lIn ) ;
+ pclose( lIn ) ;
+ /* if ( lTmp ) printf("lTmp:%s\n", lTmp); */
+ if ( lTmp ) lDunstPresent = 1 ;
+ else lDunstPresent = 0 ;
+ if (tinyfd_verbose) printf("lDunstPresent %d\n", lDunstPresent);
+ }
+ }
+ return lDunstPresent && graphicMode( ) ;
+}
+
+
+int tfd_shantyPresent(void)
+{
+ static int lShantyPresent = -1 ;
+ if ( lShantyPresent < 0 )
+ {
+ lShantyPresent = detectPresence("shanty") ;
+ }
+ /*printf("lShantyPresent %d\n", lShantyPresent);*/
+ return lShantyPresent && graphicMode( ) ;
+}
+
+
+int tfd_boxerPresent(void)
+{
+ static int lBoxerPresent = -1 ;
+ if ( lBoxerPresent < 0 )
+ {
+ lBoxerPresent = detectPresence("boxer") ;
+ }
+ /*printf("lBoxerPresent %d\n", lBoxerPresent);*/
+ return lBoxerPresent && graphicMode( ) ;
+}
+
+
+int tfd_qarmaPresent(void)
+{
+ static int lQarmaPresent = -1 ;
+ if ( lQarmaPresent < 0 )
+ {
+ lQarmaPresent = detectPresence("qarma") ;
+ }
+ return lQarmaPresent && graphicMode( ) ;
+}
+
+
+int tfd_matedialogPresent(void)
+{
+ static int lMatedialogPresent = -1 ;
+ if ( lMatedialogPresent < 0 )
+ {
+ lMatedialogPresent = detectPresence("matedialog") ;
+ }
+ return lMatedialogPresent && graphicMode( ) ;
+}
+
+
+int tfd_shellementaryPresent(void)
+{
+ static int lShellementaryPresent = -1 ;
+ if ( lShellementaryPresent < 0 )
+ {
+ lShellementaryPresent = 0 ; /*detectPresence("shellementary"); shellementary is not ready yet */
+ }
+ return lShellementaryPresent && graphicMode( ) ;
+}
+
+
+int tfd_xpropPresent(void)
+{
+ static int lXpropReady = 0 ;
+ static int lXpropDetected = -1 ;
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+
+ if ( lXpropDetected < 0 )
+ {
+ if ( getenvDISPLAY() & 1 ) lXpropDetected = detectPresence("xprop") ; /* bitwise & */
+ else lXpropDetected = 0 ;
+ }
+
+ if ( ! lXpropDetected ) return 0 ;
+
+ if ( ! lXpropReady )
+ { /* xwayland Debian issue reported by Kay F. Jahnke and solved with his help */
+ lIn = ( FILE * ) popen( "xprop -root 32x ' $0' _NET_ACTIVE_WINDOW" , "r" ) ;
+ if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {
+ if ( ! strstr( lBuff , "not found" ) )
+ {
+ if (tinyfd_verbose) printf("xprop is ready\n");
+ lXpropReady = 1 ;
+ }
+ }
+ pclose( lIn ) ;
+ }
+ return graphicMode() ? lXpropReady : 0 ;
+}
+
+
+int tfd_yadPresent(void)
+{
+ static int lYadPresent = -1;
+ if (lYadPresent < 0)
+ {
+ lYadPresent = detectPresence("yad");
+ }
+ return lYadPresent && graphicMode();
+}
+
+
+int tfd_zenityPresent(void)
+{
+ static int lZenityPresent = -1 ;
+ if ( lZenityPresent < 0 )
+ {
+ lZenityPresent = detectPresence("zenity") ;
+ }
+ return lZenityPresent && graphicMode( ) ;
+}
+
+
+int tfd_zenityVersion(void)
+{
+ static int lZenityVersion = -1 ;
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+
+ if ( lZenityVersion < 0 )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ lIn = ( FILE * ) popen( "zenity --version" , "r" ) ;
+ if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {
+ lZenityVersion = 1000 * atoi( lBuff ) ;
+ lZenityVersion += atoi( strtok( lBuff, "." ) + 2 ) ;
+ /* 3018 , 3010 , 3000, 2032 was 5 , 4 , 3, 2 */
+ }
+ pclose( lIn ) ;
+ }
+ else lZenityVersion = 0 ;
+ if (tinyfd_verbose) printf("lZenityVersion %d\n", lZenityVersion ) ;
+ }
+ return graphicMode() ? lZenityVersion : 0 ;
+}
+
+
+int tfd_kdialogPresent(void)
+{
+ static int lKdialogPresent = -1 ;
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ char * lDesktop;
+
+ if ( lKdialogPresent < 0 )
+ {
+ lDesktop = getenv("XDG_SESSION_DESKTOP");
+ if ( !lDesktop || ( strcmp(lDesktop, "KDE") && strcmp(lDesktop, "lxqt") ) )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ lKdialogPresent = 0 ;
+ return lKdialogPresent ;
+ }
+ }
+
+ lKdialogPresent = detectPresence("kdialog") ;
+ if ( lKdialogPresent && !getenv("SSH_TTY") )
+ {
+ lIn = ( FILE * ) popen( "kdialog --attach 2>&1" , "r" ) ;
+ if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {
+ if ( ! strstr( "Unknown" , lBuff ) )
+ {
+ lKdialogPresent = 2 ;
+ if (tinyfd_verbose) printf("kdialog-attach %d\n", lKdialogPresent);
+ }
+ }
+ pclose( lIn ) ;
+
+ if (lKdialogPresent == 2)
+ {
+ lKdialogPresent = 1 ;
+ lIn = ( FILE * ) popen( "kdialog --passivepopup 2>&1" , "r" ) ;
+ if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {
+ if ( ! strstr( "Unknown" , lBuff ) )
+ {
+ lKdialogPresent = 2 ;
+ if (tinyfd_verbose) printf("kdialog-popup %d\n", lKdialogPresent);
+ }
+ }
+ pclose( lIn ) ;
+ }
+ }
+ }
+ return graphicMode() ? lKdialogPresent : 0 ;
+}
+
+
+static int osx9orBetter(void)
+{
+ static int lOsx9orBetter = -1 ;
+ char lBuff[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ int V,v;
+
+ if ( lOsx9orBetter < 0 )
+ {
+ lOsx9orBetter = 0 ;
+ lIn = ( FILE * ) popen( "osascript -e 'set osver to system version of (system info)'" , "r" ) ;
+ V = 0 ;
+ if ( ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ && ( 2 == sscanf(lBuff, "%d.%d", &V, &v) ) )
+ {
+ V = V * 100 + v;
+ if ( V >= 1009 )
+ {
+ lOsx9orBetter = 1 ;
+ }
+ }
+ pclose( lIn ) ;
+ if (tinyfd_verbose) printf("Osx10 = %d, %d = %s\n", lOsx9orBetter, V, lBuff) ;
+ }
+ return lOsx9orBetter ;
+}
+
+
+static int python3Present(void)
+{
+ static int lPython3Present = -1 ;
+
+ if ( lPython3Present < 0 )
+ {
+ lPython3Present = 0 ;
+ strcpy(gPython3Name , "python3" ) ;
+ if ( detectPresence(gPython3Name) ) lPython3Present = 1;
+ if (tinyfd_verbose) printf("lPython3Present %d\n", lPython3Present) ;
+ if (tinyfd_verbose) printf("gPython3Name %s\n", gPython3Name) ;
+ }
+ return lPython3Present ;
+}
+
+
+static int python2Present(void)
+{
+ static int lPython2Present = -1 ;
+
+ if ( lPython2Present < 0 )
+ {
+ lPython2Present = 0 ;
+ strcpy(gPython2Name , "python2" ) ;
+ if ( detectPresence(gPython2Name) ) lPython2Present = 1;
+ if (tinyfd_verbose) printf("lPython2Present %d\n", lPython2Present) ;
+ if (tinyfd_verbose) printf("gPython2Name %s\n", gPython2Name) ;
+ }
+ return lPython2Present ;
+}
+
+
+static int tkinter3Present(void)
+{
+ static int lTkinter3Present = -1 ;
+ char lPythonCommand[256];
+ char lPythonParams[128] =
+ "-S -c \"try:\n\timport tkinter;\nexcept:\n\tprint(0);\"";
+
+ if ( lTkinter3Present < 0 )
+ {
+ lTkinter3Present = 0 ;
+ if ( python3Present() )
+ {
+ sprintf( lPythonCommand , "%s %s" , gPython3Name , lPythonParams ) ;
+ lTkinter3Present = tryCommand(lPythonCommand) ;
+ }
+ if (tinyfd_verbose) printf("lTkinter3Present %d\n", lTkinter3Present) ;
+ }
+ return lTkinter3Present && graphicMode() && !(tfd_isDarwin() && getenv("SSH_TTY") );
+}
+
+
+static int tkinter2Present(void)
+{
+ static int lTkinter2Present = -1 ;
+ char lPythonCommand[256];
+ char lPythonParams[128] =
+ "-S -c \"try:\n\timport Tkinter;\nexcept:\n\tprint 0;\"";
+
+ if ( lTkinter2Present < 0 )
+ {
+ lTkinter2Present = 0 ;
+ if ( python2Present() )
+ {
+ sprintf( lPythonCommand , "%s %s" , gPython2Name , lPythonParams ) ;
+ lTkinter2Present = tryCommand(lPythonCommand) ;
+ }
+ if (tinyfd_verbose) printf("lTkinter2Present %d graphicMode %d \n", lTkinter2Present, graphicMode() ) ;
+ }
+ return lTkinter2Present && graphicMode() && !(tfd_isDarwin() && getenv("SSH_TTY") );
+}
+
+
+static int pythonDbusPresent(void)
+{
+ static int lPythonDbusPresent = -1 ;
+ char lPythonCommand[384];
+ char lPythonParams[256] =
+"-c \"try:\n\timport dbus;bus=dbus.SessionBus();\
+notif=bus.get_object('org.freedesktop.Notifications','/org/freedesktop/Notifications');\
+notify=dbus.Interface(notif,'org.freedesktop.Notifications');\nexcept:\n\tprint(0);\"";
+
+ if (lPythonDbusPresent < 0 )
+ {
+ lPythonDbusPresent = 0 ;
+ if ( python2Present() )
+ {
+ strcpy(gPythonName , gPython2Name ) ;
+ sprintf( lPythonCommand , "%s %s" , gPythonName , lPythonParams ) ;
+ lPythonDbusPresent = tryCommand(lPythonCommand) ;
+ }
+
+ if ( !lPythonDbusPresent && python3Present() )
+ {
+ strcpy(gPythonName , gPython3Name ) ;
+ sprintf( lPythonCommand , "%s %s" , gPythonName , lPythonParams ) ;
+ lPythonDbusPresent = tryCommand(lPythonCommand) ;
+ }
+
+ if (tinyfd_verbose) printf("lPythonDbusPresent %d\n", lPythonDbusPresent) ;
+ if (tinyfd_verbose) printf("gPythonName %s\n", gPythonName) ;
+ }
+ return lPythonDbusPresent && graphicMode() && !(tfd_isDarwin() && getenv("SSH_TTY") );
+}
+
+
+static void sigHandler(int signum)
+{
+ FILE * lIn = ( FILE * ) popen( "pactl unload-module module-sine" , "r" );
+ if ( lIn )
+ {
+ pclose( lIn ) ;
+ }
+ if (tinyfd_verbose) printf("tinyfiledialogs caught signal %d\n", signum);
+}
+
+
+void tinyfd_beep(void)
+{
+ char lDialogString[256] ;
+ FILE * lIn ;
+
+ if ( pactlPresent() )
+ {
+ signal(SIGINT, sigHandler);
+ strcpy( lDialogString ,
+ "thnum=$(pactl load-module module-sine frequency=440);sleep .3;pactl unload-module $thnum" ) ;
+ }
+ else if ( osascriptPresent() )
+ {
+ if ( afplayPresent() >= 2 )
+ {
+ strcpy( lDialogString , "afplay /System/Library/Sounds/Ping.aiff") ;
+ }
+ else
+ {
+ strcpy( lDialogString , "osascript -e 'tell application \"System Events\" to beep'") ;
+ }
+ }
+ else if ( speakertestPresent() )
+ {
+ /*strcpy( lDialogString , "timeout -k .3 .3 speaker-test --frequency 440 --test sine > /dev/tty" ) ;*/
+ strcpy( lDialogString , "( speaker-test -t sine -f 440 > /dev/tty )& pid=$!;sleep .5; kill -9 $pid" ) ; /*.3 was too short for mac g3*/
+ }
+ else if ( ffplayPresent() )
+ {
+ strcpy(lDialogString, "ffplay -f lavfi -i sine=f=440:d=0.15 -autoexit -nodisp" );
+ }
+ else if (playPresent()) /* play is part of sox */
+ {
+ strcpy(lDialogString, "play -q -n synth .3 sine 440");
+ }
+ else if ( playsoundPresent() )
+ {
+ strcpy( lDialogString , "playsound_simple /usr/share/sounds/freedesktop/stereo/bell.oga") ;
+ }
+ else if ( paplayPresent() )
+ {
+ strcpy( lDialogString , "paplay /usr/share/sounds/freedesktop/stereo/bell.oga") ;
+ }
+ else if (beepexePresent())
+ {
+ strcpy(lDialogString, "beep.exe 440 300");
+ }
+ /*else if ( beepPresent() )
+ {
+ strcpy( lDialogString , "beep -f 440 -l 300" ) ;
+ }*/
+ else
+ {
+ strcpy( lDialogString , "printf '\\a' > /dev/tty" ) ;
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+
+ lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+ if ( lIn )
+ {
+ pclose( lIn ) ;
+ }
+
+ if ( pactlPresent() )
+ {
+ signal(SIGINT, SIG_DFL);
+ }
+}
+
+
+int tinyfd_messageBox(
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" may contain \n and \t */
+ char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */
+ char const * aIconType , /* "info" "warning" "error" "question" */
+ int aDefaultButton ) /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
+{
+ char lBuff[MAX_PATH_OR_CMD] ;
+ char lBuff2[MAX_PATH_OR_CMD] ;
+ char * lDialogString = NULL ;
+ char * lpDialogString;
+ FILE * lIn ;
+ int lWasGraphicDialog = 0 ;
+ int lWasXterm = 0 ;
+ int lResult ;
+ char lChar ;
+ struct termios infoOri;
+ struct termios info;
+ size_t lTitleLen ;
+ size_t lMessageLen ;
+
+ lBuff[0]='\0';
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_messageBox("INVALID TITLE WITH QUOTES", aMessage, aDialogType, aIconType, aDefaultButton);
+ if (tfd_quoteDetected(aMessage)) return tinyfd_messageBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDialogType, aIconType, aDefaultButton);
+
+ lTitleLen = aTitle ? strlen(aTitle) : 0 ;
+ lMessageLen = aMessage ? strlen(aMessage) : 0 ;
+ if ( !aTitle || strcmp(aTitle,"tinyfd_query") )
+ {
+ lDialogString = (char *) malloc( MAX_PATH_OR_CMD + lTitleLen + lMessageLen );
+ }
+
+ if ( osascriptPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return 1;}
+
+ strcpy( lDialogString , "osascript ");
+ if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+ strcat( lDialogString , " -e 'try' -e 'set {vButton} to {button returned} of ( display dialog \"") ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage) ;
+ }
+ strcat(lDialogString, "\" ") ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "with title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+ strcat(lDialogString, "with icon ") ;
+ if ( aIconType && ! strcmp( "error" , aIconType ) )
+ {
+ strcat(lDialogString, "stop " ) ;
+ }
+ else if ( aIconType && ! strcmp( "warning" , aIconType ) )
+ {
+ strcat(lDialogString, "caution " ) ;
+ }
+ else /* question or info */
+ {
+ strcat(lDialogString, "note " ) ;
+ }
+ if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+ {
+ if ( ! aDefaultButton )
+ {
+ strcat( lDialogString ,"default button \"Cancel\" " ) ;
+ }
+ }
+ else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+ {
+ strcat( lDialogString ,"buttons {\"No\", \"Yes\"} " ) ;
+ if (aDefaultButton)
+ {
+ strcat( lDialogString ,"default button \"Yes\" " ) ;
+ }
+ else
+ {
+ strcat( lDialogString ,"default button \"No\" " ) ;
+ }
+ strcat( lDialogString ,"cancel button \"No\"" ) ;
+ }
+ else if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat( lDialogString ,"buttons {\"No\", \"Yes\", \"Cancel\"} " ) ;
+ switch (aDefaultButton)
+ {
+ case 1: strcat( lDialogString ,"default button \"Yes\" " ) ; break;
+ case 2: strcat( lDialogString ,"default button \"No\" " ) ; break;
+ case 0: strcat( lDialogString ,"default button \"Cancel\" " ) ; break;
+ }
+ strcat( lDialogString ,"cancel button \"Cancel\"" ) ;
+ }
+ else
+ {
+ strcat( lDialogString ,"buttons {\"OK\"} " ) ;
+ strcat( lDialogString ,"default button \"OK\" " ) ;
+ }
+ strcat( lDialogString, ")' ") ;
+
+ strcat( lDialogString,
+"-e 'if vButton is \"Yes\" then' -e 'return 1' \
+-e 'else if vButton is \"OK\" then' -e 'return 1' \
+-e 'else if vButton is \"No\" then' -e 'return 2' \
+-e 'else' -e 'return 0' -e 'end if' " );
+
+ strcat( lDialogString, "-e 'on error number -128' " ) ;
+ strcat( lDialogString, "-e '0' " );
+
+ strcat( lDialogString, "-e 'end try'") ;
+ if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
+ }
+ else if ( tfd_kdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return 1;}
+
+ strcpy( lDialogString , "kdialog" ) ;
+ if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+
+ strcat( lDialogString , " --" ) ;
+ if ( aDialogType && ( ! strcmp( "okcancel" , aDialogType )
+ || ! strcmp( "yesno" , aDialogType ) || ! strcmp( "yesnocancel" , aDialogType ) ) )
+ {
+ if ( aIconType && ( ! strcmp( "warning" , aIconType )
+ || ! strcmp( "error" , aIconType ) ) )
+ {
+ strcat( lDialogString , "warning" ) ;
+ }
+ if ( ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat( lDialogString , "yesnocancel" ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "yesno" ) ;
+ }
+ }
+ else if ( aIconType && ! strcmp( "error" , aIconType ) )
+ {
+ strcat( lDialogString , "error" ) ;
+ }
+ else if ( aIconType && ! strcmp( "warning" , aIconType ) )
+ {
+ strcat( lDialogString , "sorry" ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "msgbox" ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ if ( aMessage )
+ {
+ strcat( lDialogString , aMessage ) ;
+ }
+ strcat( lDialogString , "\"" ) ;
+ if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+ {
+ strcat( lDialogString ,
+ " --yes-label Ok --no-label Cancel" ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, " --title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+ }
+
+ if ( ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat( lDialogString , "; x=$? ;if [ $x = 0 ] ;then echo 1;elif [ $x = 1 ] ;then echo 2;else echo 0;fi");
+ }
+ else
+ {
+ strcat( lDialogString , ";if [ $? = 0 ];then echo 1;else echo 0;fi");
+ }
+ }
+ else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+ || tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return 1;}
+ strcpy( lDialogString , "szAnswer=$(zenity" ) ;
+ if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_matedialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return 1;}
+ strcpy( lDialogString , "szAnswer=$(matedialog" ) ;
+ }
+ else if ( tfd_shellementaryPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return 1;}
+ strcpy( lDialogString , "szAnswer=$(shellementary" ) ;
+ }
+ else if ( tfd_qarmaPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return 1;}
+ strcpy( lDialogString , "szAnswer=$(qarma" ) ;
+ if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_boxerPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return 1;}
+ strcpy( lDialogString , "szAnswer=$(boxer" ) ;
+ }
+ else if ( tfd_shantyPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return 1;}
+ strcpy( lDialogString , "szAnswer=$(shanty" ) ;
+ }
+ else {}
+
+ strcat(lDialogString, " --");
+
+ if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+ {
+ strcat( lDialogString ,
+ "question --ok-label=Ok --cancel-label=Cancel" ) ;
+ }
+ else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+ {
+ strcat( lDialogString , "question" ) ;
+ }
+ else if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat( lDialogString , "list --column \"\" --hide-header \"Yes\" \"No\"" ) ;
+ }
+ else if ( aIconType && ! strcmp( "error" , aIconType ) )
+ {
+ strcat( lDialogString , "error" ) ;
+ }
+ else if ( aIconType && ! strcmp( "warning" , aIconType ) )
+ {
+ strcat( lDialogString , "warning" ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "info" ) ;
+ }
+
+ strcat(lDialogString, " --title=\"");
+ if ( aTitle && strlen(aTitle) ) strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"");
+
+ if (strcmp("yesnocancel", aDialogType)) strcat(lDialogString, " --no-wrap");
+
+ strcat(lDialogString, " --text=\"") ;
+ if (aMessage && strlen(aMessage)) strcat(lDialogString, aMessage) ;
+ strcat(lDialogString, "\"") ;
+
+ if ( (tfd_zenityVersion()>=3000) || tfd_boxerPresent() )
+ {
+ if ( (tfd_zenityVersion()>=3900) || tfd_boxerPresent() ) strcat( lDialogString , " --icon=dialog-" ) ;
+ else strcat( lDialogString , " --icon-name=dialog-" ) ;
+
+ if ( aIconType && (! strcmp( "question" , aIconType )
+ || ! strcmp( "error" , aIconType )
+ || ! strcmp( "warning" , aIconType ) ) )
+ {
+ strcat( lDialogString , aIconType ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "information" ) ;
+ }
+ }
+
+ if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
+
+ if ( ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat( lDialogString ,
+");if [ $? = 1 ];then echo 0;elif [ $szAnswer = \"No\" ];then echo 2;else echo 1;fi");
+ }
+ else
+ {
+ strcat( lDialogString , ");if [ $? = 0 ];then echo 1;else echo 0;fi");
+ }
+ }
+
+ else if (tfd_yadPresent())
+ {
+ if (aTitle && !strcmp(aTitle, "tinyfd_query")) { strcpy(tinyfd_response, "yad"); return 1; }
+ strcpy(lDialogString, "szAnswer=$(yad --");
+ if (aDialogType && !strcmp("ok", aDialogType))
+ {
+ strcat(lDialogString,"button=Ok:1");
+ }
+ else if (aDialogType && !strcmp("okcancel", aDialogType))
+ {
+ strcat(lDialogString,"button=Ok:1 --button=Cancel:0");
+ }
+ else if (aDialogType && !strcmp("yesno", aDialogType))
+ {
+ strcat(lDialogString, "button=Yes:1 --button=No:0");
+ }
+ else if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ strcat(lDialogString, "button=Yes:1 --button=No:2 --button=Cancel:0");
+ }
+ else if (aIconType && !strcmp("error", aIconType))
+ {
+ strcat(lDialogString, "error");
+ }
+ else if (aIconType && !strcmp("warning", aIconType))
+ {
+ strcat(lDialogString, "warning");
+ }
+ else
+ {
+ strcat(lDialogString, "info");
+ }
+ if (aTitle && strlen(aTitle))
+ {
+ strcat(lDialogString, " --title=\"");
+ strcat(lDialogString, aTitle);
+ strcat(lDialogString, "\"");
+ }
+ if (aMessage && strlen(aMessage))
+ {
+ strcat(lDialogString, " --text=\"");
+ strcat(lDialogString, aMessage);
+ strcat(lDialogString, "\"");
+ }
+
+ strcat(lDialogString, " --image=dialog-");
+ if (aIconType && (!strcmp("question", aIconType)
+ || !strcmp("error", aIconType)
+ || !strcmp("warning", aIconType)))
+ {
+ strcat(lDialogString, aIconType);
+ }
+ else
+ {
+ strcat(lDialogString, "information");
+ }
+
+ if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
+ strcat(lDialogString,");echo $?");
+ }
+
+ else if ( !gxmessagePresent() && !gmessagePresent() && !gdialogPresent() && !xdialogPresent() && tkinter3Present() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python3-tkinter");return 1;}
+
+ strcpy( lDialogString , gPython3Name ) ;
+ strcat( lDialogString ,
+ " -S -c \"import tkinter;from tkinter import messagebox;root=tkinter.Tk();root.withdraw();");
+
+ strcat( lDialogString ,"res=messagebox." ) ;
+ if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+ {
+ strcat( lDialogString , "askokcancel(" ) ;
+ if ( aDefaultButton )
+ {
+ strcat( lDialogString , "default=messagebox.OK," ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "default=messagebox.CANCEL," ) ;
+ }
+ }
+ else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+ {
+ strcat( lDialogString , "askyesno(" ) ;
+ if ( aDefaultButton )
+ {
+ strcat( lDialogString , "default=messagebox.YES," ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "default=messagebox.NO," ) ;
+ }
+ }
+ else if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat( lDialogString , "askyesnocancel(" ) ;
+ switch ( aDefaultButton )
+ {
+ case 1: strcat( lDialogString , "default=messagebox.YES," ); break;
+ case 2: strcat( lDialogString , "default=messagebox.NO," ); break;
+ case 0: strcat( lDialogString , "default=messagebox.CANCEL," ); break;
+ }
+ }
+ else
+ {
+ strcat( lDialogString , "showinfo(" ) ;
+ }
+
+ strcat( lDialogString , "icon='" ) ;
+ if ( aIconType && (! strcmp( "question" , aIconType )
+ || ! strcmp( "error" , aIconType )
+ || ! strcmp( "warning" , aIconType ) ) )
+ {
+ strcat( lDialogString , aIconType ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "info" ) ;
+ }
+
+ strcat(lDialogString, "',") ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, "message='") ;
+ lpDialogString = lDialogString + strlen(lDialogString);
+ tfd_replaceSubStr( aMessage , "\n" , "\\n" , lpDialogString ) ;
+ strcat(lDialogString, "'") ;
+ }
+
+ if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat(lDialogString, ");\n\
+if res is None :\n\tprint(0)\n\
+elif res is False :\n\tprint(2)\n\
+else :\n\tprint (1)\n\"" ) ;
+ }
+ else
+ {
+ strcat(lDialogString, ");\n\
+if res is False :\n\tprint(0)\n\
+else :\n\tprint(1)\n\"" ) ;
+ }
+ }
+ else if ( !gxmessagePresent() && !gmessagePresent() && !gdialogPresent() && !xdialogPresent() && tkinter2Present() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python2-tkinter");return 1;}
+ strcpy( lDialogString , "export PYTHONIOENCODING=utf-8;" ) ;
+ strcat( lDialogString , gPython2Name ) ;
+ if ( ! isTerminalRunning( ) && tfd_isDarwin( ) )
+ {
+ strcat( lDialogString , " -i" ) ; /* for osx without console */
+ }
+
+ strcat( lDialogString ,
+" -S -c \"import Tkinter,tkMessageBox;root=Tkinter.Tk();root.withdraw();");
+
+ if ( tfd_isDarwin( ) )
+ {
+ strcat( lDialogString ,
+"import os;os.system('''/usr/bin/osascript -e 'tell app \\\"Finder\\\" to set \
+frontmost of process \\\"Python\\\" to true' ''');");
+ }
+
+ strcat( lDialogString ,"res=tkMessageBox." ) ;
+ if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+ {
+ strcat( lDialogString , "askokcancel(" ) ;
+ if ( aDefaultButton )
+ {
+ strcat( lDialogString , "default=tkMessageBox.OK," ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "default=tkMessageBox.CANCEL," ) ;
+ }
+ }
+ else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+ {
+ strcat( lDialogString , "askyesno(" ) ;
+ if ( aDefaultButton )
+ {
+ strcat( lDialogString , "default=tkMessageBox.YES," ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "default=tkMessageBox.NO," ) ;
+ }
+ }
+ else if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat( lDialogString , "askyesnocancel(" ) ;
+ switch ( aDefaultButton )
+ {
+ case 1: strcat( lDialogString , "default=tkMessageBox.YES," ); break;
+ case 2: strcat( lDialogString , "default=tkMessageBox.NO," ); break;
+ case 0: strcat( lDialogString , "default=tkMessageBox.CANCEL," ); break;
+ }
+ }
+ else
+ {
+ strcat( lDialogString , "showinfo(" ) ;
+ }
+
+ strcat( lDialogString , "icon='" ) ;
+ if ( aIconType && (! strcmp( "question" , aIconType )
+ || ! strcmp( "error" , aIconType )
+ || ! strcmp( "warning" , aIconType ) ) )
+ {
+ strcat( lDialogString , aIconType ) ;
+ }
+ else
+ {
+ strcat( lDialogString , "info" ) ;
+ }
+
+ strcat(lDialogString, "',") ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, "message='") ;
+ lpDialogString = lDialogString + strlen(lDialogString);
+ tfd_replaceSubStr( aMessage , "\n" , "\\n" , lpDialogString ) ;
+ strcat(lDialogString, "'") ;
+ }
+
+ if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+ {
+ strcat(lDialogString, ");\n\
+if res is None :\n\tprint 0\n\
+elif res is False :\n\tprint 2\n\
+else :\n\tprint 1\n\"" ) ;
+ }
+ else
+ {
+ strcat(lDialogString, ");\n\
+if res is False :\n\tprint 0\n\
+else :\n\tprint 1\n\"" ) ;
+ }
+ }
+ else if ( gxmessagePresent() || gmessagePresent() || (!gdialogPresent() && !xdialogPresent() && xmessagePresent()) )
+ {
+ if ( gxmessagePresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"gxmessage");return 1;}
+ strcpy( lDialogString , "gxmessage");
+ }
+ else if ( gmessagePresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"gmessage");return 1;}
+ strcpy( lDialogString , "gmessage");
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"xmessage");return 1;}
+ strcpy( lDialogString , "xmessage");
+ }
+
+ if ( aDialogType && ! strcmp("okcancel" , aDialogType) )
+ {
+ strcat( lDialogString , " -buttons Ok:1,Cancel:0");
+ switch ( aDefaultButton )
+ {
+ case 1: strcat( lDialogString , " -default Ok"); break;
+ case 0: strcat( lDialogString , " -default Cancel"); break;
+ }
+ }
+ else if ( aDialogType && ! strcmp("yesno" , aDialogType) )
+ {
+ strcat( lDialogString , " -buttons Yes:1,No:0");
+ switch ( aDefaultButton )
+ {
+ case 1: strcat( lDialogString , " -default Yes"); break;
+ case 0: strcat( lDialogString , " -default No"); break;
+ }
+ }
+ else if ( aDialogType && ! strcmp("yesnocancel" , aDialogType) )
+ {
+ strcat( lDialogString , " -buttons Yes:1,No:2,Cancel:0");
+ switch ( aDefaultButton )
+ {
+ case 1: strcat( lDialogString , " -default Yes"); break;
+ case 2: strcat( lDialogString , " -default No"); break;
+ case 0: strcat( lDialogString , " -default Cancel"); break;
+ }
+ }
+ else
+ {
+ strcat( lDialogString , " -buttons Ok:1");
+ strcat( lDialogString , " -default Ok");
+ }
+
+ strcat( lDialogString , " -center \"");
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat( lDialogString , aMessage ) ;
+ }
+ strcat(lDialogString, "\"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat( lDialogString , " -title \"");
+ strcat( lDialogString , aTitle ) ;
+ strcat( lDialogString, "\"" ) ;
+ }
+ strcat( lDialogString , " ; echo $? ");
+ }
+ else if ( xdialogPresent() || gdialogPresent() || dialogName() || whiptailPresent() )
+ {
+ if ( gdialogPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"gdialog");return 1;}
+ lWasGraphicDialog = 1 ;
+ strcpy( lDialogString , "(gdialog " ) ;
+ }
+ else if ( xdialogPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"xdialog");return 1;}
+ lWasGraphicDialog = 1 ;
+ strcpy( lDialogString , "(Xdialog " ) ;
+ }
+ else if ( dialogName( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return 0;}
+ if ( isTerminalRunning( ) )
+ {
+ strcpy( lDialogString , "(dialog " ) ;
+ }
+ else
+ {
+ lWasXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'(" ) ;
+ strcat( lDialogString , dialogName() ) ;
+ strcat( lDialogString , " " ) ;
+ }
+ }
+ else if ( isTerminalRunning( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"whiptail");return 0;}
+ strcpy( lDialogString , "(whiptail " ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"whiptail");return 0;}
+ lWasXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'(whiptail " ) ;
+ }
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( !xdialogPresent() && !gdialogPresent() )
+ {
+ if ( aDialogType && ( !strcmp( "okcancel" , aDialogType ) || !strcmp( "yesno" , aDialogType )
+ || !strcmp( "yesnocancel" , aDialogType ) ) )
+ {
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString, "tab: move focus") ;
+ strcat(lDialogString, "\" ") ;
+ }
+ }
+
+ if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+ {
+ if ( ! aDefaultButton )
+ {
+ strcat( lDialogString , "--defaultno " ) ;
+ }
+ strcat( lDialogString ,
+ "--yes-label \"Ok\" --no-label \"Cancel\" --yesno " ) ;
+ }
+ else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+ {
+ if ( ! aDefaultButton )
+ {
+ strcat( lDialogString , "--defaultno " ) ;
+ }
+ strcat( lDialogString , "--yesno " ) ;
+ }
+ else if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ if (!aDefaultButton)
+ {
+ strcat(lDialogString, "--defaultno ");
+ }
+ strcat(lDialogString, "--menu ");
+ }
+ else
+ {
+ strcat( lDialogString , "--msgbox " ) ;
+
+ }
+ strcat( lDialogString , "\"" ) ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage) ;
+ }
+ strcat(lDialogString, "\" ");
+
+ if ( lWasGraphicDialog )
+ {
+ if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ strcat(lDialogString,"0 60 0 Yes \"\" No \"\") 2>/tmp/tinyfd.txt;\
+if [ $? = 0 ];then tinyfdBool=1;else tinyfdBool=0;fi;\
+tinyfdRes=$(cat /tmp/tinyfd.txt);echo $tinyfdBool$tinyfdRes") ;
+ }
+ else
+ {
+ strcat(lDialogString,
+ "10 60 ) 2>&1;if [ $? = 0 ];then echo 1;else echo 0;fi");
+ }
+ }
+ else
+ {
+ if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ strcat(lDialogString,"0 60 0 Yes \"\" No \"\" >/dev/tty ) 2>/tmp/tinyfd.txt;\
+ if [ $? = 0 ];then tinyfdBool=1;else tinyfdBool=0;fi;\
+ tinyfdRes=$(cat /tmp/tinyfd.txt);echo $tinyfdBool$tinyfdRes") ;
+
+ if ( lWasXterm )
+ {
+ strcat(lDialogString," >/tmp/tinyfd0.txt';cat /tmp/tinyfd0.txt");
+ }
+ else
+ {
+ strcat(lDialogString, "; clear >/dev/tty") ;
+ }
+ }
+ else
+ {
+ strcat(lDialogString, "10 60 >/dev/tty) 2>&1;if [ $? = 0 ];");
+ if ( lWasXterm )
+ {
+ strcat( lDialogString ,
+"then\n\techo 1\nelse\n\techo 0\nfi >/tmp/tinyfd.txt';cat /tmp/tinyfd.txt;rm /tmp/tinyfd.txt");
+ }
+ else
+ {
+ strcat(lDialogString,
+ "then echo 1;else echo 0;fi;clear >/dev/tty");
+ }
+ }
+ }
+ }
+ else if ( !isTerminalRunning() && terminalName() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"basicinput");return 0;}
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'" ) ;
+ if ( !gWarningDisplayed && !tinyfd_forceConsole)
+ {
+ gWarningDisplayed = 1 ;
+ strcat( lDialogString , "echo \"" ) ;
+ strcat( lDialogString, gTitle) ;
+ strcat( lDialogString , "\";" ) ;
+ strcat( lDialogString , "echo \"" ) ;
+ strcat( lDialogString, tinyfd_needs) ;
+ strcat( lDialogString , "\";echo;echo;" ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat( lDialogString , "echo \"" ) ;
+ strcat( lDialogString, aTitle) ;
+ strcat( lDialogString , "\";echo;" ) ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat( lDialogString , "echo \"" ) ;
+ strcat( lDialogString, aMessage) ;
+ strcat( lDialogString , "\"; " ) ;
+ }
+ if ( aDialogType && !strcmp("yesno",aDialogType) )
+ {
+ strcat( lDialogString , "echo -n \"y/n: \"; " ) ;
+ strcat( lDialogString , "stty sane -echo;" ) ;
+ strcat( lDialogString ,
+ "answer=$( while ! head -c 1 | grep -i [ny];do true ;done);");
+ strcat( lDialogString ,
+ "if echo \"$answer\" | grep -iq \"^y\";then\n");
+ strcat( lDialogString , "\techo 1\nelse\n\techo 0\nfi" ) ;
+ }
+ else if ( aDialogType && !strcmp("okcancel",aDialogType) )
+ {
+ strcat( lDialogString , "echo -n \"[O]kay/[C]ancel: \"; " ) ;
+ strcat( lDialogString , "stty sane -echo;" ) ;
+ strcat( lDialogString ,
+ "answer=$( while ! head -c 1 | grep -i [oc];do true ;done);");
+ strcat( lDialogString ,
+ "if echo \"$answer\" | grep -iq \"^o\";then\n");
+ strcat( lDialogString , "\techo 1\nelse\n\techo 0\nfi" ) ;
+ }
+ else if ( aDialogType && !strcmp("yesnocancel",aDialogType) )
+ {
+ strcat( lDialogString , "echo -n \"[Y]es/[N]o/[C]ancel: \"; " ) ;
+ strcat( lDialogString , "stty sane -echo;" ) ;
+ strcat( lDialogString ,
+ "answer=$( while ! head -c 1 | grep -i [nyc];do true ;done);");
+ strcat( lDialogString ,
+ "if echo \"$answer\" | grep -iq \"^y\";then\n\techo 1\n");
+ strcat( lDialogString , "elif echo \"$answer\" | grep -iq \"^n\";then\n\techo 2\n" ) ;
+ strcat( lDialogString , "else\n\techo 0\nfi" ) ;
+ }
+ else
+ {
+ strcat(lDialogString , "echo -n \"press enter to continue \"; ");
+ strcat( lDialogString , "stty sane -echo;" ) ;
+ strcat( lDialogString ,
+ "answer=$( while ! head -c 1;do true ;done);echo 1");
+ }
+ strcat( lDialogString ,
+ " >/tmp/tinyfd.txt';cat /tmp/tinyfd.txt;rm /tmp/tinyfd.txt");
+ }
+ else if ( !isTerminalRunning() && pythonDbusPresent() && !strcmp("ok" , aDialogType) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python-dbus");return 1;}
+ strcpy( lDialogString , gPythonName ) ;
+ strcat( lDialogString ," -c \"import dbus;bus=dbus.SessionBus();");
+ strcat( lDialogString ,"notif=bus.get_object('org.freedesktop.Notifications','/org/freedesktop/Notifications');" ) ;
+ strcat( lDialogString ,"notify=dbus.Interface(notif,'org.freedesktop.Notifications');" ) ;
+ strcat( lDialogString ,"notify.Notify('',0,'" ) ;
+ if ( aIconType && strlen(aIconType) )
+ {
+ strcat( lDialogString , aIconType ) ;
+ }
+ strcat(lDialogString, "','") ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, aTitle) ;
+ }
+ strcat(lDialogString, "','") ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ lpDialogString = lDialogString + strlen(lDialogString);
+ tfd_replaceSubStr( aMessage , "\n" , "\\n" , lpDialogString ) ;
+ }
+ strcat(lDialogString, "','','',5000)\"") ;
+ }
+ else if ( !isTerminalRunning() && (perlPresent() >= 2) && !strcmp("ok" , aDialogType) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"perl-dbus");return 1;}
+
+ strcpy( lDialogString , "perl -e \"use Net::DBus;\
+my \\$sessionBus = Net::DBus->session;\
+my \\$notificationsService = \\$sessionBus->get_service('org.freedesktop.Notifications');\
+my \\$notificationsObject = \\$notificationsService->get_object('/org/freedesktop/Notifications',\
+'org.freedesktop.Notifications');");
+
+ sprintf( lDialogString + strlen(lDialogString),
+"my \\$notificationId;\\$notificationId = \\$notificationsObject->Notify(shift, 0, '%s', '%s', '%s', [], {}, -1);\" ",
+ aIconType?aIconType:"", aTitle?aTitle:"", aMessage?aMessage:"" ) ;
+ }
+ else if ( !isTerminalRunning() && notifysendPresent() && !strcmp("ok" , aDialogType) )
+ {
+
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notifysend");return 1;}
+ strcpy( lDialogString , "notify-send" ) ;
+ if ( aIconType && strlen(aIconType) )
+ {
+ strcat( lDialogString , " -i '" ) ;
+ strcat( lDialogString , aIconType ) ;
+ strcat( lDialogString , "'" ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, aTitle) ;
+ strcat( lDialogString , " | " ) ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ tfd_replaceSubStr( aMessage , "\n\t" , " | " , lBuff ) ;
+ tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+ tfd_replaceSubStr( lBuff2 , "\t" , " " , lBuff ) ;
+ strcat(lDialogString, lBuff) ;
+ }
+ strcat( lDialogString , "\"" ) ;
+ }
+ else if ( !isTerminalRunning() && notifyPresent() && !strcmp("ok" , aDialogType) )
+ {
+
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notify");return 1;}
+ strcpy( lDialogString , "notify" ) ;
+ if ( aIconType && strlen(aIconType) )
+ {
+ strcat( lDialogString , " --icon '" ) ;
+ strcat( lDialogString , aIconType ) ;
+ strcat( lDialogString , "'" ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat( lDialogString , " --title '" ) ;
+ strcat( lDialogString , aTitle ) ;
+ strcat( lDialogString , "'" ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ tfd_replaceSubStr( aMessage , "\n\t" , " | " , lBuff ) ;
+ tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+ tfd_replaceSubStr( lBuff2 , "\t" , " " , lBuff ) ;
+ strcat(lDialogString, lBuff) ;
+ }
+ else strcat(lDialogString, "''");
+ strcat( lDialogString , "\"" ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"basicinput");return 0;}
+ if ( !gWarningDisplayed && !tinyfd_forceConsole)
+ {
+ gWarningDisplayed = 1 ;
+ printf("\n\n%s\n", gTitle);
+ printf("%s\n\n", tinyfd_needs);
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ printf("\n%s\n", aTitle);
+ }
+
+ tcgetattr(0, &infoOri);
+ tcgetattr(0, &info);
+ info.c_lflag &= ~ICANON;
+ info.c_cc[VMIN] = 1;
+ info.c_cc[VTIME] = 0;
+ tcsetattr(0, TCSANOW, &info);
+ if ( aDialogType && !strcmp("yesno",aDialogType) )
+ {
+ do
+ {
+ if ( aMessage && strlen(aMessage) )
+ {
+ printf("\n%s\n",aMessage);
+ }
+ printf("y/n: "); fflush(stdout);
+ lChar = (char) tolower( getchar() ) ;
+ printf("\n\n");
+ }
+ while ( lChar != 'y' && lChar != 'n' );
+ lResult = lChar == 'y' ? 1 : 0 ;
+ }
+ else if ( aDialogType && !strcmp("okcancel",aDialogType) )
+ {
+ do
+ {
+ if ( aMessage && strlen(aMessage) )
+ {
+ printf("\n%s\n",aMessage);
+ }
+ printf("[O]kay/[C]ancel: "); fflush(stdout);
+ lChar = (char) tolower( getchar() ) ;
+ printf("\n\n");
+ }
+ while ( lChar != 'o' && lChar != 'c' );
+ lResult = lChar == 'o' ? 1 : 0 ;
+ }
+ else if ( aDialogType && !strcmp("yesnocancel",aDialogType) )
+ {
+ do
+ {
+ if ( aMessage && strlen(aMessage) )
+ {
+ printf("\n%s\n",aMessage);
+ }
+ printf("[Y]es/[N]o/[C]ancel: "); fflush(stdout);
+ lChar = (char) tolower( getchar() ) ;
+ printf("\n\n");
+ }
+ while ( lChar != 'y' && lChar != 'n' && lChar != 'c' );
+ lResult = (lChar == 'y') ? 1 : (lChar == 'n') ? 2 : 0 ;
+ }
+ else
+ {
+ if ( aMessage && strlen(aMessage) )
+ {
+ printf("\n%s\n\n",aMessage);
+ }
+ printf("press enter to continue "); fflush(stdout);
+ (void) getchar() ;
+ printf("\n\n");
+ lResult = 1 ;
+ }
+ tcsetattr(0, TCSANOW, &infoOri);
+ free(lDialogString);
+ return lResult ;
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+
+ lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+ if ( ! lIn )
+ {
+ free(lDialogString);
+ return 0 ;
+ }
+ while ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {}
+
+ pclose( lIn ) ;
+
+ /* printf( "lBuff: %s len: %lu \n" , lBuff , strlen(lBuff) ) ; */
+ if ( strlen( lBuff ) && lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ /* printf( "lBuff1: %s len: %lu \n" , lBuff , strlen(lBuff) ) ; */
+
+ if (aDialogType && !strcmp("yesnocancel", aDialogType))
+ {
+ if ( lBuff[0]=='1' )
+ {
+ if ( !strcmp( lBuff+1 , "Yes" )) strcpy(lBuff,"1");
+ else if ( !strcmp( lBuff+1 , "No" )) strcpy(lBuff,"2");
+ }
+ }
+ /* printf( "lBuff2: %s len: %lu \n" , lBuff , strlen(lBuff) ) ; */
+
+ lResult = !strcmp( lBuff , "2" ) ? 2 : !strcmp( lBuff , "1" ) ? 1 : 0;
+
+ /* printf( "lResult: %d\n" , lResult ) ; */
+ free(lDialogString);
+ return lResult ;
+}
+
+
+/* return has only meaning for tinyfd_query */
+int tinyfd_notifyPopup(
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" may contain \n and \t */
+ char const * aIconType ) /* "info" "warning" "error" */
+{
+ char lBuff[MAX_PATH_OR_CMD];
+ char lBuff2[MAX_PATH_OR_CMD];
+ char * lDialogString = NULL ;
+ char * lpDialogString ;
+ FILE * lIn ;
+ size_t lTitleLen ;
+ size_t lMessageLen ;
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_notifyPopup("INVALID TITLE WITH QUOTES", aMessage, aIconType);
+ if (tfd_quoteDetected(aMessage)) return tinyfd_notifyPopup(aTitle, "INVALID MESSAGE WITH QUOTES", aIconType);
+
+ if ( getenv("SSH_TTY") && !dunstifyPresent() && !dunstPresent() )
+ {
+ return tinyfd_messageBox(aTitle, aMessage, "ok", aIconType, 0);
+ }
+
+ lTitleLen = aTitle ? strlen(aTitle) : 0 ;
+ lMessageLen = aMessage ? strlen(aMessage) : 0 ;
+ if ( !aTitle || strcmp(aTitle,"tinyfd_query") )
+ {
+ lDialogString = (char *) malloc( MAX_PATH_OR_CMD + lTitleLen + lMessageLen );
+ }
+
+ if ( getenv("SSH_TTY") )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dunst");return 1;}
+ strcpy( lDialogString , "notify-send \"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat( lDialogString , aTitle ) ;
+ strcat( lDialogString , "\" \"" ) ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage) ;
+ }
+ strcat( lDialogString , "\"" ) ;
+ }
+ else if ( osascriptPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return 1;}
+
+ strcpy( lDialogString , "osascript ");
+ if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+ strcat( lDialogString , " -e 'try' -e 'display notification \"") ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage) ;
+ }
+ strcat(lDialogString, " \" ") ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "with title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat( lDialogString, "' -e 'end try'") ;
+ if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
+ }
+ else if ( tfd_kdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return 1;}
+ strcpy( lDialogString , "kdialog" ) ;
+
+ if ( aIconType && strlen(aIconType) )
+ {
+ strcat( lDialogString , " --icon '" ) ;
+ strcat( lDialogString , aIconType ) ;
+ strcat( lDialogString , "'" ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat( lDialogString , " --title \"" ) ;
+ strcat( lDialogString , aTitle ) ;
+ strcat( lDialogString , "\"" ) ;
+ }
+
+ strcat( lDialogString , " --passivepopup" ) ;
+ strcat( lDialogString , " \"" ) ;
+ if ( aMessage )
+ {
+ strcat( lDialogString , aMessage ) ;
+ }
+ strcat( lDialogString , " \" 5" ) ;
+ }
+ else if ( tfd_yadPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"yad");return 1;}
+ strcpy( lDialogString , "yad --notification");
+
+ if ( aIconType && strlen( aIconType ) )
+ {
+ strcat( lDialogString , " --image=\"");
+ strcat( lDialogString , aIconType ) ;
+ strcat( lDialogString , "\"" ) ;
+ }
+
+ strcat( lDialogString , " --text=\"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\n") ;
+ }
+ if ( aMessage && strlen( aMessage ) )
+ {
+ strcat( lDialogString , aMessage ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ }
+ else if ( perlPresent() >= 2 )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"perl-dbus");return 1;}
+
+ strcpy( lDialogString , "perl -e \"use Net::DBus;\
+my \\$sessionBus = Net::DBus->session;\
+my \\$notificationsService = \\$sessionBus->get_service('org.freedesktop.Notifications');\
+my \\$notificationsObject = \\$notificationsService->get_object('/org/freedesktop/Notifications',\
+'org.freedesktop.Notifications');");
+
+ sprintf( lDialogString + strlen(lDialogString) ,
+"my \\$notificationId;\\$notificationId = \\$notificationsObject->Notify(shift, 0, '%s', '%s', '%s', [], {}, -1);\" ",
+aIconType?aIconType:"", aTitle?aTitle:"", aMessage?aMessage:"" ) ;
+ }
+ else if ( pythonDbusPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python-dbus");return 1;}
+ strcpy( lDialogString , gPythonName ) ;
+ strcat( lDialogString ," -c \"import dbus;bus=dbus.SessionBus();");
+ strcat( lDialogString ,"notif=bus.get_object('org.freedesktop.Notifications','/org/freedesktop/Notifications');" ) ;
+ strcat( lDialogString ,"notify=dbus.Interface(notif,'org.freedesktop.Notifications');" ) ;
+ strcat( lDialogString ,"notify.Notify('',0,'" ) ;
+ if ( aIconType && strlen(aIconType) )
+ {
+ strcat( lDialogString , aIconType ) ;
+ }
+ strcat(lDialogString, "','") ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, aTitle) ;
+ }
+ strcat(lDialogString, "','") ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ lpDialogString = lDialogString + strlen(lDialogString);
+ tfd_replaceSubStr( aMessage , "\n" , "\\n" , lpDialogString ) ;
+ }
+ strcat(lDialogString, "','','',5000)\"") ;
+ }
+ else if ( notifysendPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notifysend");return 1;}
+ strcpy( lDialogString , "notify-send" ) ;
+ if ( aIconType && strlen(aIconType) )
+ {
+ strcat( lDialogString , " -i '" ) ;
+ strcat( lDialogString , aIconType ) ;
+ strcat( lDialogString , "'" ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, aTitle) ;
+ strcat( lDialogString , " | " ) ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ tfd_replaceSubStr( aMessage , "\n\t" , " | " , lBuff ) ;
+ tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+ tfd_replaceSubStr( lBuff2 , "\t" , " " , lBuff ) ;
+ strcat(lDialogString, lBuff) ;
+ }
+ strcat( lDialogString , "\"" ) ;
+ }
+ else if ( notifyPresent() ) /* haiku */
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notify");return 1;}
+ strcpy( lDialogString , "notify" ) ;
+ if ( aIconType && strlen(aIconType) )
+ {
+ strcat( lDialogString , " --icon '" ) ;
+ strcat( lDialogString , aIconType ) ;
+ strcat( lDialogString , "'" ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat( lDialogString , " --title '" ) ;
+ strcat(lDialogString, aTitle) ;
+ strcat( lDialogString , "'" ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ tfd_replaceSubStr( aMessage , "\n\t" , " | " , lBuff ) ;
+ tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+ tfd_replaceSubStr( lBuff2 , "\t" , " " , lBuff ) ;
+ strcat(lDialogString, lBuff) ;
+ }
+ else strcat(lDialogString, "''" ) ;
+ strcat( lDialogString , "\"" ) ;
+ }
+ else if ( tfd_zenityVersion()>=3018 )
+ {
+ /* zenity 2.32 & 3.14 has the notification but with a bug: it doesnt return from it */
+ /* zenity 3.8 show the notification as an alert ok cancel box */
+ /* zenity 3.44 doesn't have the notification (3.42 has it) */
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return 1;}
+ strcpy( lDialogString , "zenity --notification");
+
+ if ( aIconType && strlen( aIconType ) )
+ {
+ strcat( lDialogString , " --window-icon '");
+ strcat( lDialogString , aIconType ) ;
+ strcat( lDialogString , "'" ) ;
+ }
+
+ strcat( lDialogString , " --text \"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\n") ;
+ }
+ if ( aMessage && strlen( aMessage ) )
+ {
+ strcat( lDialogString , aMessage ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ }
+ else
+ {
+ if (lDialogString) free(lDialogString);
+ return tinyfd_messageBox(aTitle, aMessage, "ok", aIconType, 0);
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+
+ lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+ if ( ! lIn )
+ {
+ free(lDialogString);
+ return 0 ;
+ }
+
+ pclose( lIn ) ;
+ free(lDialogString);
+ return 1;
+}
+
+
+/* returns NULL on cancel */
+char * tinyfd_inputBox(
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" (\n and \t have no effect) */
+ char const * aDefaultInput ) /* "" , if NULL it's a passwordBox */
+{
+ static char lBuff[MAX_PATH_OR_CMD];
+ char * lDialogString = NULL;
+ char * lpDialogString;
+ FILE * lIn ;
+ int lResult ;
+ int lWasGdialog = 0 ;
+ int lWasGraphicDialog = 0 ;
+ int lWasXterm = 0 ;
+ int lWasBasicXterm = 0 ;
+ struct termios oldt ;
+ struct termios newt ;
+ char * lEOF;
+ size_t lTitleLen ;
+ size_t lMessageLen ;
+
+ if (!aTitle && !aMessage && !aDefaultInput) return lBuff; /* now I can fill lBuff from outside */
+
+ lBuff[0]='\0';
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_inputBox("INVALID TITLE WITH QUOTES", aMessage, aDefaultInput);
+ if (tfd_quoteDetected(aMessage)) return tinyfd_inputBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDefaultInput);
+ if (tfd_quoteDetected(aDefaultInput)) return tinyfd_inputBox(aTitle, aMessage, "INVALID DEFAULT_INPUT WITH QUOTES: use the GRAVE ACCENT \\x60 instead.");
+
+ lTitleLen = aTitle ? strlen(aTitle) : 0 ;
+ lMessageLen = aMessage ? strlen(aMessage) : 0 ;
+ if ( !aTitle || strcmp(aTitle,"tinyfd_query") )
+ {
+ lDialogString = (char *) malloc( MAX_PATH_OR_CMD + lTitleLen + lMessageLen );
+ }
+
+ if ( osascriptPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return (char *)1;}
+ strcpy( lDialogString , "osascript ");
+ if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+ strcat( lDialogString , " -e 'try' -e 'display dialog \"") ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage) ;
+ }
+ strcat(lDialogString, "\" ") ;
+ strcat(lDialogString, "default answer \"") ;
+ if ( aDefaultInput && strlen(aDefaultInput) )
+ {
+ strcat(lDialogString, aDefaultInput) ;
+ }
+ strcat(lDialogString, "\" ") ;
+ if ( ! aDefaultInput )
+ {
+ strcat(lDialogString, "hidden answer true ") ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "with title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+ strcat(lDialogString, "with icon note' ") ;
+ strcat(lDialogString, "-e '\"1\" & text returned of result' " );
+ strcat(lDialogString, "-e 'on error number -128' " ) ;
+ strcat(lDialogString, "-e '0' " );
+ strcat(lDialogString, "-e 'end try'") ;
+ if ( ! osx9orBetter() ) strcat(lDialogString, " -e 'end tell'") ;
+ }
+ else if ( tfd_kdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(kdialog" ) ;
+
+ if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+
+ if ( ! aDefaultInput )
+ {
+ strcat(lDialogString, " --password ") ;
+ }
+ else
+ {
+ strcat(lDialogString, " --inputbox ") ;
+
+ }
+ strcat(lDialogString, "\"") ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage ) ;
+ }
+ strcat(lDialogString , "\" \"" ) ;
+ if ( aDefaultInput && strlen(aDefaultInput) )
+ {
+ strcat(lDialogString, aDefaultInput ) ;
+ }
+ strcat(lDialogString , "\"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, " --title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+ }
+ strcat( lDialogString ,
+ ");if [ $? = 0 ];then echo 1$szAnswer;else echo 0$szAnswer;fi");
+ }
+ else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+ || tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(zenity" ) ;
+ if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_matedialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(matedialog" ) ;
+ }
+ else if ( tfd_shellementaryPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(shellementary" ) ;
+ }
+ else if ( tfd_qarmaPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(qarma" ) ;
+ if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_boxerPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(boxer" ) ;
+ }
+ else if ( tfd_shantyPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(shanty" ) ;
+ }
+ else {}
+
+ strcat( lDialogString ," --entry" ) ;
+
+ strcat(lDialogString, " --title=\"") ;
+ if (aTitle && strlen(aTitle)) strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+
+ strcat(lDialogString, " --text=\"") ;
+ if (aMessage && strlen(aMessage)) strcat(lDialogString, aMessage) ;
+ strcat(lDialogString, "\"") ;
+
+ if ( aDefaultInput )
+ {
+ strcat(lDialogString, " --entry-text=\"") ;
+ strcat(lDialogString, aDefaultInput) ;
+ strcat(lDialogString, "\"") ;
+ }
+ else
+ {
+ strcat(lDialogString, " --hide-text") ;
+ }
+ if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
+ strcat( lDialogString ,
+ ");if [ $? = 0 ];then echo 1$szAnswer;else echo 0$szAnswer;fi");
+ }
+ else if (tfd_yadPresent())
+ {
+ if (aTitle && !strcmp(aTitle, "tinyfd_query")) { strcpy(tinyfd_response, "yad"); return (char*)1; }
+ strcpy(lDialogString, "szAnswer=$(yad --entry");
+ if (aTitle && strlen(aTitle))
+ {
+ strcat(lDialogString, " --title=\"");
+ strcat(lDialogString, aTitle);
+ strcat(lDialogString, "\"");
+ }
+ if (aMessage && strlen(aMessage))
+ {
+ strcat(lDialogString, " --text=\"");
+ strcat(lDialogString, aMessage);
+ strcat(lDialogString, "\"");
+ }
+ if (aDefaultInput && strlen(aDefaultInput))
+ {
+ strcat(lDialogString, " --entry-text=\"");
+ strcat(lDialogString, aDefaultInput);
+ strcat(lDialogString, "\"");
+ }
+ else
+ {
+ strcat(lDialogString, " --hide-text");
+ }
+ if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
+ strcat(lDialogString,
+ ");if [ $? = 0 ];then echo 1$szAnswer;else echo 0$szAnswer;fi");
+ }
+ else if ( gxmessagePresent() || gmessagePresent() )
+ {
+ if ( gxmessagePresent() ) {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"gxmessage");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(gxmessage -buttons Ok:1,Cancel:0 -center \"");
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"gmessage");return (char *)1;}
+ strcpy( lDialogString , "szAnswer=$(gmessage -buttons Ok:1,Cancel:0 -center \"");
+ }
+
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat( lDialogString , aMessage ) ;
+ }
+ strcat(lDialogString, "\"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat( lDialogString , " -title \"");
+ strcat( lDialogString , aTitle ) ;
+ strcat(lDialogString, "\" " ) ;
+ }
+ strcat(lDialogString, " -entrytext \"" ) ;
+ if ( aDefaultInput && strlen(aDefaultInput) )
+ {
+ strcat( lDialogString , aDefaultInput ) ;
+ }
+ strcat(lDialogString, "\"" ) ;
+ strcat( lDialogString , ");echo $?$szAnswer");
+ }
+ else if ( !gdialogPresent() && !xdialogPresent() && tkinter3Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python3-tkinter");return (char *)1;}
+ strcpy( lDialogString , gPython3Name ) ;
+ strcat( lDialogString ,
+ " -S -c \"import tkinter; from tkinter import simpledialog;root=tkinter.Tk();root.withdraw();");
+ strcat( lDialogString ,"res=simpledialog.askstring(" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+
+ strcat(lDialogString, "prompt='") ;
+ lpDialogString = lDialogString + strlen(lDialogString);
+ tfd_replaceSubStr( aMessage , "\n" , "\\n" , lpDialogString ) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultInput )
+ {
+ if ( strlen(aDefaultInput) )
+ {
+ strcat(lDialogString, "initialvalue='") ;
+ strcat(lDialogString, aDefaultInput) ;
+ strcat(lDialogString, "',") ;
+ }
+ }
+ else
+ {
+ strcat(lDialogString, "show='*'") ;
+ }
+ strcat(lDialogString, ");\nif res is None :\n\tprint(0)");
+ strcat(lDialogString, "\nelse :\n\tprint('1'+res)\n\"" ) ;
+ }
+ else if ( !gdialogPresent() && !xdialogPresent() && tkinter2Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python2-tkinter");return (char *)1;}
+ strcpy( lDialogString , "export PYTHONIOENCODING=utf-8;" ) ;
+ strcat( lDialogString , gPython2Name ) ;
+ if ( ! isTerminalRunning( ) && tfd_isDarwin( ) )
+ {
+ strcat( lDialogString , " -i" ) ; /* for osx without console */
+ }
+
+ strcat( lDialogString ,
+ " -S -c \"import Tkinter,tkSimpleDialog;root=Tkinter.Tk();root.withdraw();");
+
+ if ( tfd_isDarwin( ) )
+ {
+ strcat( lDialogString ,
+"import os;os.system('''/usr/bin/osascript -e 'tell app \\\"Finder\\\" to set \
+frontmost of process \\\"Python\\\" to true' ''');");
+ }
+
+ strcat( lDialogString ,"res=tkSimpleDialog.askstring(" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+
+ strcat(lDialogString, "prompt='") ;
+ lpDialogString = lDialogString + strlen(lDialogString);
+ tfd_replaceSubStr( aMessage , "\n" , "\\n" , lpDialogString ) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultInput )
+ {
+ if ( strlen(aDefaultInput) )
+ {
+ strcat(lDialogString, "initialvalue='") ;
+ strcat(lDialogString, aDefaultInput) ;
+ strcat(lDialogString, "',") ;
+ }
+ }
+ else
+ {
+ strcat(lDialogString, "show='*'") ;
+ }
+ strcat(lDialogString, ");\nif res is None :\n\tprint 0");
+ strcat(lDialogString, "\nelse :\n\tprint '1'+res\n\"" ) ;
+ }
+ else if ( gdialogPresent() || xdialogPresent() || dialogName() || whiptailPresent() )
+ {
+ if ( gdialogPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"gdialog");return (char *)1;}
+ lWasGraphicDialog = 1 ;
+ lWasGdialog = 1 ;
+ strcpy( lDialogString , "(gdialog " ) ;
+ }
+ else if ( xdialogPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"xdialog");return (char *)1;}
+ lWasGraphicDialog = 1 ;
+ strcpy( lDialogString , "(Xdialog " ) ;
+ }
+ else if ( dialogName( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ if ( isTerminalRunning( ) )
+ {
+ strcpy( lDialogString , "(dialog " ) ;
+ }
+ else
+ {
+ lWasXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'(" ) ;
+ strcat( lDialogString , dialogName() ) ;
+ strcat( lDialogString , " " ) ;
+ }
+ }
+ else if ( isTerminalRunning( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"whiptail");return (char *)0;}
+ strcpy( lDialogString , "(whiptail " ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"whiptail");return (char *)0;}
+ lWasXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'(whiptail " ) ;
+ }
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( !xdialogPresent() && !gdialogPresent() )
+ {
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString, "tab: move focus") ;
+ if ( ! aDefaultInput && !lWasGdialog )
+ {
+ strcat(lDialogString, " (sometimes nothing, no blink nor star, is shown in text field)") ;
+ }
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( aDefaultInput || lWasGdialog )
+ {
+ strcat( lDialogString , "--inputbox" ) ;
+ }
+ else
+ {
+ if ( !lWasGraphicDialog && dialogName() && isDialogVersionBetter09b() )
+ {
+ strcat( lDialogString , "--insecure " ) ;
+ }
+ strcat( lDialogString , "--passwordbox" ) ;
+ }
+ strcat( lDialogString , " \"" ) ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat(lDialogString, aMessage) ;
+ }
+ strcat(lDialogString,"\" 10 60 ") ;
+ if ( aDefaultInput && strlen(aDefaultInput) )
+ {
+ strcat(lDialogString, "\"") ;
+ strcat(lDialogString, aDefaultInput) ;
+ strcat(lDialogString, "\" ") ;
+ }
+ if ( lWasGraphicDialog )
+ {
+ strcat(lDialogString,") 2>/tmp/tinyfd.txt;\
+ if [ $? = 0 ];then tinyfdBool=1;else tinyfdBool=0;fi;\
+ tinyfdRes=$(cat /tmp/tinyfd.txt);echo $tinyfdBool$tinyfdRes") ;
+ }
+ else
+ {
+ strcat(lDialogString,">/dev/tty ) 2>/tmp/tinyfd.txt;\
+ if [ $? = 0 ];then tinyfdBool=1;else tinyfdBool=0;fi;\
+ tinyfdRes=$(cat /tmp/tinyfd.txt);echo $tinyfdBool$tinyfdRes") ;
+
+ if ( lWasXterm )
+ {
+ strcat(lDialogString," >/tmp/tinyfd0.txt';cat /tmp/tinyfd0.txt");
+ }
+ else
+ {
+ strcat(lDialogString, "; clear >/dev/tty") ;
+ }
+ }
+ }
+ else if ( ! isTerminalRunning( ) && terminalName() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"basicinput");return (char *)0;}
+ lWasBasicXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'" ) ;
+ if ( !gWarningDisplayed && !tinyfd_forceConsole)
+ {
+ gWarningDisplayed = 1 ;
+ tinyfd_messageBox(gTitle,tinyfd_needs,"ok","warning",0);
+ }
+ if ( aTitle && strlen(aTitle) && !tinyfd_forceConsole)
+ {
+ strcat( lDialogString , "echo \"" ) ;
+ strcat( lDialogString, aTitle) ;
+ strcat( lDialogString , "\";echo;" ) ;
+ }
+
+ strcat( lDialogString , "echo \"" ) ;
+ if ( aMessage && strlen(aMessage) )
+ {
+ strcat( lDialogString, aMessage) ;
+ }
+ strcat( lDialogString , "\";read " ) ;
+ if ( ! aDefaultInput )
+ {
+ strcat( lDialogString , "-s " ) ;
+ }
+ strcat( lDialogString , "-p \"" ) ;
+ strcat( lDialogString , "(esc+enter to cancel): \" ANSWER " ) ;
+ strcat( lDialogString , ";echo 1$ANSWER >/tmp/tinyfd.txt';" ) ;
+ strcat( lDialogString , "cat -v /tmp/tinyfd.txt");
+ }
+ else if ( !gWarningDisplayed && ! isTerminalRunning( ) && ! terminalName() ) {
+ gWarningDisplayed = 1 ;
+ tinyfd_messageBox(gTitle,tinyfd_needs,"ok","warning",0);
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"no_solution");return (char *)0;}
+ free(lDialogString);
+ return NULL;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"basicinput");return (char *)0;}
+ if ( !gWarningDisplayed && !tinyfd_forceConsole)
+ {
+ gWarningDisplayed = 1 ;
+ tinyfd_messageBox(gTitle,tinyfd_needs,"ok","warning",0);
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ printf("\n%s\n", aTitle);
+ }
+ if ( aMessage && strlen(aMessage) )
+ {
+ printf("\n%s\n",aMessage);
+ }
+ printf("(esc+enter to cancel): "); fflush(stdout);
+ if ( ! aDefaultInput )
+ {
+ tcgetattr(STDIN_FILENO, & oldt) ;
+ newt = oldt ;
+ newt.c_lflag &= ~ECHO ;
+ tcsetattr(STDIN_FILENO, TCSANOW, & newt);
+ }
+
+ lEOF = fgets(lBuff, MAX_PATH_OR_CMD, stdin);
+ /* printf("lbuff<%c><%d>\n",lBuff[0],lBuff[0]); */
+ if ( ! lEOF || (lBuff[0] == '\0') )
+ {
+ free(lDialogString);
+ return NULL;
+ }
+
+ if ( lBuff[0] == '\n' )
+ {
+ lEOF = fgets(lBuff, MAX_PATH_OR_CMD, stdin);
+ /* printf("lbuff<%c><%d>\n",lBuff[0],lBuff[0]); */
+ if ( ! lEOF || (lBuff[0] == '\0') )
+ {
+ free(lDialogString);
+ return NULL;
+ }
+ }
+
+ if ( ! aDefaultInput )
+ {
+ tcsetattr(STDIN_FILENO, TCSANOW, & oldt);
+ printf("\n");
+ }
+ printf("\n");
+ if ( strchr(lBuff,27) )
+ {
+ free(lDialogString);
+ return NULL ;
+ }
+ if ( lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ free(lDialogString);
+ return lBuff ;
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+ lIn = ( FILE * ) popen( lDialogString , "r" );
+ if ( ! lIn )
+ {
+ if ( fileExists("/tmp/tinyfd.txt") )
+ {
+ wipefile("/tmp/tinyfd.txt");
+ remove("/tmp/tinyfd.txt");
+ }
+ if ( fileExists("/tmp/tinyfd0.txt") )
+ {
+ wipefile("/tmp/tinyfd0.txt");
+ remove("/tmp/tinyfd0.txt");
+ }
+ free(lDialogString);
+ return NULL ;
+ }
+ while ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {}
+
+ pclose( lIn ) ;
+
+ if ( fileExists("/tmp/tinyfd.txt") )
+ {
+ wipefile("/tmp/tinyfd.txt");
+ remove("/tmp/tinyfd.txt");
+ }
+ if ( fileExists("/tmp/tinyfd0.txt") )
+ {
+ wipefile("/tmp/tinyfd0.txt");
+ remove("/tmp/tinyfd0.txt");
+ }
+
+ /* printf( "len Buff: %lu\n" , strlen(lBuff) ) ; */
+ /* printf( "lBuff0: %s\n" , lBuff ) ; */
+ if ( strlen( lBuff ) && lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ /* printf( "lBuff1: %s len: %lu \n" , lBuff , strlen(lBuff) ) ; */
+ if ( lWasBasicXterm )
+ {
+ if ( strstr(lBuff,"^[") ) /* esc was pressed */
+ {
+ free(lDialogString);
+ return NULL ;
+ }
+ }
+
+ lResult = strncmp( lBuff , "1" , 1) ? 0 : 1 ;
+ /* printf( "lResult: %d \n" , lResult ) ; */
+ if ( ! lResult )
+ {
+ free(lDialogString);
+ return NULL ;
+ }
+
+ /* printf( "lBuff+1: %s\n" , lBuff+1 ) ; */
+ free(lDialogString);
+ return lBuff+1 ;
+}
+
+
+char * tinyfd_saveFileDialog(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPathAndOrFile , /* NULL or "" , ends with / to set only a directory */
+ int aNumOfFilterPatterns , /* 0 */
+ char const * const * aFilterPatterns , /* NULL or {"*.txt","*.doc"} */
+ char const * aSingleFilterDescription ) /* NULL or "text files" */
+{
+ static char lBuff[MAX_PATH_OR_CMD] ;
+ static char lLastDirectory[MAX_PATH_OR_CMD] = "$PWD" ;
+
+ char lDialogString[MAX_PATH_OR_CMD] ;
+ char lString[MAX_PATH_OR_CMD] ;
+ int i ;
+ int lWasGraphicDialog = 0 ;
+ int lWasXterm = 0 ;
+ char * p ;
+ char * lPointerInputBox ;
+ FILE * lIn ;
+ lBuff[0]='\0';
+
+ if ( ! aFilterPatterns ) aNumOfFilterPatterns = 0 ;
+ if (tfd_quoteDetected(aTitle)) return tinyfd_saveFileDialog("INVALID TITLE WITH QUOTES", aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription);
+ if (tfd_quoteDetected(aDefaultPathAndOrFile)) return tinyfd_saveFileDialog(aTitle, "INVALID DEFAULT_PATH WITH QUOTES", aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription);
+ if (tfd_quoteDetected(aSingleFilterDescription)) return tinyfd_saveFileDialog(aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, "INVALID FILTER_DESCRIPTION WITH QUOTES");
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (tfd_quoteDetected(aFilterPatterns[i])) return tinyfd_saveFileDialog("INVALID FILTER_PATTERN WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultPathAndOrFile, 0, NULL, NULL);
+ }
+
+ if ( osascriptPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return (char *)1;}
+ strcpy( lDialogString , "osascript ");
+ if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"Finder\"' -e 'Activate'");
+ strcat( lDialogString , " -e 'try' -e 'POSIX path of ( choose file name " );
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "with prompt \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+ getPathWithoutFinalSlash( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "default location \"") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "\" " ) ;
+ }
+ getLastName( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "default name \"") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "\" " ) ;
+ }
+ strcat( lDialogString , ")' " ) ;
+ strcat(lDialogString, "-e 'on error number -128' " ) ;
+ strcat(lDialogString, "-e 'end try'") ;
+ if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
+ }
+ else if ( tfd_kdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return (char *)1;}
+
+ strcpy( lDialogString , "kdialog" ) ;
+ if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ strcat( lDialogString , " --getsavefilename " ) ;
+
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ strcat(lDialogString, "\"") ;
+ if ( aDefaultPathAndOrFile[0] != '/' )
+ {
+ strcat(lDialogString, lLastDirectory) ;
+ strcat(lDialogString , "/" ) ;
+ }
+ strcat(lDialogString, aDefaultPathAndOrFile ) ;
+ strcat(lDialogString , "\"" ) ;
+ }
+ else
+ {
+ strcat(lDialogString, "\"") ;
+ strcat(lDialogString, lLastDirectory) ;
+ strcat(lDialogString , "/" ) ;
+ strcat(lDialogString, "\"") ;
+ }
+
+ if ( aNumOfFilterPatterns > 0 )
+ {
+ strcat(lDialogString , " \"" ) ;
+ strcat( lDialogString , aFilterPatterns[0] ) ;
+ for ( i = 1 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , " " ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ }
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , " | " ) ;
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ }
+ strcat( lDialogString , "\"" ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, " --title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+ }
+ }
+ else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+ || tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
+ strcpy( lDialogString , "zenity" ) ;
+ if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_matedialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
+ strcpy( lDialogString , "matedialog" ) ;
+ }
+ else if ( tfd_shellementaryPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
+ strcpy( lDialogString , "shellementary" ) ;
+ }
+ else if ( tfd_qarmaPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
+ strcpy( lDialogString , "qarma" ) ;
+ if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_boxerPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+ strcpy( lDialogString , "boxer" ) ;
+ }
+ else if ( tfd_shantyPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+ strcpy( lDialogString , "shanty" ) ;
+ }
+ else {}
+
+ strcat(lDialogString, " --file-selection --save --confirm-overwrite" ) ;
+
+ strcat(lDialogString, " --title=\"") ;
+ if (aTitle && strlen(aTitle)) strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ strcat(lDialogString, " --filename=\"") ;
+ strcat(lDialogString, aDefaultPathAndOrFile) ;
+ strcat(lDialogString, "\"") ;
+ }
+ if ( aNumOfFilterPatterns > 0 )
+ {
+ strcat( lDialogString , " --file-filter='" ) ;
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ strcat( lDialogString , " | " ) ;
+ }
+ strcat( lDialogString , aFilterPatterns[0] ) ;
+ for ( i = 1 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , " " ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ }
+ strcat( lDialogString , "' --file-filter='All files | *'" ) ;
+ }
+ if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
+ }
+ else if (tfd_yadPresent())
+ {
+ if (aTitle && !strcmp(aTitle, "tinyfd_query")) { strcpy(tinyfd_response, "yad"); return (char*)1; }
+ strcpy(lDialogString, "yad --file --save --confirm-overwrite");
+ if (aTitle && strlen(aTitle))
+ {
+ strcat(lDialogString, " --title=\"");
+ strcat(lDialogString, aTitle);
+ strcat(lDialogString, "\"");
+ }
+ if (aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile))
+ {
+ strcat(lDialogString, " --filename=\"");
+ strcat(lDialogString, aDefaultPathAndOrFile);
+ strcat(lDialogString, "\"");
+ }
+ if (aNumOfFilterPatterns > 0)
+ {
+ strcat(lDialogString, " --file-filter='");
+ if (aSingleFilterDescription && strlen(aSingleFilterDescription))
+ {
+ strcat(lDialogString, aSingleFilterDescription);
+ strcat(lDialogString, " | ");
+ }
+ strcat(lDialogString, aFilterPatterns[0]);
+ for ( i = 1 ; i < aNumOfFilterPatterns; i++)
+ {
+ strcat(lDialogString, " ");
+ strcat(lDialogString, aFilterPatterns[i]);
+ }
+ strcat(lDialogString, "' --file-filter='All files | *'");
+ }
+ if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
+ }
+ else if ( !xdialogPresent() && tkinter3Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python3-tkinter");return (char *)1;}
+ strcpy( lDialogString , gPython3Name ) ;
+ strcat( lDialogString ,
+ " -S -c \"import tkinter;from tkinter import filedialog;root=tkinter.Tk();root.withdraw();");
+ strcat( lDialogString , "res=filedialog.asksaveasfilename(");
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ getPathWithoutFinalSlash( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialdir='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ getLastName( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialfile='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ }
+ if ( ( aNumOfFilterPatterns > 1 )
+ || ( (aNumOfFilterPatterns == 1) /* test because poor osx behaviour */
+ && ( aFilterPatterns[0][strlen(aFilterPatterns[0])-1] != '*' ) ) )
+ {
+ strcat(lDialogString , "filetypes=(" ) ;
+ strcat( lDialogString , "('" ) ;
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ }
+ strcat( lDialogString , "',(" ) ;
+ for ( i = 0 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , "'" ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ strcat( lDialogString , "'," ) ;
+ }
+ strcat( lDialogString , "))," ) ;
+ strcat( lDialogString , "('All files','*'))" ) ;
+ }
+ strcat( lDialogString, ");\nif not isinstance(res, tuple):\n\tprint(res)\n\"" ) ;
+ }
+ else if ( !xdialogPresent() && tkinter2Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python2-tkinter");return (char *)1;}
+ strcpy( lDialogString , "export PYTHONIOENCODING=utf-8;" ) ;
+ strcat( lDialogString , gPython2Name ) ;
+ if ( ! isTerminalRunning( ) && tfd_isDarwin( ))
+ {
+ strcat( lDialogString , " -i" ) ; /* for osx without console */
+ }
+ strcat( lDialogString ,
+" -S -c \"import Tkinter,tkFileDialog;root=Tkinter.Tk();root.withdraw();");
+
+ if ( tfd_isDarwin( ) )
+ {
+ strcat( lDialogString ,
+"import os;os.system('''/usr/bin/osascript -e 'tell app \\\"Finder\\\" to set \
+frontmost of process \\\"Python\\\" to true' ''');");
+ }
+
+ strcat( lDialogString , "res=tkFileDialog.asksaveasfilename(");
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ getPathWithoutFinalSlash( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialdir='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ getLastName( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialfile='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ }
+ if ( ( aNumOfFilterPatterns > 1 )
+ || ( (aNumOfFilterPatterns == 1) /* test because poor osx behaviour */
+ && ( aFilterPatterns[0][strlen(aFilterPatterns[0])-1] != '*' ) ) )
+ {
+ strcat(lDialogString , "filetypes=(" ) ;
+ strcat( lDialogString , "('" ) ;
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ }
+ strcat( lDialogString , "',(" ) ;
+ for ( i = 0 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , "'" ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ strcat( lDialogString , "'," ) ;
+ }
+ strcat( lDialogString , "))," ) ;
+ strcat( lDialogString , "('All files','*'))" ) ;
+ }
+ strcat( lDialogString, ");\nif not isinstance(res, tuple):\n\tprint res \n\"" ) ;
+ }
+ else if ( xdialogPresent() || dialogName() )
+ {
+ if ( xdialogPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"xdialog");return (char *)1;}
+ lWasGraphicDialog = 1 ;
+ strcpy( lDialogString , "(Xdialog " ) ;
+ }
+ else if ( isTerminalRunning( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ strcpy( lDialogString , "(dialog " ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ lWasXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'(" ) ;
+ strcat( lDialogString , dialogName() ) ;
+ strcat( lDialogString , " " ) ;
+ }
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( !xdialogPresent() && !gdialogPresent() )
+ {
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString,
+ "tab: focus | /: populate | spacebar: fill text field | ok: TEXT FIELD ONLY") ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat( lDialogString , "--fselect \"" ) ;
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ if ( ! strchr(aDefaultPathAndOrFile, '/') )
+ {
+ strcat(lDialogString, "./") ;
+ }
+ strcat(lDialogString, aDefaultPathAndOrFile) ;
+ }
+ else if ( ! isTerminalRunning( ) && !lWasGraphicDialog )
+ {
+ strcat(lDialogString, getenv("HOME")) ;
+ strcat(lDialogString, "/") ;
+ }
+ else
+ {
+ strcat(lDialogString, "./") ;
+ }
+
+ if ( lWasGraphicDialog )
+ {
+ strcat(lDialogString, "\" 0 60 ) 2>&1 ") ;
+ }
+ else
+ {
+ strcat(lDialogString, "\" 0 60 >/dev/tty) ") ;
+ if ( lWasXterm )
+ {
+ strcat( lDialogString ,
+ "2>/tmp/tinyfd.txt';cat /tmp/tinyfd.txt;rm /tmp/tinyfd.txt");
+ }
+ else
+ {
+ strcat(lDialogString, "2>&1 ; clear >/dev/tty") ;
+ }
+ }
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){return tinyfd_inputBox(aTitle,NULL,NULL);}
+ strcpy(lBuff, "Save file in ");
+ strcat(lBuff, getCurDir());
+ lPointerInputBox = tinyfd_inputBox(NULL, NULL, NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, lBuff, "");
+ if (p) strcpy(lBuff, p); else lBuff[0] = '\0';
+ if (lPointerInputBox) strcpy(lPointerInputBox, lString); /* restore its previous content to tinyfd_inputBox */
+ p = lBuff;
+
+ getPathWithoutFinalSlash( lString , p ) ;
+ if ( strlen( lString ) && ! dirExists( lString ) )
+ {
+ return NULL ;
+ }
+ getLastName(lString,p);
+ if ( ! strlen(lString) )
+ {
+ return NULL;
+ }
+ return p ;
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+
+ lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+ if ( ! lIn )
+ {
+ return NULL ;
+ }
+ while ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {}
+ pclose( lIn ) ;
+ if ( strlen(lBuff) && lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ /* printf( "lBuff: %s\n" , lBuff ) ; */
+ if ( ! strlen(lBuff) )
+ {
+ return NULL;
+ }
+
+ getPathWithoutFinalSlash( lString , lBuff ) ;
+ if ( strlen( lString ) && ! dirExists( lString ) )
+ {
+ return NULL ;
+ }
+ strcpy(lLastDirectory, lString) ;
+
+ getLastName(lString,lBuff);
+ if ( ! filenameValid(lString) )
+ {
+ return NULL;
+ }
+ return lBuff ;
+}
+
+
+/* in case of multiple files, the separator is | */
+char * tinyfd_openFileDialog(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPathAndOrFile , /* NULL or "" , ends with / to set only a directory */
+ int aNumOfFilterPatterns , /* 0 */
+ char const * const * aFilterPatterns , /* NULL or {"*.jpg","*.png"} */
+ char const * aSingleFilterDescription , /* NULL or "image files" */
+ int aAllowMultipleSelects ) /* 0 or 1 */
+{
+ static char * lBuff = NULL;
+ static char lLastDirectory[MAX_PATH_OR_CMD] = "$PWD" ;
+
+ char lDialogString[MAX_PATH_OR_CMD] ;
+ char lString[MAX_PATH_OR_CMD] ;
+ int i ;
+ FILE * lIn ;
+ char * p ;
+ char * lPointerInputBox ;
+ size_t lFullBuffLen ;
+ int lWasKdialog = 0 ;
+ int lWasGraphicDialog = 0 ;
+ int lWasXterm = 0 ;
+
+ if ( ! aFilterPatterns ) aNumOfFilterPatterns = 0 ;
+ if (tfd_quoteDetected(aTitle)) return tinyfd_openFileDialog("INVALID TITLE WITH QUOTES", aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription, aAllowMultipleSelects);
+ if (tfd_quoteDetected(aDefaultPathAndOrFile)) return tinyfd_openFileDialog(aTitle, "INVALID DEFAULT_PATH WITH QUOTES", aNumOfFilterPatterns, aFilterPatterns, aSingleFilterDescription, aAllowMultipleSelects);
+ if (tfd_quoteDetected(aSingleFilterDescription)) return tinyfd_openFileDialog(aTitle, aDefaultPathAndOrFile, aNumOfFilterPatterns, aFilterPatterns, "INVALID FILTER_DESCRIPTION WITH QUOTES", aAllowMultipleSelects);
+ for (i = 0; i < aNumOfFilterPatterns; i++)
+ {
+ if (tfd_quoteDetected(aFilterPatterns[i])) return tinyfd_openFileDialog("INVALID FILTER_PATTERN WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultPathAndOrFile, 0, NULL, NULL, aAllowMultipleSelects);
+ }
+
+ free(lBuff);
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query"))
+ {
+ lBuff = NULL;
+ }
+ else
+ {
+ if (aAllowMultipleSelects)
+ {
+ lFullBuffLen = MAX_MULTIPLE_FILES * MAX_PATH_OR_CMD + 1;
+ lBuff = (char *) malloc(lFullBuffLen * sizeof(char));
+ if (!lBuff)
+ {
+ lFullBuffLen = LOW_MULTIPLE_FILES * MAX_PATH_OR_CMD + 1;
+ lBuff = (char *) malloc( lFullBuffLen * sizeof(char));
+ }
+ }
+ else
+ {
+ lFullBuffLen = MAX_PATH_OR_CMD + 1;
+ lBuff = (char *) malloc(lFullBuffLen * sizeof(char));
+ }
+ if (!lBuff) return NULL;
+ lBuff[0]='\0';
+ }
+
+ if ( osascriptPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return (char *)1;}
+ strcpy( lDialogString , "osascript ");
+ if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+ strcat( lDialogString , " -e 'try' -e '" );
+ if ( ! aAllowMultipleSelects )
+ {
+
+
+ strcat( lDialogString , "POSIX path of ( " );
+ }
+ else
+ {
+ strcat( lDialogString , "set mylist to " );
+ }
+ strcat( lDialogString , "choose file " );
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "with prompt \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+ getPathWithoutFinalSlash( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "default location \"") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "\" " ) ;
+ }
+ if ( aNumOfFilterPatterns > 0 )
+ {
+ strcat(lDialogString , "of type {\"" );
+ strcat( lDialogString , aFilterPatterns[0] + 2 ) ;
+ strcat( lDialogString , "\"" ) ;
+ for ( i = 1 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , ",\"" ) ;
+ strcat( lDialogString , aFilterPatterns[i] + 2) ;
+ strcat( lDialogString , "\"" ) ;
+ }
+ strcat( lDialogString , "} " ) ;
+ }
+ if ( aAllowMultipleSelects )
+ {
+ strcat( lDialogString , "multiple selections allowed true ' " ) ;
+ strcat( lDialogString ,
+ "-e 'set mystring to POSIX path of item 1 of mylist' " );
+ strcat( lDialogString ,
+ "-e 'repeat with i from 2 to the count of mylist' " );
+ strcat( lDialogString , "-e 'set mystring to mystring & \"|\"' " );
+ strcat( lDialogString ,
+ "-e 'set mystring to mystring & POSIX path of item i of mylist' " );
+ strcat( lDialogString , "-e 'end repeat' " );
+ strcat( lDialogString , "-e 'mystring' " );
+ }
+ else
+ {
+ strcat( lDialogString , ")' " ) ;
+ }
+ strcat(lDialogString, "-e 'on error number -128' " ) ;
+ strcat(lDialogString, "-e 'end try'") ;
+ if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
+ }
+ else if ( tfd_kdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return (char *)1;}
+ lWasKdialog = 1 ;
+
+ strcpy( lDialogString , "kdialog" ) ;
+ if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ strcat( lDialogString , " --getopenfilename " ) ;
+
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ strcat(lDialogString, "\"") ;
+ if ( aDefaultPathAndOrFile[0] != '/' )
+ {
+ strcat(lDialogString, lLastDirectory) ;
+ strcat(lDialogString , "/" ) ;
+ }
+ strcat(lDialogString, aDefaultPathAndOrFile ) ;
+ strcat(lDialogString , "\"" ) ;
+ }
+ else
+ {
+ strcat(lDialogString, "\"") ;
+ strcat(lDialogString, lLastDirectory) ;
+ strcat(lDialogString , "/" ) ;
+ strcat(lDialogString, "\"") ;
+ }
+
+ if ( aNumOfFilterPatterns > 0 )
+ {
+ strcat(lDialogString , " \"" ) ;
+ strcat( lDialogString , aFilterPatterns[0] ) ;
+ for ( i = 1 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , " " ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ }
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , " | " ) ;
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ }
+ strcat( lDialogString , "\"" ) ;
+ }
+ if ( aAllowMultipleSelects )
+ {
+ strcat( lDialogString , " --multiple --separate-output" ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, " --title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+ }
+ }
+ else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+ || tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
+ strcpy( lDialogString , "zenity" ) ;
+ if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_matedialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
+ strcpy( lDialogString , "matedialog" ) ;
+ }
+ else if ( tfd_shellementaryPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
+ strcpy( lDialogString , "shellementary" ) ;
+ }
+ else if ( tfd_qarmaPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
+ strcpy( lDialogString , "qarma" ) ;
+ if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_boxerPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+ strcpy( lDialogString , "boxer" ) ;
+ }
+ else if ( tfd_shantyPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+ strcpy( lDialogString , "shanty" ) ;
+ }
+ else {}
+
+ strcat( lDialogString , " --file-selection" ) ;
+
+ if ( aAllowMultipleSelects )
+ {
+ strcat( lDialogString , " --multiple" ) ;
+ }
+
+ strcat(lDialogString, " --title=\"") ;
+ if (aTitle && strlen(aTitle)) strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ strcat(lDialogString, " --filename=\"") ;
+ strcat(lDialogString, aDefaultPathAndOrFile) ;
+ strcat(lDialogString, "\"") ;
+ }
+ if ( aNumOfFilterPatterns > 0 )
+ {
+ strcat( lDialogString , " --file-filter='" ) ;
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ strcat( lDialogString , " | " ) ;
+ }
+ strcat( lDialogString , aFilterPatterns[0] ) ;
+ for ( i = 1 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , " " ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ }
+ strcat( lDialogString , "' --file-filter='All files | *'" ) ;
+ }
+ if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
+ }
+ else if (tfd_yadPresent())
+ {
+ if (aTitle && !strcmp(aTitle, "tinyfd_query")) { strcpy(tinyfd_response, "yad"); return (char*)1; }
+ strcpy(lDialogString, "yad --file");
+ if (aAllowMultipleSelects)
+ {
+ strcat(lDialogString, " --multiple");
+ }
+ if (aTitle && strlen(aTitle))
+ {
+ strcat(lDialogString, " --title=\"");
+ strcat(lDialogString, aTitle);
+ strcat(lDialogString, "\"");
+ }
+ if (aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile))
+ {
+ strcat(lDialogString, " --filename=\"");
+ strcat(lDialogString, aDefaultPathAndOrFile);
+ strcat(lDialogString, "\"");
+ }
+ if (aNumOfFilterPatterns > 0)
+ {
+ strcat(lDialogString, " --file-filter='");
+ if (aSingleFilterDescription && strlen(aSingleFilterDescription))
+ {
+ strcat(lDialogString, aSingleFilterDescription);
+ strcat(lDialogString, " | ");
+ }
+ strcat(lDialogString, aFilterPatterns[0]);
+ for ( i = 1 ; i < aNumOfFilterPatterns; i++)
+ {
+ strcat(lDialogString, " ");
+ strcat(lDialogString, aFilterPatterns[i]);
+ }
+ strcat(lDialogString, "' --file-filter='All files | *'");
+ }
+ if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
+ }
+ else if ( tkinter3Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python3-tkinter");return (char *)1;}
+ strcpy( lDialogString , gPython3Name ) ;
+ strcat( lDialogString ,
+ " -S -c \"import tkinter;from tkinter import filedialog;root=tkinter.Tk();root.withdraw();");
+ strcat( lDialogString , "lFiles=filedialog.askopenfilename(");
+ if ( aAllowMultipleSelects )
+ {
+ strcat( lDialogString , "multiple=1," ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ getPathWithoutFinalSlash( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialdir='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ getLastName( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialfile='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ }
+ if ( ( aNumOfFilterPatterns > 1 )
+ || ( ( aNumOfFilterPatterns == 1 ) /*test because poor osx behaviour*/
+ && ( aFilterPatterns[0][strlen(aFilterPatterns[0])-1] != '*' ) ) )
+ {
+ strcat(lDialogString , "filetypes=(" ) ;
+ strcat( lDialogString , "('" ) ;
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ }
+ strcat( lDialogString , "',(" ) ;
+ for ( i = 0 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , "'" ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ strcat( lDialogString , "'," ) ;
+ }
+ strcat( lDialogString , "))," ) ;
+ strcat( lDialogString , "('All files','*'))" ) ;
+ }
+ strcat( lDialogString , ");\
+\nif not isinstance(lFiles, tuple):\n\tprint(lFiles)\nelse:\
+\n\tlFilesString=''\n\tfor lFile in lFiles:\n\t\tlFilesString+=str(lFile)+'|'\
+\n\tprint(lFilesString[:-1])\n\"" ) ;
+ }
+ else if ( tkinter2Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python2-tkinter");return (char *)1;}
+ strcpy( lDialogString , "export PYTHONIOENCODING=utf-8;" ) ;
+ strcat( lDialogString , gPython2Name ) ;
+ if ( ! isTerminalRunning( ) && tfd_isDarwin( ) )
+ {
+ strcat( lDialogString , " -i" ) ; /* for osx without console */
+ }
+ strcat( lDialogString ,
+" -S -c \"import Tkinter,tkFileDialog;root=Tkinter.Tk();root.withdraw();");
+
+ if ( tfd_isDarwin( ) )
+ {
+ strcat( lDialogString ,
+"import os;os.system('''/usr/bin/osascript -e 'tell app \\\"Finder\\\" to set \
+frontmost of process \\\"Python\\\" to true' ''');");
+ }
+ strcat( lDialogString , "lFiles=tkFileDialog.askopenfilename(");
+ if ( aAllowMultipleSelects )
+ {
+ strcat( lDialogString , "multiple=1," ) ;
+ }
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ getPathWithoutFinalSlash( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialdir='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ getLastName( lString , aDefaultPathAndOrFile ) ;
+ if ( strlen(lString) )
+ {
+ strcat(lDialogString, "initialfile='") ;
+ strcat(lDialogString, lString ) ;
+ strcat(lDialogString , "'," ) ;
+ }
+ }
+ if ( ( aNumOfFilterPatterns > 1 )
+ || ( ( aNumOfFilterPatterns == 1 ) /*test because poor osx behaviour*/
+ && ( aFilterPatterns[0][strlen(aFilterPatterns[0])-1] != '*' ) ) )
+ {
+ strcat(lDialogString , "filetypes=(" ) ;
+ strcat( lDialogString , "('" ) ;
+ if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
+ {
+ strcat( lDialogString , aSingleFilterDescription ) ;
+ }
+ strcat( lDialogString , "',(" ) ;
+ for ( i = 0 ; i < aNumOfFilterPatterns ; i ++ )
+ {
+ strcat( lDialogString , "'" ) ;
+ strcat( lDialogString , aFilterPatterns[i] ) ;
+ strcat( lDialogString , "'," ) ;
+ }
+ strcat( lDialogString , "))," ) ;
+ strcat( lDialogString , "('All files','*'))" ) ;
+ }
+ strcat( lDialogString , ");\
+\nif not isinstance(lFiles, tuple):\n\tprint lFiles\nelse:\
+\n\tlFilesString=''\n\tfor lFile in lFiles:\n\t\tlFilesString+=str(lFile)+'|'\
+\n\tprint lFilesString[:-1]\n\"" ) ;
+ }
+ else if ( xdialogPresent() || dialogName() )
+ {
+ if ( xdialogPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"xdialog");return (char *)1;}
+ lWasGraphicDialog = 1 ;
+ strcpy( lDialogString , "(Xdialog " ) ;
+ }
+ else if ( isTerminalRunning( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ strcpy( lDialogString , "(dialog " ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ lWasXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'(" ) ;
+ strcat( lDialogString , dialogName() ) ;
+ strcat( lDialogString , " " ) ;
+ }
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( !xdialogPresent() && !gdialogPresent() )
+ {
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString,
+ "tab: focus | /: populate | spacebar: fill text field | ok: TEXT FIELD ONLY") ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat( lDialogString , "--fselect \"" ) ;
+ if ( aDefaultPathAndOrFile && strlen(aDefaultPathAndOrFile) )
+ {
+ if ( ! strchr(aDefaultPathAndOrFile, '/') )
+ {
+ strcat(lDialogString, "./") ;
+ }
+ strcat(lDialogString, aDefaultPathAndOrFile) ;
+ }
+ else if ( ! isTerminalRunning( ) && !lWasGraphicDialog )
+ {
+ strcat(lDialogString, getenv("HOME")) ;
+ strcat(lDialogString, "/");
+ }
+ else
+ {
+ strcat(lDialogString, "./") ;
+ }
+
+ if ( lWasGraphicDialog )
+ {
+ strcat(lDialogString, "\" 0 60 ) 2>&1 ") ;
+ }
+ else
+ {
+ strcat(lDialogString, "\" 0 60 >/dev/tty) ") ;
+ if ( lWasXterm )
+ {
+ strcat( lDialogString ,
+ "2>/tmp/tinyfd.txt';cat /tmp/tinyfd.txt;rm /tmp/tinyfd.txt");
+ }
+ else
+ {
+ strcat(lDialogString, "2>&1 ; clear >/dev/tty") ;
+ }
+ }
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){return tinyfd_inputBox(aTitle,NULL,NULL);}
+ strcpy(lBuff, "Open file from ");
+ strcat(lBuff, getCurDir());
+ lPointerInputBox = tinyfd_inputBox(NULL, NULL, NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lDialogString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, lBuff, "");
+ if ( p ) strcpy(lBuff, p); else lBuff[0] = '\0';
+ if (lPointerInputBox) strcpy(lPointerInputBox, lDialogString); /* restore its previous content to tinyfd_inputBox */
+ if ( ! fileExists(lBuff) )
+ {
+ free(lBuff);
+ lBuff = NULL;
+ }
+ else
+ {
+ lBuff = (char *)( realloc( lBuff, (strlen(lBuff)+1) * sizeof(char)));
+ }
+ return lBuff ;
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+ lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+ if ( ! lIn )
+ {
+ free(lBuff);
+ lBuff = NULL;
+ return NULL ;
+ }
+ lBuff[0]='\0';
+ p = lBuff;
+ while ( fgets( p , sizeof( lBuff ) , lIn ) != NULL )
+ {
+ p += strlen( p );
+ }
+ pclose( lIn ) ;
+
+ if ( strlen( lBuff ) && lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ /* printf( "strlen lBuff: %d\n" , strlen( lBuff ) ) ; */
+ if ( lWasKdialog && aAllowMultipleSelects )
+ {
+ p = lBuff ;
+ while ( ( p = strchr( p , '\n' ) ) )
+ * p = '|' ;
+ }
+ /* printf( "lBuff2: %s\n" , lBuff ) ; */
+ if ( ! strlen( lBuff ) )
+ {
+ free(lBuff);
+ lBuff = NULL;
+ return NULL;
+ }
+ if ( aAllowMultipleSelects && strchr(lBuff, '|') )
+ {
+ if( ! ensureFilesExist( lBuff , lBuff ) )
+ {
+ free(lBuff);
+ lBuff = NULL;
+ return NULL;
+ }
+ }
+ else if ( !fileExists(lBuff) )
+ {
+ free(lBuff);
+ lBuff = NULL;
+ return NULL;
+ }
+
+ p = strrchr(lBuff, '|');
+ if ( !p ) p = lBuff ;
+ else p ++ ;
+ getPathWithoutFinalSlash( lString , p ) ;
+ /* printf( "lString [%lu]: %s\n" , strlen(lString) , lString ) ; */
+ if ( strlen( lString ) && ! dirExists( lString ) )
+ {
+ return NULL ;
+ }
+ strcpy(lLastDirectory, lString) ;
+
+ lBuff = (char *)( realloc( lBuff, (strlen(lBuff)+1) * sizeof(char)));
+
+ /*printf( "lBuff3 [%lu]: %s\n" , strlen(lBuff) , lBuff ) ; */
+ return lBuff ;
+}
+
+
+char * tinyfd_selectFolderDialog(
+ char const * aTitle , /* "" */
+ char const * aDefaultPath ) /* "" */
+{
+ static char lBuff[MAX_PATH_OR_CMD] ;
+ static char lLastDirectory[MAX_PATH_OR_CMD] = "$PWD" ;
+
+ char lDialogString[MAX_PATH_OR_CMD] ;
+ FILE * lIn ;
+ char * p ;
+ char * lPointerInputBox ;
+ int lWasGraphicDialog = 0 ;
+ int lWasXterm = 0 ;
+ lBuff[0]='\0';
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_selectFolderDialog("INVALID TITLE WITH QUOTES", aDefaultPath);
+ if (tfd_quoteDetected(aDefaultPath)) return tinyfd_selectFolderDialog(aTitle, "INVALID DEFAULT_PATH WITH QUOTES");
+
+ if ( osascriptPresent( ))
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return (char *)1;}
+ strcpy( lDialogString , "osascript ");
+ if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+ strcat( lDialogString , " -e 'try' -e 'POSIX path of ( choose folder ");
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "with prompt \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+ if ( aDefaultPath && strlen(aDefaultPath) )
+ {
+ strcat(lDialogString, "default location \"") ;
+ strcat(lDialogString, aDefaultPath ) ;
+ strcat(lDialogString , "\" " ) ;
+ }
+ strcat( lDialogString , ")' " ) ;
+ strcat(lDialogString, "-e 'on error number -128' " ) ;
+ strcat(lDialogString, "-e 'end try'") ;
+ if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
+ }
+ else if ( tfd_kdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return (char *)1;}
+ strcpy( lDialogString , "kdialog" ) ;
+ if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ strcat( lDialogString , " --getexistingdirectory " ) ;
+
+ if ( aDefaultPath && strlen(aDefaultPath) )
+ {
+ strcat(lDialogString, "\"") ;
+ if ( aDefaultPath[0] != '/' )
+ {
+ strcat(lDialogString, lLastDirectory) ;
+ strcat(lDialogString , "/" ) ;
+ }
+ strcat(lDialogString, aDefaultPath ) ;
+ strcat(lDialogString , "\"" ) ;
+ }
+ else
+ {
+ strcat(lDialogString, "\"") ;
+ strcat(lDialogString, lLastDirectory) ;
+ strcat(lDialogString , "/" ) ;
+ strcat(lDialogString, "\"") ;
+ }
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, " --title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+ }
+ }
+ else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+ || tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
+ {
+ if ( tfd_zenityPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
+ strcpy( lDialogString , "zenity" ) ;
+ if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_matedialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
+ strcpy( lDialogString , "matedialog" ) ;
+ }
+ else if ( tfd_shellementaryPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
+ strcpy( lDialogString , "shellementary" ) ;
+ }
+ else if ( tfd_qarmaPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
+ strcpy( lDialogString , "qarma" ) ;
+ if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_boxerPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+ strcpy( lDialogString , "boxer" ) ;
+ }
+ else if ( tfd_shantyPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+ strcpy( lDialogString , "shanty" ) ;
+ }
+ else {}
+
+ strcat( lDialogString , " --file-selection --directory" ) ;
+
+ strcat(lDialogString, " --title=\"") ;
+ if (aTitle && strlen(aTitle)) strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+
+ if ( aDefaultPath && strlen(aDefaultPath) )
+ {
+ strcat(lDialogString, " --filename=\"") ;
+ strcat(lDialogString, aDefaultPath) ;
+ strcat(lDialogString, "\"") ;
+ }
+ if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
+ }
+ else if (tfd_yadPresent())
+ {
+ if (aTitle && !strcmp(aTitle, "tinyfd_query")) { strcpy(tinyfd_response, "yad"); return (char*)1; }
+ strcpy(lDialogString, "yad --file --directory");
+ if (aTitle && strlen(aTitle))
+ {
+ strcat(lDialogString, " --title=\"");
+ strcat(lDialogString, aTitle);
+ strcat(lDialogString, "\"");
+ }
+ if (aDefaultPath && strlen(aDefaultPath))
+ {
+ strcat(lDialogString, " --filename=\"");
+ strcat(lDialogString, aDefaultPath);
+ strcat(lDialogString, "\"");
+ }
+ if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
+ }
+ else if ( !xdialogPresent() && tkinter3Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python3-tkinter");return (char *)1;}
+ strcpy( lDialogString , gPython3Name ) ;
+ strcat( lDialogString ,
+ " -S -c \"import tkinter;from tkinter import filedialog;root=tkinter.Tk();root.withdraw();");
+ strcat( lDialogString , "res=filedialog.askdirectory(");
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultPath && strlen(aDefaultPath) )
+ {
+ strcat(lDialogString, "initialdir='") ;
+ strcat(lDialogString, aDefaultPath ) ;
+ strcat(lDialogString , "'" ) ;
+ }
+ strcat( lDialogString, ");\nif not isinstance(res, tuple):\n\tprint(res)\n\"" ) ;
+ }
+ else if ( !xdialogPresent() && tkinter2Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python2-tkinter");return (char *)1;}
+ strcpy( lDialogString , "export PYTHONIOENCODING=utf-8;" ) ;
+ strcat( lDialogString , gPython2Name ) ;
+ if ( ! isTerminalRunning( ) && tfd_isDarwin( ) )
+ {
+ strcat( lDialogString , " -i" ) ; /* for osx without console */
+ }
+ strcat( lDialogString ,
+" -S -c \"import Tkinter,tkFileDialog;root=Tkinter.Tk();root.withdraw();");
+
+ if ( tfd_isDarwin( ) )
+ {
+ strcat( lDialogString ,
+"import os;os.system('''/usr/bin/osascript -e 'tell app \\\"Finder\\\" to set \
+frontmost of process \\\"Python\\\" to true' ''');");
+ }
+
+ strcat( lDialogString , "print tkFileDialog.askdirectory(");
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "',") ;
+ }
+ if ( aDefaultPath && strlen(aDefaultPath) )
+ {
+ strcat(lDialogString, "initialdir='") ;
+ strcat(lDialogString, aDefaultPath ) ;
+ strcat(lDialogString , "'" ) ;
+ }
+ strcat( lDialogString , ")\"" ) ;
+ }
+ else if ( xdialogPresent() || dialogName() )
+ {
+ if ( xdialogPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"xdialog");return (char *)1;}
+ lWasGraphicDialog = 1 ;
+ strcpy( lDialogString , "(Xdialog " ) ;
+ }
+ else if ( isTerminalRunning( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ strcpy( lDialogString , "(dialog " ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"dialog");return (char *)0;}
+ lWasXterm = 1 ;
+ strcpy( lDialogString , terminalName() ) ;
+ strcat( lDialogString , "'(" ) ;
+ strcat( lDialogString , dialogName() ) ;
+ strcat( lDialogString , " " ) ;
+ }
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, "--title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ if ( !xdialogPresent() && !gdialogPresent() )
+ {
+ strcat(lDialogString, "--backtitle \"") ;
+ strcat(lDialogString,
+ "tab: focus | /: populate | spacebar: fill text field | ok: TEXT FIELD ONLY") ;
+ strcat(lDialogString, "\" ") ;
+ }
+
+ strcat( lDialogString , "--dselect \"" ) ;
+ if ( aDefaultPath && strlen(aDefaultPath) )
+ {
+ strcat(lDialogString, aDefaultPath) ;
+ ensureFinalSlash(lDialogString);
+ }
+ else if ( ! isTerminalRunning( ) && !lWasGraphicDialog )
+ {
+ strcat(lDialogString, getenv("HOME")) ;
+ strcat(lDialogString, "/");
+ }
+ else
+ {
+ strcat(lDialogString, "./") ;
+ }
+
+ if ( lWasGraphicDialog )
+ {
+ strcat(lDialogString, "\" 0 60 ) 2>&1 ") ;
+ }
+ else
+ {
+ strcat(lDialogString, "\" 0 60 >/dev/tty) ") ;
+ if ( lWasXterm )
+ {
+ strcat( lDialogString ,
+ "2>/tmp/tinyfd.txt';cat /tmp/tinyfd.txt;rm /tmp/tinyfd.txt");
+ }
+ else
+ {
+ strcat(lDialogString, "2>&1 ; clear >/dev/tty") ;
+ }
+ }
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){return tinyfd_inputBox(aTitle,NULL,NULL);}
+ strcpy(lBuff, "Select folder from ");
+ strcat(lBuff, getCurDir());
+ lPointerInputBox = tinyfd_inputBox(NULL, NULL, NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lDialogString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, lBuff, "");
+ if (p) strcpy(lBuff, p); else lBuff[0] = '\0';
+ if (lPointerInputBox) strcpy(lPointerInputBox, lDialogString); /* restore its previous content to tinyfd_inputBox */
+ p = lBuff;
+
+ if ( !p || ! strlen( p ) || ! dirExists( p ) )
+ {
+ return NULL ;
+ }
+ return p ;
+ }
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+ lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+ if ( ! lIn )
+ {
+ return NULL ;
+ }
+ while ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {}
+ pclose( lIn ) ;
+ if ( strlen( lBuff ) && lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+ /* printf( "lBuff: %s\n" , lBuff ) ; */
+ if ( ! strlen( lBuff ) || ! dirExists( lBuff ) )
+ {
+ return NULL ;
+ }
+
+ getPathWithoutFinalSlash( lLastDirectory , lBuff ) ;
+
+ return lBuff ;
+}
+
+
+/* aDefaultRGB is used only if aDefaultHexRGB is absent */
+/* aDefaultRGB and aoResultRGB can be the same array */
+/* returns NULL on cancel */
+/* returns the hexcolor as a string "#FF0000" */
+/* aoResultRGB also contains the result */
+char * tinyfd_colorChooser(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultHexRGB , /* NULL or "#FF0000"*/
+ unsigned char const aDefaultRGB[3] , /* { 0 , 255 , 255 } */
+ unsigned char aoResultRGB[3] ) /* { 0 , 0 , 0 } */
+{
+ static char lDefaultHexRGB[16];
+ char lBuff[128] ;
+
+ char lTmp[128] ;
+#if !((defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__))
+ char * lTmp2 ;
+#endif
+ char lDialogString[MAX_PATH_OR_CMD] ;
+ unsigned char lDefaultRGB[3];
+ char * p;
+ char * lPointerInputBox;
+ FILE * lIn ;
+ int i ;
+ int lWasZenity2032 = 0 ;
+ int lWasOsascript = 0 ;
+ int lWasXdialog = 0 ;
+ lBuff[0]='\0';
+
+ if (tfd_quoteDetected(aTitle)) return tinyfd_colorChooser("INVALID TITLE WITH QUOTES", aDefaultHexRGB, aDefaultRGB, aoResultRGB);
+ if (tfd_quoteDetected(aDefaultHexRGB)) return tinyfd_colorChooser(aTitle, "INVALID DEFAULT_HEX_RGB WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultRGB, aoResultRGB);
+
+ if (aDefaultHexRGB && (strlen(aDefaultHexRGB)==7) )
+ {
+ Hex2RGB(aDefaultHexRGB, lDefaultRGB);
+ strcpy(lDefaultHexRGB, aDefaultHexRGB);
+ }
+ else
+ {
+ lDefaultRGB[0] = aDefaultRGB[0];
+ lDefaultRGB[1] = aDefaultRGB[1];
+ lDefaultRGB[2] = aDefaultRGB[2];
+ RGB2Hex(aDefaultRGB, lDefaultHexRGB);
+ }
+
+ if ( osascriptPresent( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return (char *)1;}
+ lWasOsascript = 1 ;
+ strcpy( lDialogString , "osascript");
+
+ if ( ! osx9orBetter() )
+ {
+ strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+ strcat( lDialogString , " -e 'try' -e 'set mycolor to choose color default color {");
+ }
+ else
+ {
+ strcat( lDialogString ,
+" -e 'try' -e 'tell app (path to frontmost application as Unicode text) \
+to set mycolor to choose color default color {");
+ }
+
+ sprintf(lTmp, "%d", 256 * lDefaultRGB[0] ) ;
+ strcat(lDialogString, lTmp ) ;
+ strcat(lDialogString, "," ) ;
+ sprintf(lTmp, "%d", 256 * lDefaultRGB[1] ) ;
+ strcat(lDialogString, lTmp ) ;
+ strcat(lDialogString, "," ) ;
+ sprintf(lTmp, "%d", 256 * lDefaultRGB[2] ) ;
+ strcat(lDialogString, lTmp ) ;
+ strcat(lDialogString, "}' " ) ;
+ strcat( lDialogString ,
+"-e 'set mystring to ((item 1 of mycolor) div 256 as integer) as string' " );
+ strcat( lDialogString ,
+"-e 'repeat with i from 2 to the count of mycolor' " );
+ strcat( lDialogString ,
+"-e 'set mystring to mystring & \" \" & ((item i of mycolor) div 256 as integer) as string' " );
+ strcat( lDialogString , "-e 'end repeat' " );
+ strcat( lDialogString , "-e 'mystring' ");
+ strcat(lDialogString, "-e 'on error number -128' " ) ;
+ strcat(lDialogString, "-e 'end try'") ;
+ if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
+ }
+ else if ( tfd_kdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return (char *)1;}
+ strcpy( lDialogString , "kdialog" ) ;
+ if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ sprintf( lDialogString + strlen(lDialogString) , " --getcolor --default '%s'" , lDefaultHexRGB ) ;
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, " --title \"") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+ }
+ }
+ else if ( (tfd_zenityVersion()>=2032) || tfd_matedialogPresent() || tfd_shellementaryPresent()
+ || tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
+ {
+ lWasZenity2032 = 1 ;
+ if ( tfd_zenityVersion()>=2032 )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity3");return (char *)1;}
+ strcpy( lDialogString , "zenity" );
+ if ( (tfd_zenityVersion() >=3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_matedialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
+ strcpy( lDialogString , "matedialog" ) ;
+ }
+ else if ( tfd_shellementaryPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
+ strcpy( lDialogString , "shellementary" ) ;
+ }
+ else if ( tfd_qarmaPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
+ strcpy( lDialogString , "qarma" ) ;
+ if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+ {
+ strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+ }
+ }
+ else if ( tfd_boxerPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+ strcpy( lDialogString , "boxer" ) ;
+ }
+ else if ( tfd_shantyPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+ strcpy( lDialogString , "shanty" ) ;
+ }
+ else {}
+
+ strcat( lDialogString , " --color-selection" ) ;
+ if ( tfd_zenityVersion()>=2032 || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() || tfd_boxerPresent() )
+ {
+ strcat( lDialogString , " --show-palette" ) ;
+ }
+ sprintf( lDialogString + strlen(lDialogString), " --color=%s" , lDefaultHexRGB ) ;
+
+ strcat(lDialogString, " --title=\"") ;
+ if (aTitle && strlen(aTitle)) strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "\"") ;
+
+ if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
+ }
+ else if (tfd_yadPresent())
+ {
+ if (aTitle && !strcmp(aTitle, "tinyfd_query")) { strcpy(tinyfd_response, "yad"); return (char*)1; }
+ strcpy(lDialogString, "yad --color");
+ sprintf(lDialogString + strlen(lDialogString), " --init-color=%s", lDefaultHexRGB);
+ if (aTitle && strlen(aTitle))
+ {
+ strcat(lDialogString, " --title=\"");
+ strcat(lDialogString, aTitle);
+ strcat(lDialogString, "\"");
+ }
+ if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
+ }
+ else if ( xdialogPresent() )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"xdialog");return (char *)1;}
+ lWasXdialog = 1 ;
+ strcpy( lDialogString , "Xdialog --colorsel \"" ) ;
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, aTitle) ;
+ }
+ strcat(lDialogString, "\" 0 60 ") ;
+#if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
+ sprintf(lTmp,"%hhu %hhu %hhu",lDefaultRGB[0],lDefaultRGB[1],lDefaultRGB[2]);
+#else
+ sprintf(lTmp,"%hu %hu %hu",lDefaultRGB[0],lDefaultRGB[1],lDefaultRGB[2]);
+#endif
+ strcat(lDialogString, lTmp) ;
+ strcat(lDialogString, " 2>&1");
+ }
+ else if ( tkinter3Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python3-tkinter");return (char *)1;}
+ strcpy( lDialogString , gPython3Name ) ;
+ strcat( lDialogString ,
+ " -S -c \"import tkinter;from tkinter import colorchooser;root=tkinter.Tk();root.withdraw();");
+ strcat( lDialogString , "res=colorchooser.askcolor(color='" ) ;
+ strcat(lDialogString, lDefaultHexRGB ) ;
+ strcat(lDialogString, "'") ;
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, ",title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "'") ;
+ }
+ strcat( lDialogString , ");\
+\nif res[1] is not None:\n\tprint(res[1])\"" ) ;
+ }
+ else if ( tkinter2Present( ) )
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python2-tkinter");return (char *)1;}
+ strcpy( lDialogString , "export PYTHONIOENCODING=utf-8;" ) ;
+ strcat( lDialogString , gPython2Name ) ;
+ if ( ! isTerminalRunning( ) && tfd_isDarwin( ) )
+ {
+ strcat( lDialogString , " -i" ) ; /* for osx without console */
+ }
+
+ strcat( lDialogString ,
+" -S -c \"import Tkinter,tkColorChooser;root=Tkinter.Tk();root.withdraw();");
+
+ if ( tfd_isDarwin( ) )
+ {
+ strcat( lDialogString ,
+"import os;os.system('''osascript -e 'tell app \\\"Finder\\\" to set \
+frontmost of process \\\"Python\\\" to true' ''');");
+ }
+
+ strcat( lDialogString , "res=tkColorChooser.askcolor(color='" ) ;
+ strcat(lDialogString, lDefaultHexRGB ) ;
+ strcat(lDialogString, "'") ;
+
+
+ if ( aTitle && strlen(aTitle) )
+ {
+ strcat(lDialogString, ",title='") ;
+ strcat(lDialogString, aTitle) ;
+ strcat(lDialogString, "'") ;
+ }
+ strcat( lDialogString , ");\
+\nif res[1] is not None:\n\tprint res[1]\"" ) ;
+ }
+ else
+ {
+ if (aTitle&&!strcmp(aTitle,"tinyfd_query")){return tinyfd_inputBox(aTitle,NULL,NULL);}
+ lPointerInputBox = tinyfd_inputBox(NULL, NULL, NULL); /* obtain a pointer on the current content of tinyfd_inputBox */
+ if (lPointerInputBox) strcpy(lDialogString, lPointerInputBox); /* preserve the current content of tinyfd_inputBox */
+ p = tinyfd_inputBox(aTitle, "Enter hex rgb color (i.e. #f5ca20)", lDefaultHexRGB);
+
+ if ( !p || (strlen(p) != 7) || (p[0] != '#') )
+ {
+ return NULL ;
+ }
+ for ( i = 1 ; i < 7 ; i ++ )
+ {
+ if ( ! isxdigit( (int) p[i] ) )
+ {
+ return NULL ;
+ }
+ }
+ Hex2RGB(p,aoResultRGB);
+ strcpy(lDefaultHexRGB, p);
+ if (lPointerInputBox) strcpy(lPointerInputBox, lDialogString); /* restore its previous content to tinyfd_inputBox */
+ return lDefaultHexRGB;
+ }
+
+ if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+ lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+ if ( ! lIn )
+ {
+ return NULL ;
+ }
+ while ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+ {
+ }
+ pclose( lIn ) ;
+ if ( ! strlen( lBuff ) )
+ {
+ return NULL ;
+ }
+ /* printf( "len Buff: %lu\n" , strlen(lBuff) ) ; */
+ /* printf( "lBuff0: %s\n" , lBuff ) ; */
+ if ( lBuff[strlen( lBuff ) -1] == '\n' )
+ {
+ lBuff[strlen( lBuff ) -1] = '\0' ;
+ }
+
+ if ( lWasZenity2032 )
+ {
+ if ( lBuff[0] == '#' )
+ {
+ if ( strlen(lBuff)>7 )
+ {
+ lBuff[3]=lBuff[5];
+ lBuff[4]=lBuff[6];
+ lBuff[5]=lBuff[9];
+ lBuff[6]=lBuff[10];
+ lBuff[7]='\0';
+ }
+ Hex2RGB(lBuff,aoResultRGB);
+ }
+ else if ( lBuff[3] == '(' ) {
+#if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
+ sscanf(lBuff,"rgb(%hhu,%hhu,%hhu", & aoResultRGB[0], & aoResultRGB[1],& aoResultRGB[2]);
+#else
+ aoResultRGB[0] = (unsigned char) strtol(lBuff+4, & lTmp2, 10 );
+ aoResultRGB[1] = (unsigned char) strtol(lTmp2+1, & lTmp2, 10 );
+ aoResultRGB[2] = (unsigned char) strtol(lTmp2+1, NULL, 10 );
+#endif
+ RGB2Hex(aoResultRGB,lBuff);
+ }
+ else if ( lBuff[4] == '(' ) {
+#if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
+ sscanf(lBuff,"rgba(%hhu,%hhu,%hhu", & aoResultRGB[0], & aoResultRGB[1],& aoResultRGB[2]);
+#else
+ aoResultRGB[0] = (unsigned char) strtol(lBuff+5, & lTmp2, 10 );
+ aoResultRGB[1] = (unsigned char) strtol(lTmp2+1, & lTmp2, 10 );
+ aoResultRGB[2] = (unsigned char) strtol(lTmp2+1, NULL, 10 );
+#endif
+ RGB2Hex(aoResultRGB,lBuff);
+ }
+ }
+ else if ( lWasOsascript || lWasXdialog )
+ {
+ /* printf( "lBuff: %s\n" , lBuff ) ; */
+#if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
+ sscanf(lBuff,"%hhu %hhu %hhu", & aoResultRGB[0], & aoResultRGB[1],& aoResultRGB[2]);
+#else
+ aoResultRGB[0] = (unsigned char) strtol(lBuff, & lTmp2, 10 );
+ aoResultRGB[1] = (unsigned char) strtol(lTmp2+1, & lTmp2, 10 );
+ aoResultRGB[2] = (unsigned char) strtol(lTmp2+1, NULL, 10 );
+#endif
+ RGB2Hex(aoResultRGB,lBuff);
+ }
+ else
+ {
+ Hex2RGB(lBuff,aoResultRGB);
+ }
+ /* printf("%d %d %d\n", aoResultRGB[0],aoResultRGB[1],aoResultRGB[2]); */
+ /* printf( "lBuff: %s\n" , lBuff ) ; */
+
+ strcpy(lDefaultHexRGB,lBuff);
+ return lDefaultHexRGB ;
+}
+
+#endif /* _WIN32 */
+
+
+/* Modified prototypes for R */
+
+void tfd_messageBox(
+ char const * aTitle ,
+ char const * aMessage ,
+ char const * aDialogType ,
+ char const * aIconType ,
+ int * aiDefaultButton )
+{
+ * aiDefaultButton = tinyfd_messageBox( aTitle , aMessage , aDialogType , aIconType , * aiDefaultButton ) ;
+}
+
+
+void tfd_inputBox(
+ char const * aTitle ,
+ char const * aMessage ,
+ char * * aiDefaultInput )
+{
+ char * lReturnedInput ;
+ if ( ! strcmp( * aiDefaultInput , "NULL") ) lReturnedInput = tinyfd_inputBox( aTitle , aMessage , NULL ) ;
+ else lReturnedInput = tinyfd_inputBox( aTitle , aMessage , * aiDefaultInput ) ;
+
+ if ( lReturnedInput ) strcpy ( * aiDefaultInput , lReturnedInput ) ;
+ else strcpy ( * aiDefaultInput , "NULL" ) ;
+}
+
+
+void tfd_saveFileDialog(
+ char const * aTitle ,
+ char * * aiDefaultPathAndFile ,
+ int const * aNumOfFilterPatterns ,
+ char const * const * aFilterPatterns ,
+ char const * aSingleFilterDescription )
+{
+ char * lSavefile ;
+
+ /* printf( "aFilterPatterns %s\n" , aFilterPatterns [0]); */
+
+ lSavefile = tinyfd_saveFileDialog( aTitle , * aiDefaultPathAndFile , * aNumOfFilterPatterns ,
+ aFilterPatterns, aSingleFilterDescription ) ;
+ if ( lSavefile ) strcpy ( * aiDefaultPathAndFile , lSavefile ) ;
+ else strcpy ( * aiDefaultPathAndFile , "NULL" ) ;
+}
+
+
+void tfd_openFileDialog(
+ char const * aTitle ,
+ char * * aiDefaultPathAndFile ,
+ int const * aNumOfFilterPatterns ,
+ char const * const * aFilterPatterns ,
+ char const * aSingleFilterDescription ,
+ int const * aAllowMultipleSelects )
+{
+ char * lOpenfile ;
+
+ /* printf( "aFilterPatterns %s\n" , aFilterPatterns [0]); */
+
+ lOpenfile = tinyfd_openFileDialog( aTitle , * aiDefaultPathAndFile , * aNumOfFilterPatterns ,
+ aFilterPatterns , aSingleFilterDescription , * aAllowMultipleSelects ) ;
+
+ if ( lOpenfile ) strcpy ( * aiDefaultPathAndFile , lOpenfile ) ;
+ else strcpy ( * aiDefaultPathAndFile , "NULL" ) ;
+}
+
+
+void tfd_selectFolderDialog(
+ char const * aTitle ,
+ char * * aiDefaultPath )
+{
+ char * lSelectedfolder ;
+ lSelectedfolder = tinyfd_selectFolderDialog( aTitle, * aiDefaultPath ) ;
+ if ( lSelectedfolder ) strcpy ( * aiDefaultPath , lSelectedfolder ) ;
+ else strcpy ( * aiDefaultPath , "NULL" ) ;
+}
+
+
+void tfd_colorChooser(
+ char const * aTitle ,
+ char * * aiDefaultHexRGB )
+{
+ unsigned char const aDefaultRGB [ 3 ] = {128,128,128} ;
+ unsigned char aoResultRGB [ 3 ] = {128,128,128} ;
+ char * lChosenColor ;
+ lChosenColor = tinyfd_colorChooser( aTitle, * aiDefaultHexRGB, aDefaultRGB, aoResultRGB ) ;
+ if ( lChosenColor ) strcpy ( * aiDefaultHexRGB , lChosenColor ) ;
+ else strcpy ( * aiDefaultHexRGB , "NULL" ) ;
+}
+
+/* end of Modified prototypes for R */
+
+
+
+/*
+int main( int argc , char * argv[] )
+{
+char const * lTmp;
+char const * lTheSaveFileName;
+char const * lTheOpenFileName;
+char const * lTheSelectFolderName;
+char const * lTheHexColor;
+char const * lWillBeGraphicMode;
+unsigned char lRgbColor[3];
+FILE * lIn;
+char lBuffer[1024];
+char lString[1024];
+char const * lFilterPatterns[2] = { "*.txt", "*.text" };
+
+tinyfd_verbose = argc - 1;
+tinyfd_silent = 1;
+
+lWillBeGraphicMode = tinyfd_inputBox("tinyfd_query", NULL, NULL);
+
+strcpy(lBuffer, "v");
+strcat(lBuffer, tinyfd_version);
+if (lWillBeGraphicMode)
+{
+ strcat(lBuffer, "\ngraphic mode: ");
+}
+else
+{
+ strcat(lBuffer, "\nconsole mode: ");
+}
+strcat(lBuffer, tinyfd_response);
+strcat(lBuffer, "\n");
+strcat(lBuffer, tinyfd_needs+78);
+strcpy(lString, "tinyfiledialogs");
+tinyfd_messageBox(lString, lBuffer, "ok", "info", 0);
+
+tinyfd_notifyPopup("the title", "the message\n\tfrom outer-space", "info");
+
+if (lWillBeGraphicMode && !tinyfd_forceConsole)
+{
+ tinyfd_forceConsole = ! tinyfd_messageBox("Hello World",
+ "graphic dialogs [yes] / console mode [no]?",
+ "yesno", "question", 1);
+}
+
+lTmp = tinyfd_inputBox(
+ "a password box", "your password will be revealed", NULL);
+
+if (!lTmp) return 1;
+
+strcpy(lString, lTmp);
+
+lTheSaveFileName = tinyfd_saveFileDialog(
+ "let us save this password",
+ "passwordFile.txt",
+ 2,
+ lFilterPatterns,
+ NULL);
+
+if (!lTheSaveFileName)
+{
+ tinyfd_messageBox(
+ "Error",
+ "Save file name is NULL",
+ "ok",
+ "error",
+ 1);
+ return 1;
+}
+
+lIn = fopen(lTheSaveFileName, "w");
+if (!lIn)
+{
+ tinyfd_messageBox(
+ "Error",
+ "Can not open this file in write mode",
+ "ok",
+ "error",
+ 1);
+ return 1;
+}
+fputs(lString, lIn);
+fclose(lIn);
+
+lTheOpenFileName = tinyfd_openFileDialog(
+ "let us read the password back",
+ "",
+ 2,
+ lFilterPatterns,
+ NULL,
+ 0);
+
+if (!lTheOpenFileName)
+{
+ tinyfd_messageBox(
+ "Error",
+ "Open file name is NULL",
+ "ok",
+ "error",
+ 1);
+ return 1;
+}
+
+lIn = fopen(lTheOpenFileName, "r");
+
+if (!lIn)
+{
+ tinyfd_messageBox(
+ "Error",
+ "Can not open this file in read mode",
+ "ok",
+ "error",
+ 1);
+ return(1);
+}
+lBuffer[0] = '\0';
+fgets(lBuffer, sizeof(lBuffer), lIn);
+fclose(lIn);
+
+tinyfd_messageBox("your password is",
+ lBuffer, "ok", "info", 1);
+
+lTheSelectFolderName = tinyfd_selectFolderDialog(
+ "let us just select a directory", NULL);
+
+if (!lTheSelectFolderName)
+{
+ tinyfd_messageBox(
+ "Error",
+ "Select folder name is NULL",
+ "ok",
+ "error",
+ 1);
+ return 1;
+}
+
+tinyfd_messageBox("The selected folder is",
+ lTheSelectFolderName, "ok", "info", 1);
+
+lTheHexColor = tinyfd_colorChooser(
+ "choose a nice color",
+ "#FF0077",
+ lRgbColor,
+ lRgbColor);
+
+if (!lTheHexColor)
+{
+ tinyfd_messageBox(
+ "Error",
+ "hexcolor is NULL",
+ "ok",
+ "error",
+ 1);
+ return 1;
+}
+
+tinyfd_messageBox("The selected hexcolor is",
+ lTheHexColor, "ok", "info", 1);
+
+ tinyfd_beep();
+
+ return 0;
+}
+*/
+
+#ifdef _MSC_VER
+#pragma warning(default:4996)
+#pragma warning(default:4100)
+#pragma warning(default:4706)
+#endif
diff --git a/ext/tinyfiledialogs/tinyfiledialogs.h b/ext/tinyfiledialogs/tinyfiledialogs.h
new file mode 100644
index 0000000..60bdf13
--- /dev/null
+++ b/ext/tinyfiledialogs/tinyfiledialogs.h
@@ -0,0 +1,314 @@
+/* SPDX-License-Identifier: Zlib
+Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
+ ____________________________________________________________________
+ | |
+ | 100% compatible C C++ -> You can rename tinfiledialogs.c as .cpp |
+ |____________________________________________________________________|
+
+********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
+ _________
+ / \ tinyfiledialogs.h v3.21.3 [Feb 12, 2026]
+ |tiny file| Unique header file created [November 9, 2014]
+ | dialogs |
+ \____ ___/ http://tinyfiledialogs.sourceforge.net
+ \| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+ ____________________________________________
+| |
+| email: tinyfiledialogs at ysengrin.com |
+|____________________________________________|
+ ________________________________________________________________________________
+| ____________________________________________________________________________ |
+| | | |
+| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
+| | | |
+| | on windows: | |
+| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
+| | | |
+| | - _wfopen() requires wchar_t | |
+| | - fopen() uses char but expects ASCII or MBCS (not UTF-8) | |
+| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
+| | | |
+| | - alternatively, tinyfiledialogs provides | |
+| | functions to convert between UTF-8, UTF-16 and MBCS | |
+| |____________________________________________________________________________| |
+|________________________________________________________________________________|
+
+If you like tinyfiledialogs, please upvote my stackoverflow answer
+https://stackoverflow.com/a/47651444
+
+- License -
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+ __________________________________________
+ | ______________________________________ |
+ | | | |
+ | | DO NOT USE USER INPUT IN THE DIALOGS | |
+ | |______________________________________| |
+ |__________________________________________|
+*/
+
+#ifndef TINYFILEDIALOGS_H
+#define TINYFILEDIALOGS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/******************************************************************************************************/
+/**************************************** UTF-8 on Windows ********************************************/
+/******************************************************************************************************/
+#ifdef _WIN32
+/* On windows, if you want to use UTF-8 ( instead of the UTF-16/wchar_t functions at the end of this file )
+Make sure your code is really prepared for UTF-8 (on windows, functions like fopen() expect MBCS and not UTF-8) */
+extern int tinyfd_winUtf8; /* on windows char strings can be 1:UTF-8(default) or 0:MBCS */
+/* for MBCS change this to 0, in tinyfiledialogs.c or in your code */
+
+/* Here are some functions to help you convert between UTF-16 UTF-8 MBSC */
+char * tinyfd_utf8toMbcs(char const * aUtf8string);
+char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string);
+wchar_t * tinyfd_mbcsTo16(char const * aMbcsString);
+char * tinyfd_mbcsTo8(char const * aMbcsString);
+wchar_t * tinyfd_utf8to16(char const * aUtf8string);
+char * tinyfd_utf16to8(wchar_t const * aUtf16string);
+#endif
+/******************************************************************************************************/
+/******************************************************************************************************/
+/******************************************************************************************************/
+
+/************* 3 funtions for C# (you don't need this in C or C++) : */
+char const * tinyfd_getGlobalChar(char const * aCharVariableName); /* returns NULL on error */
+int tinyfd_getGlobalInt(char const * aIntVariableName); /* returns -1 on error */
+int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue); /* returns -1 on error */
+/* aCharVariableName: "tinyfd_version" "tinyfd_needs" "tinyfd_response"
+ aIntVariableName : "tinyfd_verbose" "tinyfd_silent" "tinyfd_allowCursesDialogs"
+ "tinyfd_forceConsole" "tinyfd_assumeGraphicDisplay" "tinyfd_winUtf8"
+**************/
+
+extern char tinyfd_version[8]; /* contains tinyfd current version number */
+extern char tinyfd_needs[]; /* info about requirements */
+extern int tinyfd_verbose; /* 0 (default) or 1 : on unix, prints the command line calls */
+extern int tinyfd_silent; /* 1 (default) or 0 : on unix, hide errors and warnings from called dialogs */
+
+/** Curses dialogs are difficult to use and counter-intuitive.
+On windows they are only ascii and still uses the unix backslash ! **/
+extern int tinyfd_allowCursesDialogs; /* 0 (default) or 1 */
+
+extern int tinyfd_forceConsole; /* 0 (default) or 1 */
+/* for unix & windows: 0 (graphic mode) or 1 (console mode).
+0: try to use a graphic solution, if it fails then it uses console mode.
+1: forces all dialogs into console mode even when an X server is present.
+ if enabled, it can use the package Dialog or dialog.exe.
+ on windows it only make sense for console applications */
+
+/* extern int tinyfd_assumeGraphicDisplay; */ /* 0 (default) or 1 */
+/* some systems don't set the environment variable DISPLAY even when a graphic display is present.
+set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display */
+
+extern char tinyfd_response[1024];
+/* if you pass "tinyfd_query" as aTitle,
+the functions will not display the dialogs
+but will return 0 for console mode, 1 for graphic mode.
+tinyfd_response is then filled with the retain solution.
+possible values for tinyfd_response are (all lowercase)
+for graphic mode:
+ windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
+ shellementary qarma shanty boxer python2-tkinter python3-tkinter python-dbus
+ perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
+for console mode:
+ dialog whiptail basicinput no_solution */
+
+void tinyfd_beep(void);
+
+int tinyfd_notifyPopup(
+ char const * aTitle, /* NULL or "" */
+ char const * aMessage, /* NULL or "" may contain \n \t */
+ char const * aIconType); /* "info" "warning" "error" */
+ /* return has only meaning for tinyfd_query */
+
+int tinyfd_messageBox(
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" may contain \n \t */
+ char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */
+ char const * aIconType , /* "info" "warning" "error" "question" */
+ int aDefaultButton ) ;
+ /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
+
+char * tinyfd_inputBox(
+ char const * aTitle , /* NULL or "" */
+ char const * aMessage , /* NULL or "" (\n and \t have no effect) */
+ char const * aDefaultInput ) ; /* NULL = passwordBox, "" = inputbox */
+ /* returns NULL on cancel */
+
+char * tinyfd_saveFileDialog(
+ char const * aTitle , /* NULL or "" */
+ char const * aDefaultPathAndOrFile , /* NULL or "" , ends with / to set only a directory */
+ int aNumOfFilterPatterns , /* 0 (1 in the following example) */
+ char const * const * aFilterPatterns , /* NULL or char const * lFilterPatterns[1]={"*.txt"} */
+ char const * aSingleFilterDescription ) ; /* NULL or "text files" */
+ /* returns NULL on cancel */
+
+char * tinyfd_openFileDialog(
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultPathAndOrFile, /* NULL or "" , ends with / to set only a directory */
+ int aNumOfFilterPatterns , /* 0 (2 in the following example) */
+ char const * const * aFilterPatterns, /* NULL or char const * lFilterPatterns[2]={"*.png","*.jpg"}; */
+ char const * aSingleFilterDescription, /* NULL or "image files" */
+ int aAllowMultipleSelects ) ; /* 0 or 1 */
+ /* in case of multiple files, the separator is | */
+ /* returns NULL on cancel */
+
+char * tinyfd_selectFolderDialog(
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultPath); /* NULL or "" */
+ /* returns NULL on cancel */
+
+char * tinyfd_colorChooser(
+ char const * aTitle, /* NULL or "" */
+ char const * aDefaultHexRGB, /* NULL or "" or "#FF0000" */
+ unsigned char const aDefaultRGB[3] , /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
+ unsigned char aoResultRGB[3] ) ; /* unsigned char lResultRGB[3]; */
+ /* aDefaultRGB is used only if aDefaultHexRGB is absent */
+ /* aDefaultRGB and aoResultRGB can be the same array */
+ /* returns NULL on cancel */
+ /* returns the hexcolor as a string "#FF0000" */
+ /* aoResultRGB also contains the result */
+
+
+/************ WINDOWS ONLY SECTION ************************/
+#ifdef _WIN32
+
+/* windows only - utf-16 version */
+int tinyfd_notifyPopupW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
+ wchar_t const * aIconType); /* L"info" L"warning" L"error" */
+
+/* windows only - utf-16 version */
+int tinyfd_messageBoxW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
+ wchar_t const * aDialogType, /* L"ok" L"okcancel" L"yesno" */
+ wchar_t const * aIconType, /* L"info" L"warning" L"error" L"question" */
+ int aDefaultButton ); /* 0 for cancel/no , 1 for ok/yes */
+ /* returns 0 for cancel/no , 1 for ok/yes */
+
+/* windows only - utf-16 version */
+wchar_t * tinyfd_inputBoxW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aMessage, /* NULL or L"" (\n nor \t not respected) */
+ wchar_t const * aDefaultInput); /* NULL passwordBox, L"" inputbox */
+
+/* windows only - utf-16 version */
+wchar_t * tinyfd_saveFileDialogW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
+ int aNumOfFilterPatterns, /* 0 (1 in the following example) */
+ wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[1]={L"*.txt"} */
+ wchar_t const * aSingleFilterDescription); /* NULL or L"text files" */
+ /* returns NULL on cancel */
+
+/* windows only - utf-16 version */
+wchar_t * tinyfd_openFileDialogW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
+ int aNumOfFilterPatterns , /* 0 (2 in the following example) */
+ wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[2]={L"*.png","*.jpg"} */
+ wchar_t const * aSingleFilterDescription, /* NULL or L"image files" */
+ int aAllowMultipleSelects ) ; /* 0 or 1 */
+ /* in case of multiple files, the separator is | */
+ /* returns NULL on cancel */
+
+/* windows only - utf-16 version */
+wchar_t * tinyfd_selectFolderDialogW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aDefaultPath); /* NULL or L"" */
+ /* returns NULL on cancel */
+
+/* windows only - utf-16 version */
+wchar_t * tinyfd_colorChooserW(
+ wchar_t const * aTitle, /* NULL or L"" */
+ wchar_t const * aDefaultHexRGB, /* NULL or L"#FF0000" */
+ unsigned char const aDefaultRGB[3], /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
+ unsigned char aoResultRGB[3]); /* unsigned char lResultRGB[3]; */
+ /* returns the hexcolor as a string L"#FF0000" */
+ /* aoResultRGB also contains the result */
+ /* aDefaultRGB is used only if aDefaultHexRGB is NULL */
+ /* aDefaultRGB and aoResultRGB can be the same array */
+ /* returns NULL on cancel */
+
+#endif /*_WIN32 */
+
+#ifdef __cplusplus
+} /*extern "C"*/
+#endif
+
+#endif /* TINYFILEDIALOGS_H */
+
+/*
+ ________________________________________________________________________________
+| ____________________________________________________________________________ |
+| | | |
+| | on windows: | |
+| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
+| | - _wfopen() requires wchar_t | |
+| | | |
+| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
+| | - but fopen() expects MBCS (not UTF-8) | |
+| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
+| | | |
+| | - alternatively, tinyfiledialogs provides | |
+| | functions to convert between UTF-8, UTF-16 and MBCS | |
+| |____________________________________________________________________________| |
+|________________________________________________________________________________|
+
+- This is not for ios nor android (it works in termux though).
+- The files can be renamed with extension ".cpp" as the code is 100% compatible C C++
+ (just comment out << extern "C" >> in the header file)
+- Windows is fully supported from XP to 10 (maybe even older versions)
+- C# & LUA via dll, see files in the folder EXTRAS
+- OSX supported from 10.4 to latest (maybe even older versions)
+- Do not use " and ' as the dialogs will be displayed with a warning
+ instead of the title, message, etc...
+- There's one file filter only, it may contain several patterns.
+- If no filter description is provided,
+ the list of patterns will become the description.
+- On windows link against Comdlg32.lib and Ole32.lib
+ (on windows the no linking claim is a lie)
+- On unix: it tries command line calls, so no such need (NO LINKING).
+- On unix you need one of the following:
+ applescript, kdialog, zenity, matedialog, shellementary, qarma, shanty, boxer,
+ yad, python (2 or 3)/tkinter/python-dbus (optional), Xdialog
+ or curses dialogs (opens terminal if running without console).
+- One of those is already included on most (if not all) desktops.
+- In the absence of those it will use gdialog, gxmessage or whiptail
+ with a textinputbox. If nothing is found, it switches to basic console input,
+ it opens a console if needed (requires xterm + bash).
+- for curses dialogs you must set tinyfd_allowCursesDialogs=1
+- You can query the type of dialog that will be used (pass "tinyfd_query" as aTitle)
+- String memory is preallocated statically for all the returned values.
+- File and path names are tested before return, they should be valid.
+- tinyfd_forceConsole=1; at run time, forces dialogs into console mode.
+- On windows, console mode only make sense for console applications.
+- On windows, console mode is not implemented for wchar_T UTF-16.
+- Mutiple selects are not possible in console mode.
+- The package dialog must be installed to run in curses dialogs in console mode.
+ It is already installed on most unix systems.
+- On osx, the package dialog can be installed via
+ http://macappstore.org/dialog or http://macports.org
+- On windows, for curses dialogs console mode,
+ dialog.exe should be copied somewhere on your executable path.
+ It can be found at the bottom of the following page:
+ http://andrear.altervista.org/home/cdialog.php
+*/
diff --git a/game.c b/game.c
new file mode 100644
index 0000000..53b68cf
--- /dev/null
+++ b/game.c
@@ -0,0 +1,46 @@
+#include "game.h"
+
+GameData game = {0};
+
+void
+game_init(void) {
+ game.time.secs = 12 * GAME_SECS_PER_HOUR;
+ game.time.day_len = 600;
+ game.time.scale = 5;
+ game.paused = false;
+}
+
+void
+game_exit(void) {
+}
+
+void
+game_update(void) {
+ if (game.paused || game.time.day_len <= 0) {
+ return;
+ }
+
+ f64 rate = (f64)GAME_SECS_PER_DAY / game.time.day_len;
+ game.time.secs += GetFrameTime() * rate * game.time.scale;
+}
+
+void
+game_time_set(u32 hour, u32 min, f64 sec) {
+ hour = MIN(hour, 24);
+ min = MIN(min, 60);
+ sec = CLAMP(sec, 0.0, 60.0);
+}
+
+f64
+game_time_of_day(void) {
+ f64 time = fmod(game.time.secs, (f64)GAME_SECS_PER_DAY);
+ if (time < 0.0) {
+ time += GAME_SECS_PER_DAY;
+ }
+ return time;
+}
+
+u32
+game_time_day(void) {
+ return (u32)floor(game.time.secs / GAME_SECS_PER_DAY);
+}
diff --git a/game.h b/game.h
new file mode 100644
index 0000000..03aa95e
--- /dev/null
+++ b/game.h
@@ -0,0 +1,31 @@
+#ifndef GAME_H
+#define GAME_H
+
+#include "common.h"
+
+enum {
+ GAME_SECS_PER_MIN = 60,
+ GAME_SECS_PER_HOUR = 60 * GAME_SECS_PER_MIN,
+ GAME_SECS_PER_DAY = 24 * GAME_SECS_PER_HOUR,
+};
+
+typedef struct {
+ f64 secs, day_len, scale;
+} GameTime;
+
+typedef struct {
+ GameTime time;
+ bool paused;
+} GameData;
+
+extern GameData game;
+
+void game_init(void);
+void game_exit(void);
+void game_update(void);
+
+void game_time_set(u32 hour, u32 min, f64 sec);
+f64 game_time_of_day(void);
+u32 game_time_day(void);
+
+#endif
diff --git a/physics.c b/physics.c
new file mode 100644
index 0000000..53a895d
--- /dev/null
+++ b/physics.c
@@ -0,0 +1,40 @@
+#include "physics.h"
+
+PhysicsInfo physics = {0};
+
+void
+physics_init(void) {
+ physics.world_def = b3DefaultWorldDef();
+ physics.world_id = b3CreateWorld(&physics.world_def);
+ physics.simulate = false;
+}
+
+void
+physics_exit(void) {
+ if (b3World_IsValid(physics.world_id)) {
+ b3DestroyWorld(physics.world_id);
+ }
+
+ physics.world_id = b3_nullWorldId;
+}
+
+void
+physics_update(void) {
+ if (!physics.simulate) {
+ return;
+ }
+
+ // TODO: timestep is temporary, should be global value
+ b3World_Step(physics.world_id, 1.0f / 60.0f, 4);
+}
+
+// PhysicsObject
+// physics_obj_create_def(void) {
+// PhysicsObject obj = {0};
+// return obj;
+// }
+
+// void
+// physics_obj_delete(PhysicsObject* phys) {
+
+// }
diff --git a/physics.h b/physics.h
new file mode 100644
index 0000000..0a92118
--- /dev/null
+++ b/physics.h
@@ -0,0 +1,156 @@
+#ifndef PHYSICS_H
+#define PHYSICS_H
+
+#include "common.h"
+
+typedef enum {
+ PHYS_SHAPE_BOX,
+ PHYS_SHAPE_SPHERE,
+ PHYS_SHAPE_CAPSULE,
+ PHYS_SHAPE_CYLINDER,
+ PHYS_SHAPE_CONVEX_HULL,
+ PHYS_SHAPE_STATIC_TRIMESH,
+} PhysicsShapeTag;
+
+typedef enum {
+ PHYS_JOINT_SPHERICAL,
+ PHYS_JOINT_REVOLUTE,
+ PHYS_JOINT_PRISMATIC,
+ PHYS_JOINT_WELD,
+ PHYS_JOINT_WHEEL,
+ PHYS_JOINT_DISTANCE,
+} PhysicsJointTag;
+
+typedef enum {
+ PHYS_JOINT_LIMIT,
+ PHYS_JOINT_SPRING,
+ PHYS_JOINT_MOTOR,
+} PhysicsJointFlags;
+
+typedef enum {
+ PHYS_MODE_STATIC,
+ PHYS_MODE_KINEMATIC,
+ PHYS_MODE_DYNAMIC,
+} PhysicsMode;
+
+typedef enum {
+ PHYS_MASK_DEFAULT,
+ PHYS_MASK_WORLD,
+ PHYS_MASK_PROP,
+ PHYS_MASK_PLAYER,
+ PHYS_MASK_PARTICLE,
+
+ PHYS_MASK_EDITOR_SELECTABLE,
+ PHYS_MASK_EDITOR_RAYCAST,
+} PhysicsMask;
+
+typedef struct {
+ PhysicsShapeTag tag;
+ union {
+ struct {
+ Vec3 sz;
+ } Box;
+
+ struct {
+ f32 radius;
+ } Sphere;
+
+ struct {
+ f32 height, radius;
+ } Capsule, Cylinder;
+
+ struct {
+ Vec3 *points;
+ u32 point_cnt;
+ } ConvexHull;
+
+ struct {
+ Vec3 *points;
+ u32 *indices;
+ u32 point_cnt, idx_cnt;
+ } TriMesh;
+ };
+
+ /* local to body */
+ Vec3 pos;
+ Quaternion rot;
+
+ f32 density, friction, restitution;
+ Flags32 category_bits, mask_bits;
+} PhysicsShape;
+
+typedef struct {
+ PhysicsShape *shapes;
+ u32 shape_cnt;
+
+ /* local to entity */
+ Vec3 pos;
+ Quaternion rot;
+
+ f32 linear_damping, angular_damping;
+ f32 gravity_scale;
+ PhysicsMode mode;
+ bool enable_sleep;
+} PhysicsBody;
+
+/*
+ physics joint fields meaning changes depending on their context.
+
+ anchor_a/axis_a are local to body_a.
+ anchor_b/axis_b are local to body_b.
+ axes should be normalized.
+
+ lower/upper are generic limits
+ - revolute: radians
+ - prismatic: metres
+ - distance: metres
+ - wheel: suspension travel
+
+ springs:
+ - spring_hz: frequency of the spring response; higher = stiffer/faster
+ - spring_damping: damping ratio; controls oscillation
+
+ motors:
+ revolute:
+ motor_speed = angular velocity
+ motor_force = maximum torque
+
+ prismatic:
+ motor_speed = linear velocity
+ motor_force = maximum force
+
+ wheel:
+ motor_speed = wheel angular velocity
+ motor_force = maximum drive/braking torque
+*/
+typedef struct {
+ PhysicsJointTag tag;
+ u32 body_a, body_b;
+ Flags32 flags;
+
+ Vec3 anchor_a, anchor_b;
+ Vec3 axis_a, axis_b;
+
+ f32 lower, upper;
+ f32 spring_hz, spring_damping;
+ f32 motor_speed, motor_force;
+} PhysicsJoint;
+
+typedef struct {
+ b3BodyId id;
+ b3ShapeId *shapes; // count is stored in PhysicsCmp
+} PhysicsBodyRuntime;
+
+typedef struct {
+ b3WorldDef world_def;
+ b3WorldId world_id;
+ bool simulate;
+} PhysicsInfo;
+
+extern PhysicsInfo physics;
+
+void physics_init(void);
+void physics_exit(void);
+void physics_update(void);
+
+#endif
diff --git a/raygui.h b/raygui.h
new file mode 100644
index 0000000..f66bd51
--- /dev/null
+++ b/raygui.h
@@ -0,0 +1,6319 @@
+/*******************************************************************************************
+*
+* raygui v5.0 - A simple and easy-to-use immediate-mode gui library
+*
+* DESCRIPTION:
+* raygui is a tools-dev-focused immediate-mode-gui library based on raylib but also
+* available as a standalone library, as long as input and drawing functions are provided
+*
+* FEATURES:
+* - Immediate-mode gui, minimal retained data
+* - +25 controls provided (basic and advanced)
+* - Styling system for colors, font and metrics
+* - Icons supported, embedded as a 1-bit icons pack
+* - Standalone mode option (custom input/graphics backend)
+* - Multiple support tools provided for raygui development
+*
+* POSSIBLE IMPROVEMENTS:
+* - Better standalone mode API for easy plug of custom backends
+* - Externalize required inputs, allow user easier customization
+*
+* LIMITATIONS:
+* - No editable multi-line word-wraped text box supported
+* - No auto-layout mechanism, up to the user to define controls position and size
+* - Standalone mode requires library modification and some user work to plug another backend
+*
+* NOTES:
+* - WARNING: GuiLoadStyle() and GuiLoadStyle{Custom}() functions, allocate memory for
+* font atlas recs and glyphs, freeing that memory is (usually) up to the user,
+* no unload function is explicitly provided... but note that GuiLoadStyleDefault() unloads
+* by default any previously loaded font (texture, recs, glyphs)
+* - Global UI alpha (guiAlpha) is applied inside GuiDrawRectangle() and GuiDrawText() functions
+*
+* CONTROLS PROVIDED:
+* # Container/separators Controls
+* - WindowBox --> StatusBar, Panel
+* - GroupBox --> Line
+* - Line
+* - Panel --> StatusBar
+* - ScrollPanel --> StatusBar
+* - TabBar --> Button
+*
+* # Basic Controls
+* - Label
+* - LabelButton --> Label
+* - Button
+* - Toggle
+* - ToggleGroup --> Toggle
+* - ToggleSlider
+* - CheckBox
+* - ComboBox
+* - DropdownBox
+* - TextBox
+* - ValueBox --> TextBox
+* - Spinner --> Button, ValueBox
+* - Slider
+* - SliderBar --> Slider
+* - ProgressBar
+* - StatusBar
+* - DummyRec
+* - Grid
+*
+* # Advance Controls
+* - ListView
+* - ColorPicker --> ColorPanel, ColorBarHue
+* - MessageBox --> Window, Label, Button
+* - TextInputBox --> Window, Label, TextBox, Button
+*
+* It also provides a set of functions for styling the controls based on its properties (size, color)
+*
+*
+* RAYGUI STYLE (guiStyle):
+* raygui uses a global data array for all gui style properties (allocated on data segment by default),
+* when a new style is loaded, it is loaded over the global style... but a default gui style could always be
+* recovered with GuiLoadStyleDefault() function, that overwrites the current style to the default one
+*
+* The global style array size is fixed and depends on the number of controls and properties:
+*
+* static unsigned int guiStyle[RAYGUI_MAX_CONTROLS*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED)];
+*
+* guiStyle size is by default: 16*(16 + 8) = 384 int = 384*4 bytes = 1536 bytes = 1.5 KB
+*
+* Note that the first set of BASE properties (by default guiStyle[0..15]) belong to the generic style
+* used for all controls, when any of those base values is set, it is automatically populated to all
+* controls, so, specific control values overwriting generic style should be set after base values
+*
+* After the first BASE properties set, the EXTENDED properties set is defined (by default guiStyle[16..23]),
+* those properties are actually common to all controls and can not be overwritten individually (like BASE ones)
+* Some of those properties are: TEXT_SIZE, TEXT_SPACING, LINE_COLOR, BACKGROUND_COLOR
+*
+* Custom control properties can be defined using the EXTENDED properties for each independent control.
+*
+* TOOL: rGuiStyler is a visual tool to customize raygui style: github.com/raysan5/rguistyler
+*
+*
+* RAYGUI ICONS (guiIcons):
+* raygui could use a global array containing icons data (allocated on data segment by default),
+* a custom icons set could be loaded over this array using GuiLoadIcons(), but loaded icons set
+* must be same RAYGUI_ICON_SIZE and no more than RAYGUI_ICON_MAX_ICONS will be loaded
+*
+* Every icon is codified in binary form, using 1 bit per pixel, so, every 16x16 icon
+* requires 8 integers (16*16/32) to be stored in memory.
+*
+* When the icon is draw, actually one quad per pixel is drawn if the bit for that pixel is set
+*
+* The global icons array size is fixed and depends on the number of icons and size:
+*
+* static unsigned int guiIcons[RAYGUI_ICON_MAX_ICONS*RAYGUI_ICON_DATA_ELEMENTS];
+*
+* guiIcons size is by default: 256*(16*16/32) = 2048*4 = 8192 bytes = 8 KB
+*
+* TOOL: rGuiIcons is a visual tool to customize/create raygui icons: github.com/raysan5/rguiicons
+*
+* RAYGUI LAYOUT:
+* raygui currently does not provide an auto-layout mechanism like other libraries,
+* layouts must be defined manually on controls drawing, providing the right bounds Rectangle for it
+*
+* TOOL: rGuiLayout is a visual tool to create raygui layouts: github.com/raysan5/rguilayout
+*
+* CONFIGURATION:
+* #define RAYGUI_IMPLEMENTATION
+* Generates the implementation of the library into the included file
+* If not defined, the library is in header only mode and can be included in other headers
+* or source files without problems. But only ONE file should hold the implementation
+*
+* #define RAYGUI_STANDALONE
+* Avoid raylib.h header inclusion in this file. Data types defined on raylib are defined
+* internally in the library and input management and drawing functions must be provided by
+* the user (check library implementation for further details)
+*
+* #define RAYGUI_NO_ICONS
+* Avoid including embedded ricons data (256 icons, 16x16 pixels, 1-bit per pixel, 2KB)
+*
+* #define RAYGUI_CUSTOM_ICONS
+* Includes custom ricons.h header defining a set of custom icons,
+* this file can be generated using rGuiIcons tool
+*
+* #define RAYGUI_FONT_ICONS_BAKING
+* On gui font loading from style file, append the icons to font atlas image, so,
+* icons can be drawn along the text as a texture, instead of using shapes to draw them
+*
+* #define RAYGUI_DEBUG_RECS_BOUNDS
+* Draw control bounds rectangles for debug
+*
+* #define RAYGUI_DEBUG_TEXT_BOUNDS
+* Draw text bounds rectangles for debug
+*
+* VERSIONS HISTORY:
+* 5.0 (xx-Jun-2026) ADDED: TABBAR control: GuiTabBar()
+* ADDED: Support up to 512 icons (v500)
+* ADDED: Support icons baking into font atlas image
+* ADDED: guiControlExclusiveMode and guiControlExclusiveRec for exclusive modes
+* ADDED: Altrnative VALUEBOX: GuiValueBoxFloat()
+* ADDED: GuiDropdonwBox() properties: DROPDOWN_ARROW_HIDDEN, DROPDOWN_ROLL_UP
+* ADDED: GuiListView() property: LIST_ITEMS_BORDER_WIDTH
+* ADDED: GuiLoadIconsFromMemory(), used by GuiLoadIcons()
+* ADDED: Macros for inputs customization, raylib decoupling
+* REMOVED: GuiSpinner() from controls list, using BUTTON + VALUEBOX properties
+* REMOVED: GuiSliderPro(), functionality was redundant
+* REMOVED: TextSplit() raylib function requirement on RAYGUI_STANDALONE
+* REVIEWED: GuiLoadIconsFromMemory(), fixed memory issues
+* REVIEWED: Controls using text labels to use LABEL properties
+* REVIEWED: Replaced sprintf() by snprintf() for more safety
+* REVIEWED: GuiTabBar(), close tab with mouse middle button
+* REVIEWED: GuiScrollPanel(), scroll speed proportional to content
+* REVIEWED: GuiDropdownBox(), support roll up and hidden arrow
+* REVIEWED: GuiTextBox(), cursor position initialization
+* REVIEWED: GuiSliderPro(), control value change check
+* REVIEWED: GuiGrid(), simplified implementation
+* REVIEWED: GuiIconText(), increase buffer size and reviewed padding
+* REVIEWED: GuiDrawText(), improved wrap mode drawing
+* REVIEWED: GuiScrollBar(), minor tweaks
+* REVIEWED: GuiProgressBar(), improved borders computing
+* REVIEWED: GuiTextBox(), multiple improvements: autocursor and more
+* REVIEWED: Functions descriptions, removed wrong return value reference
+* REDESIGNED: GuiLoadStyleFromMemory() to support icons baking into font atlas
+* REDESIGNED: GuiToggleGroup() to process rows/cols with no need for GuiTextSplit()
+* REDESIGNED: GuiColorPanel(), improved HSV <-> RGBA convertion
+* REDESIGNED: WARNING: TEXT_LINE_SPACING does not consider text height, only lines spacing
+*
+* 4.0 (12-Sep-2023) ADDED: GuiToggleSlider()
+* ADDED: GuiColorPickerHSV() and GuiColorPanelHSV()
+* ADDED: Multiple new icons, mostly compiler related
+* ADDED: New DEFAULT properties: TEXT_LINE_SPACING, TEXT_ALIGNMENT_VERTICAL, TEXT_WRAP_MODE
+* ADDED: New enum values: GuiTextAlignment, GuiTextAlignmentVertical, GuiTextWrapMode
+* ADDED: Support loading styles with custom font charset from external file
+* REDESIGNED: GuiTextBox(), support mouse cursor positioning
+* REDESIGNED: GuiDrawText(), support multiline and word-wrap modes (read only)
+* REDESIGNED: GuiProgressBar() to be more visual, progress affects border color
+* REDESIGNED: Global alpha consideration moved to GuiDrawRectangle() and GuiDrawText()
+* REDESIGNED: GuiScrollPanel(), get parameters by reference and return result value
+* REDESIGNED: GuiToggleGroup(), get parameters by reference and return result value
+* REDESIGNED: GuiComboBox(), get parameters by reference and return result value
+* REDESIGNED: GuiCheckBox(), get parameters by reference and return result value
+* REDESIGNED: GuiSlider(), get parameters by reference and return result value
+* REDESIGNED: GuiSliderBar(), get parameters by reference and return result value
+* REDESIGNED: GuiProgressBar(), get parameters by reference and return result value
+* REDESIGNED: GuiListView(), get parameters by reference and return result value
+* REDESIGNED: GuiColorPicker(), get parameters by reference and return result value
+* REDESIGNED: GuiColorPanel(), get parameters by reference and return result value
+* REDESIGNED: GuiColorBarAlpha(), get parameters by reference and return result value
+* REDESIGNED: GuiColorBarHue(), get parameters by reference and return result value
+* REDESIGNED: GuiGrid(), get parameters by reference and return result value
+* REDESIGNED: GuiGrid(), added extra parameter
+* REDESIGNED: GuiListViewEx(), change parameters order
+* REDESIGNED: All controls return result as int value
+* REVIEWED: GuiScrollPanel() to avoid smallish scroll-bars
+* REVIEWED: All examples and specially controls_test_suite
+* RENAMED: gui_file_dialog module to gui_window_file_dialog
+* UPDATED: All styles to include ISO-8859-15 charset (as much as possible)
+*
+* 3.6 (10-May-2023) ADDED: New icon: SAND_TIMER
+* ADDED: GuiLoadStyleFromMemory() (binary only)
+* REVIEWED: GuiScrollBar() horizontal movement key
+* REVIEWED: GuiTextBox() crash on cursor movement
+* REVIEWED: GuiTextBox(), additional inputs support
+* REVIEWED: GuiLabelButton(), avoid text cut
+* REVIEWED: GuiTextInputBox(), password input
+* REVIEWED: Local GetCodepointNext(), aligned with raylib
+* REDESIGNED: GuiSlider*()/GuiScrollBar() to support out-of-bounds
+*
+* 3.5 (20-Apr-2023) ADDED: GuiTabBar(), based on GuiToggle()
+* ADDED: Helper functions to split text in separate lines
+* ADDED: Multiple new icons, useful for code editing tools
+* REMOVED: Unneeded icon editing functions
+* REMOVED: GuiTextBoxMulti(), very limited and broken
+* REMOVED: MeasureTextEx() dependency, logic directly implemented
+* REMOVED: DrawTextEx() dependency, logic directly implemented
+* REVIEWED: GuiScrollBar(), improve mouse-click behaviour
+* REVIEWED: Library header info, more info, better organized
+* REDESIGNED: GuiTextBox() to support cursor movement
+* REDESIGNED: GuiDrawText() to divide drawing by lines
+*
+* 3.2 (22-May-2022) RENAMED: Some enum values, for unification, avoiding prefixes
+* REMOVED: GuiScrollBar(), only internal
+* REDESIGNED: GuiPanel() to support text parameter
+* REDESIGNED: GuiScrollPanel() to support text parameter
+* REDESIGNED: GuiColorPicker() to support text parameter
+* REDESIGNED: GuiColorPanel() to support text parameter
+* REDESIGNED: GuiColorBarAlpha() to support text parameter
+* REDESIGNED: GuiColorBarHue() to support text parameter
+* REDESIGNED: GuiTextInputBox() to support password
+*
+* 3.1 (12-Jan-2022) REVIEWED: Default style for consistency (aligned with rGuiLayout v2.5 tool)
+* REVIEWED: GuiLoadStyle() to support compressed font atlas image data and unload previous textures
+* REVIEWED: External icons usage logic
+* REVIEWED: GuiLine() for centered alignment when including text
+* RENAMED: Multiple controls properties definitions to prepend RAYGUI_
+* RENAMED: RICON_ references to RAYGUI_ICON_ for library consistency
+* Projects updated and multiple tweaks
+*
+* 3.0 (04-Nov-2021) Integrated ricons data to avoid external file
+* REDESIGNED: GuiTextBoxMulti()
+* REMOVED: GuiImageButton*()
+* Multiple minor tweaks and bugs corrected
+*
+* 2.9 (17-Mar-2021) REMOVED: Tooltip API
+* 2.8 (03-May-2020) Centralized rectangles drawing to GuiDrawRectangle()
+* 2.7 (20-Feb-2020) ADDED: Possible tooltips API
+* 2.6 (09-Sep-2019) ADDED: GuiTextInputBox()
+* REDESIGNED: GuiListView*(), GuiDropdownBox(), GuiSlider*(), GuiProgressBar(), GuiMessageBox()
+* REVIEWED: GuiTextBox(), GuiSpinner(), GuiValueBox(), GuiLoadStyle()
+* Replaced property INNER_PADDING by TEXT_PADDING, renamed some properties
+* ADDED: 8 new custom styles ready to use
+* Multiple minor tweaks and bugs corrected
+*
+* 2.5 (28-May-2019) Implemented extended GuiTextBox(), GuiValueBox(), GuiSpinner()
+* 2.3 (29-Apr-2019) ADDED: rIcons auxiliar library and support for it, multiple controls reviewed
+* Refactor all controls drawing mechanism to use control state
+* 2.2 (05-Feb-2019) ADDED: GuiScrollBar(), GuiScrollPanel(), reviewed GuiListView(), removed Gui*Ex() controls
+* 2.1 (26-Dec-2018) REDESIGNED: GuiCheckBox(), GuiComboBox(), GuiDropdownBox(), GuiToggleGroup() > Use combined text string
+* REDESIGNED: Style system (breaking change)
+* 2.0 (08-Nov-2018) ADDED: Support controls guiLock and custom fonts
+* REVIEWED: GuiComboBox(), GuiListView()...
+* 1.9 (09-Oct-2018) REVIEWED: GuiGrid(), GuiTextBox(), GuiTextBoxMulti(), GuiValueBox()...
+* 1.8 (01-May-2018) Lot of rework and redesign to align with rGuiStyler and rGuiLayout
+* 1.5 (21-Jun-2017) Working in an improved styles system
+* 1.4 (15-Jun-2017) Rewritten all GUI functions (removed useless ones)
+* 1.3 (12-Jun-2017) Complete redesign of style system
+* 1.1 (01-Jun-2017) Complete review of the library
+* 1.0 (07-Jun-2016) Converted to header-only by Ramon Santamaria
+* 0.9 (07-Mar-2016) Reviewed and tested by Albert Martos, Ian Eito, Sergio Martinez and Ramon Santamaria
+* 0.8 (27-Aug-2015) Initial release. Implemented by Kevin Gato, Daniel Nicolás and Ramon Santamaria
+*
+* DEPENDENCIES:
+* raylib 6.1-dev - Inputs reading (keyboard/mouse), shapes drawing, font loading and text drawing
+*
+* STANDALONE MODE:
+* By default raygui depends on raylib mostly for the inputs and the drawing functionality but that dependency can be disabled
+* with the config flag RAYGUI_STANDALONE. In that case is up to the user to provide another backend to cover library needs
+*
+* The following functions should be redefined for a custom backend:
+*
+* - Vector2 GetMousePosition(void);
+* - float GetMouseWheelMove(void);
+* - bool IsMouseButtonDown(int button);
+* - bool IsMouseButtonPressed(int button);
+* - bool IsMouseButtonReleased(int button);
+* - bool IsKeyDown(int key);
+* - bool IsKeyPressed(int key);
+* - int GetCharPressed(void); // -- GuiTextBox(), GuiValueBox()
+*
+* - void DrawRectangle(int x, int y, int width, int height, Color color); // -- GuiDrawRectangle()
+* - void DrawRectangleGradientEx(Rectangle rec, Color col1, Color col2, Color col3, Color col4); // -- GuiColorPicker()
+*
+* - Font GetFontDefault(void); // -- GuiLoadStyleDefault()
+* - Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount); // -- GuiLoadStyle()
+* - Texture2D LoadTextureFromImage(Image image); // -- GuiLoadStyle(), required to load texture from embedded font atlas image
+* - void SetShapesTexture(Texture2D tex, Rectangle rec); // -- GuiLoadStyle(), required to set shapes rec to font white rec (optimization)
+* - char *LoadFileText(const char *fileName); // -- GuiLoadStyle(), required to load charset data
+* - void UnloadFileText(char *text); // -- GuiLoadStyle(), required to unload charset data
+* - const char *GetDirectoryPath(const char *filePath); // -- GuiLoadStyle(), required to find charset/font file from text .rgs
+* - int *LoadCodepoints(const char *text, int *count); // -- GuiLoadStyle(), required to load required font codepoints list
+* - void UnloadCodepoints(int *codepoints); // -- GuiLoadStyle(), required to unload codepoints list
+* - unsigned char *DecompressData(const unsigned char *compData, int compDataSize, int *dataSize); // -- GuiLoadStyle()
+*
+* CONTRIBUTORS:
+* Ramon Santamaria: Supervision, review, redesign, update and maintenance
+* Vlad Adrian: Complete rewrite of GuiTextBox() to support extended features (2019)
+* Sergio Martinez: Review, testing (2015) and redesign of multiple controls (2018)
+* Adria Arranz: Testing and implementation of additional controls (2018)
+* Jordi Jorba: Testing and implementation of additional controls (2018)
+* Albert Martos: Review and testing of the library (2015)
+* Ian Eito: Review and testing of the library (2015)
+* Kevin Gato: Initial implementation of basic components (2014)
+* Daniel Nicolas: Initial implementation of basic components (2014)
+*
+*
+* LICENSE: zlib/libpng
+*
+* Copyright (c) 2014-2026 Ramon Santamaria (@raysan5)
+*
+* This software is provided "as-is", without any express or implied warranty. In no event
+* will the authors be held liable for any damages arising from the use of this software.
+*
+* Permission is granted to anyone to use this software for any purpose, including commercial
+* applications, and to alter it and redistribute it freely, subject to the following restrictions:
+*
+* 1. The origin of this software must not be misrepresented; you must not claim that you
+* wrote the original software. If you use this software in a product, an acknowledgment
+* in the product documentation would be appreciated but is not required.
+*
+* 2. Altered source versions must be plainly marked as such, and must not be misrepresented
+* as being the original software.
+*
+* 3. This notice may not be removed or altered from any source distribution.
+*
+**********************************************************************************************/
+
+#ifndef RAYGUI_H
+#define RAYGUI_H
+
+#define RAYGUI_VERSION_MAJOR 5
+#define RAYGUI_VERSION_MINOR 0
+#define RAYGUI_VERSION_PATCH 0
+#define RAYGUI_VERSION "5.0-dev"
+
+#if !defined(RAYGUI_STANDALONE)
+ #include "raylib.h"
+#endif
+
+// Function specifiers in case library is build/used as a shared library (Windows)
+// NOTE: Microsoft specifiers to tell compiler that symbols are imported/exported from a .dll
+#if defined(_WIN32)
+ #if defined(BUILD_LIBTYPE_SHARED)
+ #define RAYGUIAPI __declspec(dllexport) // Building the library as a Win32 shared library (.dll)
+ #elif defined(USE_LIBTYPE_SHARED)
+ #define RAYGUIAPI __declspec(dllimport) // Using the library as a Win32 shared library (.dll)
+ #endif
+ #if !defined(_CRT_SECURE_NO_WARNINGS)
+ #define _CRT_SECURE_NO_WARNINGS // Disable unsafe warnings on scanf() functions in MSVC
+ #endif
+#else
+ #if defined(BUILD_LIBTYPE_SHARED)
+ #define RAYGUIAPI __attribute__((visibility("default"))) // Building as a Unix shared library (.so/.dylib)
+ #endif
+#endif
+
+// Function specifiers definition
+#ifndef RAYGUIAPI
+ #define RAYGUIAPI // Functions defined as 'extern' by default (implicit specifiers)
+#endif
+
+//----------------------------------------------------------------------------------
+// Defines and Macros
+//----------------------------------------------------------------------------------
+// Simple log system to avoid printf() calls if required
+// NOTE: Avoiding those calls, also avoids const strings memory usage
+#define RAYGUI_SUPPORT_LOG_INFO
+#if defined(RAYGUI_SUPPORT_LOG_INFO)
+ #define RAYGUI_LOG(...) printf(__VA_ARGS__)
+#else
+ #define RAYGUI_LOG(...)
+#endif
+
+#if !defined(RAYGUI_STANDALONE)
+// Macros to define required UI inputs, including mapping to gamepad controls
+#if !defined(GUI_BUTTON_DOWN)
+ #define GUI_BUTTON_DOWN (IsMouseButtonDown(MOUSE_LEFT_BUTTON) || IsGamepadButtonDown(0, GAMEPAD_BUTTON_RIGHT_FACE_DOWN))
+#endif
+#if !defined(GUI_BUTTON_DOWN_ALT)
+ // Mapping to alternative button down pressed
+ #define GUI_BUTTON_DOWN_ALT (IsMouseButtonDown(MOUSE_RIGHT_BUTTON) || IsGamepadButtonDown(0, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT))
+#endif
+#if !defined(GUI_BUTTON_PRESSED)
+ #define GUI_BUTTON_PRESSED (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_RIGHT_FACE_DOWN))
+#endif
+#if !defined(GUI_BUTTON_PRESSED_MID)
+ #define GUI_BUTTON_PRESSED_MID (IsMouseButtonPressed(MOUSE_MIDDLE_BUTTON) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_RIGHT_FACE_UP))
+#endif
+#if !defined(GUI_BUTTON_RELEASED)
+ #define GUI_BUTTON_RELEASED (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) || IsGamepadButtonReleased(0, GAMEPAD_BUTTON_RIGHT_FACE_DOWN))
+#endif
+#if !defined(GUI_SCROLL_DELTA)
+ // Mapping to scroll delta changes
+ // TODO: Review inconsistencies between platforms
+ #if defined(PLATFORM_WEB)
+ // NOTE: Gamepad axis triggers not detected on web platform
+ #define GUI_SCROLL_DELTA ((float)IsGamepadButtonDown(0, GAMEPAD_BUTTON_RIGHT_TRIGGER_2) - (float)IsGamepadButtonDown(0, GAMEPAD_BUTTON_LEFT_TRIGGER_2))
+ #else
+ #define GUI_SCROLL_DELTA (GetMouseWheelMove() + (GetGamepadAxisMovement(0, GAMEPAD_AXIS_RIGHT_TRIGGER) + 1) - (GetGamepadAxisMovement(0, GAMEPAD_AXIS_LEFT_TRIGGER) + 1))
+ #endif
+#endif
+#if !defined(GUI_POINTER_POSITION)
+ #define GUI_POINTER_POSITION GetMousePosition()
+#endif
+#if !defined(GUI_KEY_DOWN)
+ #define GUI_KEY_DOWN(key) IsKeyDown(key)
+#endif
+#if !defined(GUI_KEY_PRESSED)
+ #define GUI_KEY_PRESSED(key) IsKeyPressed(key)
+#endif
+#if !defined(GUI_INPUT_KEY)
+ #define GUI_INPUT_KEY GetCharPressed()
+#endif
+#else
+ #define GUI_BUTTON_DOWN 0
+ #define GUI_BUTTON_DOWN_ALT 0
+ #define GUI_BUTTON_PRESSED 0
+ #define GUI_BUTTON_PRESSED_MID 0
+ #define GUI_BUTTON_RELEASED 0
+ #define GUI_SCROLL_DELTA 0
+ #define GUI_POINTER_POSITION (Vector2){ 0 }
+ #define GUI_KEY_DOWN(key) 0
+ #define GUI_KEY_PRESSED(key) 0
+ #define GUI_INPUT_KEY 0
+#endif
+
+//----------------------------------------------------------------------------------
+// Types and Structures Definition
+// NOTE: Some types are required for RAYGUI_STANDALONE usage
+//----------------------------------------------------------------------------------
+#if defined(RAYGUI_STANDALONE)
+ #ifndef __cplusplus
+ // Boolean type
+ #ifndef true
+ typedef enum { false, true } bool;
+ #endif
+ #endif
+
+ // Vector2 type
+ typedef struct Vector2 {
+ float x;
+ float y;
+ } Vector2;
+
+ // Vector3 type // -- ConvertHSVtoRGB(), ConvertRGBtoHSV()
+ typedef struct Vector3 {
+ float x;
+ float y;
+ float z;
+ } Vector3;
+
+ // Color type, RGBA (32bit)
+ typedef struct Color {
+ unsigned char r;
+ unsigned char g;
+ unsigned char b;
+ unsigned char a;
+ } Color;
+
+#define BLANK (Color){ 0, 0, 0, 0 } // Blank (Transparent)
+
+ // Rectangle type
+ typedef struct Rectangle {
+ float x;
+ float y;
+ float width;
+ float height;
+ } Rectangle;
+
+ // NOTE: Texture2D type is very coupled to raylib, required by Font type
+ // It should be redesigned to be provided by user
+ typedef struct Texture {
+ unsigned int id; // OpenGL texture id
+ int width; // Texture base width
+ int height; // Texture base height
+ int mipmaps; // Mipmap levels, 1 by default
+ int format; // Data format (PixelFormat type)
+ } Texture;
+
+ // Texture2D, same as Texture
+ typedef Texture Texture2D;
+
+ // Image, pixel data stored in CPU memory (RAM)
+ typedef struct Image {
+ void *data; // Image raw data
+ int width; // Image base width
+ int height; // Image base height
+ int mipmaps; // Mipmap levels, 1 by default
+ int format; // Data format (PixelFormat type)
+ } Image;
+
+ // GlyphInfo, font characters glyphs info
+ typedef struct GlyphInfo {
+ int value; // Character value (Unicode)
+ int offsetX; // Character offset X when drawing
+ int offsetY; // Character offset Y when drawing
+ int advanceX; // Character advance position X
+ Image image; // Character image data
+ } GlyphInfo;
+
+ // NOTE: Font type is very coupled to raylib, mostly required by GuiLoadStyle()
+ // It should be redesigned to be provided by user
+ typedef struct Font {
+ int baseSize; // Base size (default chars height)
+ int glyphCount; // Number of glyph characters
+ int glyphPadding; // Padding around the glyph characters
+ Texture2D texture; // Texture atlas containing the glyphs
+ Rectangle *recs; // Rectangles in texture for the glyphs
+ GlyphInfo *glyphs; // Glyphs info data
+ } Font;
+#endif
+
+// Style property
+// NOTE: Used when exporting style as code for convenience
+typedef struct GuiStyleProp {
+ unsigned short controlId; // Control identifier
+ unsigned short propertyId; // Property identifier
+ int propertyValue; // Property value
+} GuiStyleProp;
+
+/*
+// Controls text style -NOT USED-
+// NOTE: Text style is defined by control
+typedef struct GuiTextStyle {
+ unsigned int size;
+ int charSpacing;
+ int lineSpacing;
+ int alignmentH;
+ int alignmentV;
+ int padding;
+} GuiTextStyle;
+*/
+
+// Gui control state
+typedef enum {
+ STATE_NORMAL = 0,
+ STATE_FOCUSED,
+ STATE_PRESSED,
+ STATE_DISABLED
+} GuiState;
+
+// Gui control text alignment
+typedef enum {
+ TEXT_ALIGN_LEFT = 0,
+ TEXT_ALIGN_CENTER,
+ TEXT_ALIGN_RIGHT
+} GuiTextAlignment;
+
+// Gui control text alignment vertical
+// NOTE: Text vertical position inside the text bounds
+typedef enum {
+ TEXT_ALIGN_TOP = 0,
+ TEXT_ALIGN_MIDDLE,
+ TEXT_ALIGN_BOTTOM
+} GuiTextAlignmentVertical;
+
+// Gui control text wrap mode
+// NOTE: Useful for multiline text
+typedef enum {
+ TEXT_WRAP_NONE = 0,
+ TEXT_WRAP_CHAR,
+ TEXT_WRAP_WORD
+} GuiTextWrapMode;
+
+// Gui controls
+// NOTE: Up to 16 controls supported or 32 controls (v500)
+typedef enum {
+ // Default -> populates to all controls when set
+ DEFAULT = 0,
+
+ // Basic controls
+ LABEL = 1, // Used also for: LABELBUTTON
+ BUTTON = 2,
+ TOGGLE = 3, // Used also for: TOGGLEGROUP
+ SLIDER = 4, // Used also for: SLIDERBAR, TOGGLESLIDER
+ PROGRESSBAR = 5,
+ CHECKBOX = 6,
+ COMBOBOX = 7,
+ DROPDOWNBOX = 8,
+ TEXTBOX = 9, // Used also for: TEXTBOXMULTI
+ VALUEBOX = 10,
+ TABBAR = 11,
+ LISTVIEW = 12,
+ COLORPICKER = 13,
+ SCROLLBAR = 14,
+ STATUSBAR = 15
+ // NOTE: More controls can be added if required
+} GuiControl;
+
+// Controls BASE properties for every control (RAYGUI_MAX_PROPS_BASE = 16)
+// NOTE: Properties required for all controls, DEFAULT control sets
+// default values for them but they can be overriden per control
+typedef enum {
+ BORDER_COLOR_NORMAL = 0, // Control border color in STATE_NORMAL
+ BASE_COLOR_NORMAL = 1, // Control base color in STATE_NORMAL
+ TEXT_COLOR_NORMAL = 2, // Control text color in STATE_NORMAL
+ BORDER_COLOR_FOCUSED = 3, // Control border color in STATE_FOCUSED
+ BASE_COLOR_FOCUSED = 4, // Control base color in STATE_FOCUSED
+ TEXT_COLOR_FOCUSED = 5, // Control text color in STATE_FOCUSED
+ BORDER_COLOR_PRESSED = 6, // Control border color in STATE_PRESSED
+ BASE_COLOR_PRESSED = 7, // Control base color in STATE_PRESSED
+ TEXT_COLOR_PRESSED = 8, // Control text color in STATE_PRESSED
+ BORDER_COLOR_DISABLED = 9, // Control border color in STATE_DISABLED
+ BASE_COLOR_DISABLED = 10, // Control base color in STATE_DISABLED
+ TEXT_COLOR_DISABLED = 11, // Control text color in STATE_DISABLED
+ BORDER_WIDTH = 12, // Control border size, 0 for no border
+ TEXT_PADDING = 13, // Control text padding, not considering border
+ TEXT_ALIGNMENT = 14, // Control text horizontal alignment inside control text bound (after border and padding): 0-Left, 1-Center, 2-Right
+ BASEPROP16 = 15 // Not used yet...
+} GuiControlProperty;
+
+
+// WARNING: Some properties have been chosen to be global with DEFAULT - EXTENDED properties:
+// - TEXT_SIZE, // Control text size (glyphs max height)
+// - TEXT_SPACING, // Control text spacing between glyphs
+// - TEXT_LINE_SPACING, // Control text spacing between lines
+// - TEXT_WRAP_MODE // Control text wrap-mode inside text bounds
+//
+// While other properties can be setup per globally (DEFAULT) or per control:
+// - TEXT_PADDING // Control text padding, not considering border
+// - TEXT_ALIGNMENT // Control text horizontal alignment inside control text bound
+
+
+// Controls EXTENDED properties (RAYGUI_MAX_PROPS_EXTENDED = 8)
+// WARNING: Only 8 slots vailable for those properties per control, including DEFAULT
+//----------------------------------------------------------------------------------
+// DEFAULT control, extended properties
+// NOTE: Those properties are global for all controls, they can not be setup per control
+typedef enum {
+ TEXT_SIZE = 16, // Text size (glyphs max height)
+ TEXT_SPACING = 17, // Text spacing between glyphs
+ LINE_COLOR = 18, // Line control color
+ BACKGROUND_COLOR = 19, // Background color
+ TEXT_LINE_SPACING = 20, // Text spacing between lines
+ TEXT_ALIGNMENT_VERTICAL = 21, // Text vertical alignment inside text bounds (after border and padding): 0-Top, 1-Middle, 2-Bottom
+ TEXT_WRAP_MODE = 22, // Text wrap-mode inside text bounds
+ EXTPROP08 = 23 // Not used yet...
+} GuiDefaultProperty;
+
+// Other possible text extended properties:
+// TEXT_DECORATION // Text decoration: 0-None, 1-Underline, 2-Line-through, 3-Overline
+// TEXT_DECORATION_THICK // Text decoration line thickness
+// TEXT_FONT_WEIGHT // Text font weight: 0-Normal, 1-Bold, 2-Italic -> Requires specific font change
+
+// Label
+//typedef enum { } GuiLabelProperty;
+
+// Button
+//typedef enum { } GuiButtonProperty;
+
+// Toggle/ToggleGroup
+typedef enum {
+ GROUP_PADDING = 16, // ToggleGroup separation between toggles
+ GROUP_WIDTH_FULL // ToggleGroup bounds width considers all items: 0-Width per item, 1-Full width
+} GuiToggleProperty;
+
+// Slider/SliderBar
+typedef enum {
+ SLIDER_WIDTH = 16, // Slider size of internal bar
+ SLIDER_PADDING // Slider/SliderBar internal bar padding
+} GuiSliderProperty;
+
+// ProgressBar
+typedef enum {
+ PROGRESS_PADDING = 16, // ProgressBar internal padding
+ PROGRESS_SIDE, // ProgressBar increment side: 0-Left->Right, 1-Right->Left
+} GuiProgressBarProperty;
+
+// ScrollBar
+typedef enum {
+ ARROWS_SIZE = 16, // ScrollBar arrows size
+ ARROWS_VISIBLE, // ScrollBar arrows visible
+ SCROLL_SLIDER_PADDING, // ScrollBar slider internal padding
+ SCROLL_SLIDER_SIZE, // ScrollBar slider size
+ SCROLL_PADDING, // ScrollBar scroll padding from arrows
+ SCROLL_SPEED, // ScrollBar scrolling speed
+} GuiScrollBarProperty;
+
+// CheckBox
+typedef enum {
+ CHECK_PADDING = 16 // CheckBox internal check padding
+} GuiCheckBoxProperty;
+
+// ComboBox
+typedef enum {
+ COMBO_BUTTON_WIDTH = 16, // ComboBox right button width
+ COMBO_BUTTON_SPACING // ComboBox button separation
+} GuiComboBoxProperty;
+
+// DropdownBox
+typedef enum {
+ ARROW_PADDING = 16, // DropdownBox arrow separation from border and items
+ DROPDOWN_ITEMS_SPACING, // DropdownBox items separation
+ DROPDOWN_ARROW_HIDDEN, // DropdownBox arrow hidden
+ DROPDOWN_ROLL_UP // DropdownBox roll up flag: 0-Roll down, 1-Roll up
+} GuiDropdownBoxProperty;
+
+// TextBox/TextBoxMulti/ValueBox/Spinner
+typedef enum {
+ TEXT_READONLY = 16, // TextBox in read-only mode: 0-Text editable, 1-Text read-only
+} GuiTextBoxProperty;
+
+// ValueBox/Spinner
+typedef enum {
+ SPINNER_BUTTON_WIDTH = 16, // Spinner left/right buttons width
+ SPINNER_BUTTON_SPACING, // Spinner buttons separation
+} GuiValueBoxProperty;
+
+// TabBar
+typedef enum {
+ TAB_ITEMS_WIDTH = 16, // TabBar tab items width
+ TAB_CLOSE_BUTTON, // TabBar tab close button: 0-Not shown, 1-Shown
+ TAB_LINE_SIDE, // TabBar tabs side: 0-Bottom, 1-Top
+} GuiTabBarProperty;
+
+// ListView
+#define SCROLLBAR_LEFT_SIDE 0
+#define SCROLLBAR_RIGHT_SIDE 1
+
+typedef enum {
+ LIST_ITEMS_HEIGHT = 16, // ListView items height
+ LIST_ITEMS_SPACING, // ListView items separation
+ SCROLLBAR_WIDTH, // ListView scrollbar size (usually width)
+ SCROLLBAR_SIDE, // ListView scrollbar side: 0-Left side, 1-Right Side
+ LIST_ITEMS_BORDER_NORMAL, // ListView items border enabled in normal state
+ LIST_ITEMS_BORDER_WIDTH // ListView items border width
+} GuiListViewProperty;
+
+// ColorPicker
+typedef enum {
+ COLOR_SELECTOR_SIZE = 16, // ColorPicker selector square size
+ HUEBAR_WIDTH, // ColorPicker right hue bar width
+ HUEBAR_PADDING, // ColorPicker right hue bar separation from panel
+ HUEBAR_SELECTOR_HEIGHT, // ColorPicker right hue bar selector height
+ HUEBAR_SELECTOR_OVERFLOW // ColorPicker right hue bar selector overflow
+} GuiColorPickerProperty;
+
+//----------------------------------------------------------------------------------
+// Global Variables Definition
+//----------------------------------------------------------------------------------
+// ...
+
+//----------------------------------------------------------------------------------
+// Module Functions Declaration
+//----------------------------------------------------------------------------------
+
+#if defined(__cplusplus)
+extern "C" { // Prevents name mangling of functions
+#endif
+
+// Global gui state control functions
+RAYGUIAPI void GuiEnable(void); // Enable gui controls (global state)
+RAYGUIAPI void GuiDisable(void); // Disable gui controls (global state)
+RAYGUIAPI void GuiLock(void); // Lock gui controls (global state)
+RAYGUIAPI void GuiUnlock(void); // Unlock gui controls (global state)
+RAYGUIAPI bool GuiIsLocked(void); // Check if gui is locked (global state)
+RAYGUIAPI void GuiSetAlpha(float alpha); // Set gui controls alpha (global state), alpha goes from 0.0f to 1.0f
+RAYGUIAPI void GuiSetState(int state); // Set gui state (global state)
+RAYGUIAPI int GuiGetState(void); // Get gui state (global state)
+
+// Font set/get functions
+RAYGUIAPI void GuiSetFont(Font font); // Set gui custom font (global state)
+RAYGUIAPI Font GuiGetFont(void); // Get gui custom font (global state)
+
+// Style set/get functions
+RAYGUIAPI void GuiSetStyle(int control, int property, int value); // Set one style property
+RAYGUIAPI int GuiGetStyle(int control, int property); // Get one style property
+
+// Styles loading functions
+RAYGUIAPI void GuiLoadStyle(const char *fileName); // Load style file over global style variable (.rgs)
+RAYGUIAPI void GuiLoadStyleFromMemory(const unsigned char *fileData, int dataSize); // Load style from memory (binary only)
+RAYGUIAPI void GuiLoadStyleDefault(void); // Load style default over global style
+
+// Tooltips management functions
+RAYGUIAPI void GuiEnableTooltip(void); // Enable gui tooltips (global state)
+RAYGUIAPI void GuiDisableTooltip(void); // Disable gui tooltips (global state)
+RAYGUIAPI void GuiSetTooltip(const char *tooltip); // Set tooltip string
+
+// Icons functionality
+RAYGUIAPI const char *GuiIconText(int iconId, const char *text); // Get text with icon id prepended (if supported)
+#if !defined(RAYGUI_NO_ICONS)
+RAYGUIAPI void GuiSetIconScale(int scale); // Set default icon drawing size
+RAYGUIAPI unsigned int *GuiGetIcons(void); // Get raygui icons data pointer
+RAYGUIAPI char **GuiLoadIcons(const char *fileName, bool loadIconsName); // Load raygui icons file (.rgi) into internal icons data
+RAYGUIAPI char **GuiLoadIconsFromMemory(const unsigned char *fileData, int dataSize, bool loadIconsName); // Load raygui icons file (.rgi) from memory into internal icons data
+RAYGUIAPI void GuiDrawIcon(int iconId, int posX, int posY, int pixelSize, Color color); // Draw icon using pixel size at specified position
+#endif
+
+// Utility functions
+RAYGUIAPI int GuiGetTextWidth(const char *text); // Get text width considering gui style and icon size (if required)
+
+// Controls
+//----------------------------------------------------------------------------------------------------------
+// Container/separator controls, useful for controls organization
+RAYGUIAPI int GuiWindowBox(Rectangle bounds, const char *title); // Window Box control, shows a window that can be closed
+RAYGUIAPI int GuiGroupBox(Rectangle bounds, const char *text); // Group Box control with text name
+RAYGUIAPI int GuiLine(Rectangle bounds, const char *text); // Line separator control, could contain text
+RAYGUIAPI int GuiPanel(Rectangle bounds, const char *text); // Panel control, useful to group controls
+RAYGUIAPI int GuiScrollPanel(Rectangle bounds, const char *text, Rectangle content, Vector2 *scroll, Rectangle *view); // Scroll Panel control
+
+// Basic controls set
+RAYGUIAPI int GuiLabel(Rectangle bounds, const char *text); // Label control
+RAYGUIAPI int GuiButton(Rectangle bounds, const char *text); // Button control, returns true when clicked
+RAYGUIAPI int GuiLabelButton(Rectangle bounds, const char *text); // Label button control, returns true when clicked
+RAYGUIAPI int GuiToggle(Rectangle bounds, const char *text, bool *active); // Toggle Button control
+RAYGUIAPI int GuiToggleGroup(Rectangle bounds, const char *text, int *active); // Toggle Group control
+RAYGUIAPI int GuiToggleSlider(Rectangle bounds, const char *text, int *active); // Toggle Slider control
+RAYGUIAPI int GuiCheckBox(Rectangle bounds, const char *text, bool *checked); // Check Box control, returns true when active
+RAYGUIAPI int GuiComboBox(Rectangle bounds, const char *text, int *active); // Combo Box control
+
+RAYGUIAPI int GuiDropdownBox(Rectangle bounds, const char *text, int *active, bool editMode); // Dropdown Box control
+RAYGUIAPI int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Spinner control
+RAYGUIAPI int GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Value Box control, updates input text with numbers
+RAYGUIAPI int GuiValueBoxFloat(Rectangle bounds, const char *text, char *textValue, float *value, bool editMode); // Value box control for float values
+RAYGUIAPI int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode); // Text Box control, updates input text
+
+RAYGUIAPI int GuiSlider(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue); // Slider control
+RAYGUIAPI int GuiSliderBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue); // Slider Bar control
+RAYGUIAPI int GuiProgressBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue); // Progress Bar control
+RAYGUIAPI int GuiStatusBar(Rectangle bounds, const char *text); // Status Bar control, shows info text
+RAYGUIAPI int GuiDummyRec(Rectangle bounds, const char *text); // Dummy control for placeholders
+RAYGUIAPI int GuiGrid(Rectangle bounds, const char *text, float spacing, int subdivs, Vector2 *mouseCell); // Grid control
+
+// Advance controls set
+RAYGUIAPI int GuiListView(Rectangle bounds, const char *text, int *scrollIndex, int *active); // List View control
+RAYGUIAPI int GuiListViewEx(Rectangle bounds, char **text, int count, int *scrollIndex, int *active, int *focus); // List View control, using text entries list and returning focus entry
+RAYGUIAPI int GuiTabBar(Rectangle bounds, const char *text, int *hscroll, int *active); // Tab Bar control
+RAYGUIAPI int GuiTabBarEx(Rectangle bounds, char **text, int count, int *hscroll, int *active, int *focus); // Tab Bar control, using text entries list and returning focus entry
+RAYGUIAPI int GuiMessageBox(Rectangle bounds, const char *title, const char *message, const char *buttons); // Message Box control, displays a message
+RAYGUIAPI int GuiTextInputBox(Rectangle bounds, const char *title, const char *message, const char *buttons, char *text, int textMaxSize, bool *secretViewActive); // Text Input Box control, ask for text, supports secret
+RAYGUIAPI int GuiColorPicker(Rectangle bounds, const char *text, Color *color); // Color Picker control, includes Color bar controls
+RAYGUIAPI int GuiColorPanel(Rectangle bounds, const char *text, Color *color); // Color Panel control
+RAYGUIAPI int GuiColorBarAlpha(Rectangle bounds, const char *text, float *alpha); // Color Bar Alpha control
+RAYGUIAPI int GuiColorBarHue(Rectangle bounds, const char *text, float *value); // Color Bar Hue control
+RAYGUIAPI int GuiColorPickerHSV(Rectangle bounds, const char *text, Vector3 *colorHsv); // Color Picker control, using Hue-Saturation-Value color data, includes Color bar controls
+RAYGUIAPI int GuiColorPanelHSV(Rectangle bounds, const char *text, Vector3 *colorHsv); // Color Panel control, using Hue-Saturation-Value color data
+//----------------------------------------------------------------------------------------------------------
+
+#if !defined(RAYGUI_NO_ICONS)
+
+#if !defined(RAYGUI_CUSTOM_ICONS)
+//----------------------------------------------------------------------------------
+// Icons enumeration
+//----------------------------------------------------------------------------------
+typedef enum {
+ ICON_NONE = 0,
+ ICON_FOLDER_FILE_OPEN = 1,
+ ICON_FILE_SAVE_CLASSIC = 2,
+ ICON_FOLDER_OPEN = 3,
+ ICON_FOLDER_SAVE = 4,
+ ICON_FILE_OPEN = 5,
+ ICON_FILE_SAVE = 6,
+ ICON_FILE_EXPORT = 7,
+ ICON_FILE_ADD = 8,
+ ICON_FILE_DELETE = 9,
+ ICON_FILETYPE_TEXT = 10,
+ ICON_FILETYPE_AUDIO = 11,
+ ICON_FILETYPE_IMAGE = 12,
+ ICON_FILETYPE_PLAY = 13,
+ ICON_FILETYPE_VIDEO = 14,
+ ICON_FILETYPE_INFO = 15,
+ ICON_FILE_COPY = 16,
+ ICON_FILE_CUT = 17,
+ ICON_FILE_PASTE = 18,
+ ICON_CURSOR_HAND = 19,
+ ICON_CURSOR_POINTER = 20,
+ ICON_CURSOR_CLASSIC = 21,
+ ICON_PENCIL = 22,
+ ICON_PENCIL_BIG = 23,
+ ICON_BRUSH_CLASSIC = 24,
+ ICON_BRUSH_PAINTER = 25,
+ ICON_WATER_DROP = 26,
+ ICON_COLOR_PICKER = 27,
+ ICON_RUBBER = 28,
+ ICON_COLOR_BUCKET = 29,
+ ICON_TEXT_T = 30,
+ ICON_TEXT_A = 31,
+ ICON_SCALE = 32,
+ ICON_RESIZE = 33,
+ ICON_FILTER_POINT = 34,
+ ICON_FILTER_BILINEAR = 35,
+ ICON_CROP = 36,
+ ICON_CROP_ALPHA = 37,
+ ICON_SQUARE_TOGGLE = 38,
+ ICON_SYMMETRY = 39,
+ ICON_SYMMETRY_HORIZONTAL = 40,
+ ICON_SYMMETRY_VERTICAL = 41,
+ ICON_LENS = 42,
+ ICON_LENS_BIG = 43,
+ ICON_EYE_ON = 44,
+ ICON_EYE_OFF = 45,
+ ICON_FILTER_TOP = 46,
+ ICON_FILTER = 47,
+ ICON_TARGET_POINT = 48,
+ ICON_TARGET_SMALL = 49,
+ ICON_TARGET_BIG = 50,
+ ICON_TARGET_MOVE = 51,
+ ICON_CURSOR_MOVE = 52,
+ ICON_CURSOR_SCALE = 53,
+ ICON_CURSOR_SCALE_RIGHT = 54,
+ ICON_CURSOR_SCALE_LEFT = 55,
+ ICON_UNDO = 56,
+ ICON_REDO = 57,
+ ICON_REREDO = 58,
+ ICON_MUTATE = 59,
+ ICON_ROTATE = 60,
+ ICON_REPEAT = 61,
+ ICON_SHUFFLE = 62,
+ ICON_EMPTYBOX = 63,
+ ICON_TARGET = 64,
+ ICON_TARGET_SMALL_FILL = 65,
+ ICON_TARGET_BIG_FILL = 66,
+ ICON_TARGET_MOVE_FILL = 67,
+ ICON_CURSOR_MOVE_FILL = 68,
+ ICON_CURSOR_SCALE_FILL = 69,
+ ICON_CURSOR_SCALE_RIGHT_FILL = 70,
+ ICON_CURSOR_SCALE_LEFT_FILL = 71,
+ ICON_UNDO_FILL = 72,
+ ICON_REDO_FILL = 73,
+ ICON_REREDO_FILL = 74,
+ ICON_MUTATE_FILL = 75,
+ ICON_ROTATE_FILL = 76,
+ ICON_REPEAT_FILL = 77,
+ ICON_SHUFFLE_FILL = 78,
+ ICON_EMPTYBOX_SMALL = 79,
+ ICON_BOX = 80,
+ ICON_BOX_TOP = 81,
+ ICON_BOX_TOP_RIGHT = 82,
+ ICON_BOX_RIGHT = 83,
+ ICON_BOX_BOTTOM_RIGHT = 84,
+ ICON_BOX_BOTTOM = 85,
+ ICON_BOX_BOTTOM_LEFT = 86,
+ ICON_BOX_LEFT = 87,
+ ICON_BOX_TOP_LEFT = 88,
+ ICON_BOX_CENTER = 89,
+ ICON_BOX_CIRCLE_MASK = 90,
+ ICON_POT = 91,
+ ICON_ALPHA_MULTIPLY = 92,
+ ICON_ALPHA_CLEAR = 93,
+ ICON_DITHERING = 94,
+ ICON_MIPMAPS = 95,
+ ICON_BOX_GRID = 96,
+ ICON_GRID = 97,
+ ICON_BOX_CORNERS_SMALL = 98,
+ ICON_BOX_CORNERS_BIG = 99,
+ ICON_FOUR_BOXES = 100,
+ ICON_GRID_FILL = 101,
+ ICON_BOX_MULTISIZE = 102,
+ ICON_ZOOM_SMALL = 103,
+ ICON_ZOOM_MEDIUM = 104,
+ ICON_ZOOM_BIG = 105,
+ ICON_ZOOM_ALL = 106,
+ ICON_ZOOM_CENTER = 107,
+ ICON_BOX_DOTS_SMALL = 108,
+ ICON_BOX_DOTS_BIG = 109,
+ ICON_BOX_CONCENTRIC = 110,
+ ICON_BOX_GRID_BIG = 111,
+ ICON_OK_TICK = 112,
+ ICON_CROSS = 113,
+ ICON_ARROW_LEFT = 114,
+ ICON_ARROW_RIGHT = 115,
+ ICON_ARROW_DOWN = 116,
+ ICON_ARROW_UP = 117,
+ ICON_ARROW_LEFT_FILL = 118,
+ ICON_ARROW_RIGHT_FILL = 119,
+ ICON_ARROW_DOWN_FILL = 120,
+ ICON_ARROW_UP_FILL = 121,
+ ICON_AUDIO = 122,
+ ICON_FX = 123,
+ ICON_WAVE = 124,
+ ICON_WAVE_SINUS = 125,
+ ICON_WAVE_SQUARE = 126,
+ ICON_WAVE_TRIANGULAR = 127,
+ ICON_CROSS_SMALL = 128,
+ ICON_PLAYER_PREVIOUS = 129,
+ ICON_PLAYER_PLAY_BACK = 130,
+ ICON_PLAYER_PLAY = 131,
+ ICON_PLAYER_PAUSE = 132,
+ ICON_PLAYER_STOP = 133,
+ ICON_PLAYER_NEXT = 134,
+ ICON_PLAYER_RECORD = 135,
+ ICON_MAGNET = 136,
+ ICON_LOCK_CLOSE = 137,
+ ICON_LOCK_OPEN = 138,
+ ICON_CLOCK = 139,
+ ICON_TOOLS = 140,
+ ICON_GEAR = 141,
+ ICON_GEAR_BIG = 142,
+ ICON_BIN = 143,
+ ICON_HAND_POINTER = 144,
+ ICON_LASER = 145,
+ ICON_COIN = 146,
+ ICON_EXPLOSION = 147,
+ ICON_1UP = 148,
+ ICON_PLAYER = 149,
+ ICON_PLAYER_JUMP = 150,
+ ICON_KEY = 151,
+ ICON_DEMON = 152,
+ ICON_TEXT_POPUP = 153,
+ ICON_GEAR_EX = 154,
+ ICON_CRACK = 155,
+ ICON_CRACK_POINTS = 156,
+ ICON_STAR = 157,
+ ICON_DOOR = 158,
+ ICON_EXIT = 159,
+ ICON_MODE_2D = 160,
+ ICON_MODE_3D = 161,
+ ICON_CUBE = 162,
+ ICON_CUBE_FACE_TOP = 163,
+ ICON_CUBE_FACE_LEFT = 164,
+ ICON_CUBE_FACE_FRONT = 165,
+ ICON_CUBE_FACE_BOTTOM = 166,
+ ICON_CUBE_FACE_RIGHT = 167,
+ ICON_CUBE_FACE_BACK = 168,
+ ICON_CAMERA = 169,
+ ICON_SPECIAL = 170,
+ ICON_LINK_NET = 171,
+ ICON_LINK_BOXES = 172,
+ ICON_LINK_MULTI = 173,
+ ICON_LINK = 174,
+ ICON_LINK_BROKE = 175,
+ ICON_TEXT_NOTES = 176,
+ ICON_NOTEBOOK = 177,
+ ICON_SUITCASE = 178,
+ ICON_SUITCASE_ZIP = 179,
+ ICON_MAILBOX = 180,
+ ICON_MONITOR = 181,
+ ICON_PRINTER = 182,
+ ICON_PHOTO_CAMERA = 183,
+ ICON_PHOTO_CAMERA_FLASH = 184,
+ ICON_HOUSE = 185,
+ ICON_HEART = 186,
+ ICON_CORNER = 187,
+ ICON_VERTICAL_BARS = 188,
+ ICON_VERTICAL_BARS_FILL = 189,
+ ICON_LIFE_BARS = 190,
+ ICON_INFO = 191,
+ ICON_CROSSLINE = 192,
+ ICON_HELP = 193,
+ ICON_FILETYPE_ALPHA = 194,
+ ICON_FILETYPE_HOME = 195,
+ ICON_LAYERS_VISIBLE = 196,
+ ICON_LAYERS = 197,
+ ICON_WINDOW = 198,
+ ICON_HIDPI = 199,
+ ICON_FILETYPE_BINARY = 200,
+ ICON_HEX = 201,
+ ICON_SHIELD = 202,
+ ICON_FILE_NEW = 203,
+ ICON_FOLDER_ADD = 204,
+ ICON_ALARM = 205,
+ ICON_CPU = 206,
+ ICON_ROM = 207,
+ ICON_STEP_OVER = 208,
+ ICON_STEP_INTO = 209,
+ ICON_STEP_OUT = 210,
+ ICON_RESTART = 211,
+ ICON_BREAKPOINT_ON = 212,
+ ICON_BREAKPOINT_OFF = 213,
+ ICON_BURGER_MENU = 214,
+ ICON_CASE_SENSITIVE = 215,
+ ICON_REG_EXP = 216,
+ ICON_FOLDER = 217,
+ ICON_FILE = 218,
+ ICON_SAND_TIMER = 219,
+ ICON_WARNING = 220,
+ ICON_HELP_BOX = 221,
+ ICON_INFO_BOX = 222,
+ ICON_PRIORITY = 223,
+ ICON_LAYERS_ISO = 224,
+ ICON_LAYERS2 = 225,
+ ICON_MLAYERS = 226,
+ ICON_MAPS = 227,
+ ICON_HOT = 228,
+ ICON_LABEL = 229,
+ ICON_NAME_ID = 230,
+ ICON_SLICING = 231,
+ ICON_MANUAL_CONTROL = 232,
+ ICON_COLLISION = 233,
+ ICON_CIRCLE_ADD = 234,
+ ICON_CIRCLE_ADD_FILL = 235,
+ ICON_CIRCLE_WARNING = 236,
+ ICON_CIRCLE_WARNING_FILL = 237,
+ ICON_BOX_MORE = 238,
+ ICON_BOX_MORE_FILL = 239,
+ ICON_BOX_MINUS = 240,
+ ICON_BOX_MINUS_FILL = 241,
+ ICON_UNION = 242,
+ ICON_INTERSECTION = 243,
+ ICON_DIFFERENCE = 244,
+ ICON_SPHERE = 245,
+ ICON_CYLINDER = 246,
+ ICON_CONE = 247,
+ ICON_ELLIPSOID = 248,
+ ICON_CAPSULE = 249,
+ ICON_FILETYPE_FONT = 250,
+ ICON_FILETYPE_3D = 251,
+ ICON_FILETYPE_CODE_XML = 252,
+ ICON_FILETYPE_CODE_C = 253,
+ ICON_FILETYPE_CODE_PYTHON = 254,
+ ICON_FILETYPE_CODE_JS = 255,
+ ICON_FILETYPE_ICON = 256,
+} GuiIconName;
+#endif
+
+#endif // RAYGUI_NO_ICONS
+
+#if defined(__cplusplus)
+} // Prevents name mangling of functions
+#endif
+
+#endif // RAYGUI_H
+
+/***********************************************************************************
+*
+* RAYGUI IMPLEMENTATION
+*
+************************************************************************************/
+
+#if defined(RAYGUI_IMPLEMENTATION)
+
+#include <stdio.h> // Required for: FILE, fopen(), fclose(), fprintf(), feof(), fscanf(), snprintf(), vsprintf() [GuiLoadStyle(), GuiLoadIcons()]
+#include <string.h> // Required for: strlen() [GuiTextBox(), GuiValueBox()], memset(), memcpy()
+#include <stdarg.h> // Required for: va_list, va_start(), vfprintf(), va_end() [TextFormat()]
+#include <math.h> // Required for: roundf() [GuiColorPicker()]
+#include <ctype.h> // Required for: isspace() [GuiTextBox()]
+
+// Allow custom memory allocators
+#if defined(RAYGUI_MALLOC) || defined(RAYGUI_CALLOC) || defined(RAYGUI_FREE)
+ #if !defined(RAYGUI_MALLOC) || !defined(RAYGUI_CALLOC) || !defined(RAYGUI_FREE)
+ #error "RAYGUI: if RAYGUI_MALLOC, RAYGUI_CALLOC, or RAYGUI_FREE is customized, all three must be customized"
+ #endif
+#else
+ #include <stdlib.h> // Required for: malloc(), calloc(), free() [GuiLoadStyle(), GuiLoadIcons()]
+
+ #define RAYGUI_MALLOC(sz) malloc(sz)
+ #define RAYGUI_CALLOC(n,sz) calloc(n,sz)
+ #define RAYGUI_FREE(p) free(p)
+#endif
+
+#ifdef __cplusplus
+ #define RAYGUI_CLITERAL(name) name
+#else
+ #define RAYGUI_CLITERAL(name) (name)
+#endif
+
+// Check if two rectangles are equal, used to validate a slider bounds as an id
+#ifndef CHECK_BOUNDS_ID
+ #define CHECK_BOUNDS_ID(src, dst) (((int)src.x == (int)dst.x) && ((int)src.y == (int)dst.y) && ((int)src.width == (int)dst.width) && ((int)src.height == (int)dst.height))
+#endif
+
+#if !defined(RAYGUI_NO_ICONS) && !defined(RAYGUI_CUSTOM_ICONS)
+
+// Embedded icons, no external file provided
+#define RAYGUI_ICON_SIZE 16 // Size of icons in pixels (squared)
+#define RAYGUI_ICON_MAX_ICONS 512 // Maximum number of icons
+#define RAYGUI_ICON_MAX_FONT_BACKED 257 // Maximum number of icons to back in font atlas
+#define RAYGUI_ICON_MAX_NAME_LENGTH 32 // Maximum length of icon name id
+#define RAYGUI_ICON_FONT_ATLAS_PADDING 1 // Padding between backed icons in font atlas
+
+// Icons data is defined by bit array (every bit represents one pixel)
+// Those arrays are stored as unsigned int data arrays, so,
+// every array element defines 32 pixels (bits) of information
+// One icon is defined by 8 int, (8 int*32 bit = 256 bit = 16*16 pixels)
+// NOTE: Number of elemens depend on RAYGUI_ICON_SIZE (by default 16x16 pixels)
+#define RAYGUI_ICON_DATA_ELEMENTS (RAYGUI_ICON_SIZE*RAYGUI_ICON_SIZE/32)
+
+//----------------------------------------------------------------------------------
+// Icons data for all gui possible icons (allocated on data segment by default)
+//
+// NOTE 1: Every icon is codified in binary form, using 1 bit per pixel, so,
+// every 16x16 icon requires 8 integers (16*16/32) to be stored
+//
+// NOTE 2: A different icon set could be loaded over this array using GuiLoadIcons(),
+// but loaded icons set must be same RAYGUI_ICON_SIZE and no more than RAYGUI_ICON_MAX_ICONS
+//
+// guiIcons size is by default: 512*(16*16/32) = 16384 bytes = 16 KB
+//----------------------------------------------------------------------------------
+static unsigned int guiIcons[RAYGUI_ICON_MAX_ICONS*RAYGUI_ICON_DATA_ELEMENTS] = {
+ 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, // ICON_NONE
+ 0x3ff80000, 0x2f082008, 0x2042207e, 0x40027fc2, 0x40024002, 0x40024002, 0x40024002, 0x00007ffe, // ICON_FOLDER_FILE_OPEN
+ 0x3ffe0000, 0x44226422, 0x400247e2, 0x5ffa4002, 0x57ea500a, 0x500a500a, 0x40025ffa, 0x00007ffe, // ICON_FILE_SAVE_CLASSIC
+ 0x00000000, 0x0042007e, 0x40027fc2, 0x40024002, 0x41024002, 0x44424282, 0x793e4102, 0x00000100, // ICON_FOLDER_OPEN
+ 0x00000000, 0x0042007e, 0x40027fc2, 0x40024002, 0x41024102, 0x44424102, 0x793e4282, 0x00000000, // ICON_FOLDER_SAVE
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x21042004, 0x24442284, 0x21042104, 0x20042104, 0x00003ffc, // ICON_FILE_OPEN
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x21042004, 0x21042104, 0x22842444, 0x20042104, 0x00003ffc, // ICON_FILE_SAVE
+ 0x3ff00000, 0x201c2010, 0x00042004, 0x20041004, 0x20844784, 0x00841384, 0x20042784, 0x00003ffc, // ICON_FILE_EXPORT
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x22042204, 0x22042f84, 0x20042204, 0x00003ffc, // ICON_FILE_ADD
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x25042884, 0x25042204, 0x20042884, 0x00003ffc, // ICON_FILE_DELETE
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x20042ff4, 0x20042ff4, 0x20042ff4, 0x20042004, 0x00003ffc, // ICON_FILETYPE_TEXT
+ 0x3ff00000, 0x201c2010, 0x27042004, 0x244424c4, 0x26442444, 0x20642664, 0x20042004, 0x00003ffc, // ICON_FILETYPE_AUDIO
+ 0x3ff00000, 0x201c2010, 0x26042604, 0x20042004, 0x35442884, 0x2414222c, 0x20042004, 0x00003ffc, // ICON_FILETYPE_IMAGE
+ 0x3ff00000, 0x201c2010, 0x20c42004, 0x22442144, 0x22442444, 0x20c42144, 0x20042004, 0x00003ffc, // ICON_FILETYPE_PLAY
+ 0x3ff00000, 0x3ffc2ff0, 0x3f3c2ff4, 0x3dbc2eb4, 0x3dbc2bb4, 0x3f3c2eb4, 0x3ffc2ff4, 0x00002ff4, // ICON_FILETYPE_VIDEO
+ 0x3ff00000, 0x201c2010, 0x21842184, 0x21842004, 0x21842184, 0x21842184, 0x20042184, 0x00003ffc, // ICON_FILETYPE_INFO
+ 0x0ff00000, 0x381c0810, 0x28042804, 0x28042804, 0x28042804, 0x28042804, 0x20102ffc, 0x00003ff0, // ICON_FILE_COPY
+ 0x00000000, 0x701c0000, 0x079c1e14, 0x55a000f0, 0x079c00f0, 0x701c1e14, 0x00000000, 0x00000000, // ICON_FILE_CUT
+ 0x01c00000, 0x13e41bec, 0x3f841004, 0x204420c4, 0x20442044, 0x20442044, 0x207c2044, 0x00003fc0, // ICON_FILE_PASTE
+ 0x00000000, 0x3aa00fe0, 0x2abc2aa0, 0x2aa42aa4, 0x20042aa4, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_CURSOR_HAND
+ 0x00000000, 0x003c000c, 0x030800c8, 0x30100c10, 0x10202020, 0x04400840, 0x01800280, 0x00000000, // ICON_CURSOR_POINTER
+ 0x00000000, 0x00180000, 0x01f00078, 0x03e007f0, 0x07c003e0, 0x04000e40, 0x00000000, 0x00000000, // ICON_CURSOR_CLASSIC
+ 0x00000000, 0x04000000, 0x11000a00, 0x04400a80, 0x01100220, 0x00580088, 0x00000038, 0x00000000, // ICON_PENCIL
+ 0x04000000, 0x15000a00, 0x50402880, 0x14102820, 0x05040a08, 0x015c028c, 0x007c00bc, 0x00000000, // ICON_PENCIL_BIG
+ 0x01c00000, 0x01400140, 0x01400140, 0x0ff80140, 0x0ff80808, 0x0aa80808, 0x0aa80aa8, 0x00000ff8, // ICON_BRUSH_CLASSIC
+ 0x1ffc0000, 0x5ffc7ffe, 0x40004000, 0x00807f80, 0x01c001c0, 0x01c001c0, 0x01c001c0, 0x00000080, // ICON_BRUSH_PAINTER
+ 0x00000000, 0x00800000, 0x01c00080, 0x03e001c0, 0x07f003e0, 0x036006f0, 0x000001c0, 0x00000000, // ICON_WATER_DROP
+ 0x00000000, 0x3e003800, 0x1f803f80, 0x0c201e40, 0x02080c10, 0x00840104, 0x00380044, 0x00000000, // ICON_COLOR_PICKER
+ 0x00000000, 0x07800300, 0x1fe00fc0, 0x3f883fd0, 0x0e021f04, 0x02040402, 0x00f00108, 0x00000000, // ICON_RUBBER
+ 0x00c00000, 0x02800140, 0x08200440, 0x20081010, 0x2ffe3004, 0x03f807fc, 0x00e001f0, 0x00000040, // ICON_COLOR_BUCKET
+ 0x00000000, 0x21843ffc, 0x01800180, 0x01800180, 0x01800180, 0x01800180, 0x03c00180, 0x00000000, // ICON_TEXT_T
+ 0x00800000, 0x01400180, 0x06200340, 0x0c100620, 0x1ff80c10, 0x380c1808, 0x70067004, 0x0000f80f, // ICON_TEXT_A
+ 0x78000000, 0x50004000, 0x00004800, 0x03c003c0, 0x03c003c0, 0x00100000, 0x0002000a, 0x0000000e, // ICON_SCALE
+ 0x75560000, 0x5e004002, 0x54001002, 0x41001202, 0x408200fe, 0x40820082, 0x40820082, 0x00006afe, // ICON_RESIZE
+ 0x00000000, 0x3f003f00, 0x3f003f00, 0x3f003f00, 0x00400080, 0x001c0020, 0x001c001c, 0x00000000, // ICON_FILTER_POINT
+ 0x6d800000, 0x00004080, 0x40804080, 0x40800000, 0x00406d80, 0x001c0020, 0x001c001c, 0x00000000, // ICON_FILTER_BILINEAR
+ 0x40080000, 0x1ffe2008, 0x14081008, 0x11081208, 0x10481088, 0x10081028, 0x10047ff8, 0x00001002, // ICON_CROP
+ 0x00100000, 0x3ffc0010, 0x2ab03550, 0x22b02550, 0x20b02150, 0x20302050, 0x2000fff0, 0x00002000, // ICON_CROP_ALPHA
+ 0x40000000, 0x1ff82000, 0x04082808, 0x01082208, 0x00482088, 0x00182028, 0x35542008, 0x00000002, // ICON_SQUARE_TOGGLE
+ 0x00000000, 0x02800280, 0x06c006c0, 0x0ea00ee0, 0x1e901eb0, 0x3e883e98, 0x7efc7e8c, 0x00000000, // ICON_SYMMETRY
+ 0x01000000, 0x05600100, 0x1d480d50, 0x7d423d44, 0x3d447d42, 0x0d501d48, 0x01000560, 0x00000100, // ICON_SYMMETRY_HORIZONTAL
+ 0x01800000, 0x04200240, 0x10080810, 0x00001ff8, 0x00007ffe, 0x0ff01ff8, 0x03c007e0, 0x00000180, // ICON_SYMMETRY_VERTICAL
+ 0x00000000, 0x010800f0, 0x02040204, 0x02040204, 0x07f00308, 0x1c000e00, 0x30003800, 0x00000000, // ICON_LENS
+ 0x00000000, 0x061803f0, 0x08240c0c, 0x08040814, 0x0c0c0804, 0x23f01618, 0x18002400, 0x00000000, // ICON_LENS_BIG
+ 0x00000000, 0x00000000, 0x1c7007c0, 0x638e3398, 0x1c703398, 0x000007c0, 0x00000000, 0x00000000, // ICON_EYE_ON
+ 0x00000000, 0x10002000, 0x04700fc0, 0x610e3218, 0x1c703098, 0x001007a0, 0x00000008, 0x00000000, // ICON_EYE_OFF
+ 0x00000000, 0x00007ffc, 0x40047ffc, 0x10102008, 0x04400820, 0x02800280, 0x02800280, 0x00000100, // ICON_FILTER_TOP
+ 0x00000000, 0x40027ffe, 0x10082004, 0x04200810, 0x02400240, 0x02400240, 0x01400240, 0x000000c0, // ICON_FILTER
+ 0x00800000, 0x00800080, 0x00000080, 0x3c9e0000, 0x00000000, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_POINT
+ 0x00800000, 0x00800080, 0x00800080, 0x3f7e01c0, 0x008001c0, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_SMALL
+ 0x00800000, 0x00800080, 0x03e00080, 0x3e3e0220, 0x03e00220, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_BIG
+ 0x01000000, 0x04400280, 0x01000100, 0x43842008, 0x43849ab2, 0x01002008, 0x04400100, 0x01000280, // ICON_TARGET_MOVE
+ 0x01000000, 0x04400280, 0x01000100, 0x41042108, 0x41049ff2, 0x01002108, 0x04400100, 0x01000280, // ICON_CURSOR_MOVE
+ 0x781e0000, 0x500a4002, 0x04204812, 0x00000240, 0x02400000, 0x48120420, 0x4002500a, 0x0000781e, // ICON_CURSOR_SCALE
+ 0x00000000, 0x20003c00, 0x24002800, 0x01000200, 0x00400080, 0x00140024, 0x003c0004, 0x00000000, // ICON_CURSOR_SCALE_RIGHT
+ 0x00000000, 0x0004003c, 0x00240014, 0x00800040, 0x02000100, 0x28002400, 0x3c002000, 0x00000000, // ICON_CURSOR_SCALE_LEFT
+ 0x00000000, 0x00100020, 0x10101fc8, 0x10001020, 0x10001000, 0x10001000, 0x00001fc0, 0x00000000, // ICON_UNDO
+ 0x00000000, 0x08000400, 0x080813f8, 0x00080408, 0x00080008, 0x00080008, 0x000003f8, 0x00000000, // ICON_REDO
+ 0x00000000, 0x3ffc0000, 0x20042004, 0x20002000, 0x20402000, 0x3f902020, 0x00400020, 0x00000000, // ICON_REREDO
+ 0x00000000, 0x3ffc0000, 0x20042004, 0x27fc2004, 0x20202000, 0x3fc82010, 0x00200010, 0x00000000, // ICON_MUTATE
+ 0x00000000, 0x0ff00000, 0x10081818, 0x11801008, 0x10001180, 0x18101020, 0x00100fc8, 0x00000020, // ICON_ROTATE
+ 0x00000000, 0x04000200, 0x240429fc, 0x20042204, 0x20442004, 0x3f942024, 0x00400020, 0x00000000, // ICON_REPEAT
+ 0x00000000, 0x20001000, 0x22104c0e, 0x00801120, 0x11200040, 0x4c0e2210, 0x10002000, 0x00000000, // ICON_SHUFFLE
+ 0x7ffe0000, 0x50024002, 0x44024802, 0x41024202, 0x40424082, 0x40124022, 0x4002400a, 0x00007ffe, // ICON_EMPTYBOX
+ 0x00800000, 0x03e00080, 0x08080490, 0x3c9e0808, 0x08080808, 0x03e00490, 0x00800080, 0x00000000, // ICON_TARGET
+ 0x00800000, 0x00800080, 0x00800080, 0x3ffe01c0, 0x008001c0, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_SMALL_FILL
+ 0x00800000, 0x00800080, 0x03e00080, 0x3ffe03e0, 0x03e003e0, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_BIG_FILL
+ 0x01000000, 0x07c00380, 0x01000100, 0x638c2008, 0x638cfbbe, 0x01002008, 0x07c00100, 0x01000380, // ICON_TARGET_MOVE_FILL
+ 0x01000000, 0x07c00380, 0x01000100, 0x610c2108, 0x610cfffe, 0x01002108, 0x07c00100, 0x01000380, // ICON_CURSOR_MOVE_FILL
+ 0x781e0000, 0x6006700e, 0x04204812, 0x00000240, 0x02400000, 0x48120420, 0x700e6006, 0x0000781e, // ICON_CURSOR_SCALE_FILL
+ 0x00000000, 0x38003c00, 0x24003000, 0x01000200, 0x00400080, 0x000c0024, 0x003c001c, 0x00000000, // ICON_CURSOR_SCALE_RIGHT_FILL
+ 0x00000000, 0x001c003c, 0x0024000c, 0x00800040, 0x02000100, 0x30002400, 0x3c003800, 0x00000000, // ICON_CURSOR_SCALE_LEFT_FILL
+ 0x00000000, 0x00300020, 0x10301ff8, 0x10001020, 0x10001000, 0x10001000, 0x00001fc0, 0x00000000, // ICON_UNDO_FILL
+ 0x00000000, 0x0c000400, 0x0c081ff8, 0x00080408, 0x00080008, 0x00080008, 0x000003f8, 0x00000000, // ICON_REDO_FILL
+ 0x00000000, 0x3ffc0000, 0x20042004, 0x20002000, 0x20402000, 0x3ff02060, 0x00400060, 0x00000000, // ICON_REREDO_FILL
+ 0x00000000, 0x3ffc0000, 0x20042004, 0x27fc2004, 0x20202000, 0x3ff82030, 0x00200030, 0x00000000, // ICON_MUTATE_FILL
+ 0x00000000, 0x0ff00000, 0x10081818, 0x11801008, 0x10001180, 0x18301020, 0x00300ff8, 0x00000020, // ICON_ROTATE_FILL
+ 0x00000000, 0x06000200, 0x26042ffc, 0x20042204, 0x20442004, 0x3ff42064, 0x00400060, 0x00000000, // ICON_REPEAT_FILL
+ 0x00000000, 0x30001000, 0x32107c0e, 0x00801120, 0x11200040, 0x7c0e3210, 0x10003000, 0x00000000, // ICON_SHUFFLE_FILL
+ 0x00000000, 0x30043ffc, 0x24042804, 0x21042204, 0x20442084, 0x20142024, 0x3ffc200c, 0x00000000, // ICON_EMPTYBOX_SMALL
+ 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX
+ 0x00000000, 0x23c43ffc, 0x23c423c4, 0x200423c4, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_TOP
+ 0x00000000, 0x3e043ffc, 0x3e043e04, 0x20043e04, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_TOP_RIGHT
+ 0x00000000, 0x20043ffc, 0x20042004, 0x3e043e04, 0x3e043e04, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_RIGHT
+ 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x3e042004, 0x3e043e04, 0x3ffc3e04, 0x00000000, // ICON_BOX_BOTTOM_RIGHT
+ 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x23c42004, 0x23c423c4, 0x3ffc23c4, 0x00000000, // ICON_BOX_BOTTOM
+ 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x207c2004, 0x207c207c, 0x3ffc207c, 0x00000000, // ICON_BOX_BOTTOM_LEFT
+ 0x00000000, 0x20043ffc, 0x20042004, 0x207c207c, 0x207c207c, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_LEFT
+ 0x00000000, 0x207c3ffc, 0x207c207c, 0x2004207c, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_TOP_LEFT
+ 0x00000000, 0x20043ffc, 0x20042004, 0x23c423c4, 0x23c423c4, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_CENTER
+ 0x7ffe0000, 0x40024002, 0x47e24182, 0x4ff247e2, 0x47e24ff2, 0x418247e2, 0x40024002, 0x00007ffe, // ICON_BOX_CIRCLE_MASK
+ 0x7fff0000, 0x40014001, 0x40014001, 0x49555ddd, 0x4945495d, 0x400149c5, 0x40014001, 0x00007fff, // ICON_POT
+ 0x7ffe0000, 0x53327332, 0x44ce4cce, 0x41324332, 0x404e40ce, 0x48125432, 0x4006540e, 0x00007ffe, // ICON_ALPHA_MULTIPLY
+ 0x7ffe0000, 0x53327332, 0x44ce4cce, 0x41324332, 0x5c4e40ce, 0x44124432, 0x40065c0e, 0x00007ffe, // ICON_ALPHA_CLEAR
+ 0x7ffe0000, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x00007ffe, // ICON_DITHERING
+ 0x07fe0000, 0x1ffa0002, 0x7fea000a, 0x402a402a, 0x5b2a512a, 0x5128552a, 0x40205128, 0x00007fe0, // ICON_MIPMAPS
+ 0x00000000, 0x1ff80000, 0x12481248, 0x12481ff8, 0x1ff81248, 0x12481248, 0x00001ff8, 0x00000000, // ICON_BOX_GRID
+ 0x12480000, 0x7ffe1248, 0x12481248, 0x12487ffe, 0x7ffe1248, 0x12481248, 0x12487ffe, 0x00001248, // ICON_GRID
+ 0x00000000, 0x1c380000, 0x1c3817e8, 0x08100810, 0x08100810, 0x17e81c38, 0x00001c38, 0x00000000, // ICON_BOX_CORNERS_SMALL
+ 0x700e0000, 0x700e5ffa, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x5ffa700e, 0x0000700e, // ICON_BOX_CORNERS_BIG
+ 0x3f7e0000, 0x21422142, 0x21422142, 0x00003f7e, 0x21423f7e, 0x21422142, 0x3f7e2142, 0x00000000, // ICON_FOUR_BOXES
+ 0x00000000, 0x3bb80000, 0x3bb83bb8, 0x3bb80000, 0x3bb83bb8, 0x3bb80000, 0x3bb83bb8, 0x00000000, // ICON_GRID_FILL
+ 0x7ffe0000, 0x7ffe7ffe, 0x77fe7000, 0x77fe77fe, 0x777e7700, 0x777e777e, 0x777e777e, 0x0000777e, // ICON_BOX_MULTISIZE
+ 0x781e0000, 0x40024002, 0x00004002, 0x01800000, 0x00000180, 0x40020000, 0x40024002, 0x0000781e, // ICON_ZOOM_SMALL
+ 0x781e0000, 0x40024002, 0x00004002, 0x03c003c0, 0x03c003c0, 0x40020000, 0x40024002, 0x0000781e, // ICON_ZOOM_MEDIUM
+ 0x781e0000, 0x40024002, 0x07e04002, 0x07e007e0, 0x07e007e0, 0x400207e0, 0x40024002, 0x0000781e, // ICON_ZOOM_BIG
+ 0x781e0000, 0x5ffa4002, 0x1ff85ffa, 0x1ff81ff8, 0x1ff81ff8, 0x5ffa1ff8, 0x40025ffa, 0x0000781e, // ICON_ZOOM_ALL
+ 0x00000000, 0x2004381c, 0x00002004, 0x00000000, 0x00000000, 0x20040000, 0x381c2004, 0x00000000, // ICON_ZOOM_CENTER
+ 0x00000000, 0x1db80000, 0x10081008, 0x10080000, 0x00001008, 0x10081008, 0x00001db8, 0x00000000, // ICON_BOX_DOTS_SMALL
+ 0x35560000, 0x00002002, 0x00002002, 0x00002002, 0x00002002, 0x00002002, 0x35562002, 0x00000000, // ICON_BOX_DOTS_BIG
+ 0x7ffe0000, 0x40024002, 0x48124ff2, 0x49924812, 0x48124992, 0x4ff24812, 0x40024002, 0x00007ffe, // ICON_BOX_CONCENTRIC
+ 0x00000000, 0x10841ffc, 0x10841084, 0x1ffc1084, 0x10841084, 0x10841084, 0x00001ffc, 0x00000000, // ICON_BOX_GRID_BIG
+ 0x00000000, 0x00000000, 0x10000000, 0x04000800, 0x01040200, 0x00500088, 0x00000020, 0x00000000, // ICON_OK_TICK
+ 0x00000000, 0x10080000, 0x04200810, 0x01800240, 0x02400180, 0x08100420, 0x00001008, 0x00000000, // ICON_CROSS
+ 0x00000000, 0x02000000, 0x00800100, 0x00200040, 0x00200010, 0x00800040, 0x02000100, 0x00000000, // ICON_ARROW_LEFT
+ 0x00000000, 0x00400000, 0x01000080, 0x04000200, 0x04000800, 0x01000200, 0x00400080, 0x00000000, // ICON_ARROW_RIGHT
+ 0x00000000, 0x00000000, 0x00000000, 0x08081004, 0x02200410, 0x00800140, 0x00000000, 0x00000000, // ICON_ARROW_DOWN
+ 0x00000000, 0x00000000, 0x01400080, 0x04100220, 0x10040808, 0x00000000, 0x00000000, 0x00000000, // ICON_ARROW_UP
+ 0x00000000, 0x02000000, 0x03800300, 0x03e003c0, 0x03e003f0, 0x038003c0, 0x02000300, 0x00000000, // ICON_ARROW_LEFT_FILL
+ 0x00000000, 0x00400000, 0x01c000c0, 0x07c003c0, 0x07c00fc0, 0x01c003c0, 0x004000c0, 0x00000000, // ICON_ARROW_RIGHT_FILL
+ 0x00000000, 0x00000000, 0x00000000, 0x0ff81ffc, 0x03e007f0, 0x008001c0, 0x00000000, 0x00000000, // ICON_ARROW_DOWN_FILL
+ 0x00000000, 0x00000000, 0x01c00080, 0x07f003e0, 0x1ffc0ff8, 0x00000000, 0x00000000, 0x00000000, // ICON_ARROW_UP_FILL
+ 0x00000000, 0x18a008c0, 0x32881290, 0x24822686, 0x26862482, 0x12903288, 0x08c018a0, 0x00000000, // ICON_AUDIO
+ 0x00000000, 0x04800780, 0x004000c0, 0x662000f0, 0x08103c30, 0x130a0e18, 0x0000318e, 0x00000000, // ICON_FX
+ 0x00000000, 0x00800000, 0x08880888, 0x2aaa0a8a, 0x0a8a2aaa, 0x08880888, 0x00000080, 0x00000000, // ICON_WAVE
+ 0x00000000, 0x00600000, 0x01080090, 0x02040108, 0x42044204, 0x24022402, 0x00001800, 0x00000000, // ICON_WAVE_SINUS
+ 0x00000000, 0x07f80000, 0x04080408, 0x04080408, 0x04080408, 0x7c0e0408, 0x00000000, 0x00000000, // ICON_WAVE_SQUARE
+ 0x00000000, 0x00000000, 0x00a00040, 0x22084110, 0x08021404, 0x00000000, 0x00000000, 0x00000000, // ICON_WAVE_TRIANGULAR
+ 0x00000000, 0x00000000, 0x04200000, 0x01800240, 0x02400180, 0x00000420, 0x00000000, 0x00000000, // ICON_CROSS_SMALL
+ 0x00000000, 0x18380000, 0x12281428, 0x10a81128, 0x112810a8, 0x14281228, 0x00001838, 0x00000000, // ICON_PLAYER_PREVIOUS
+ 0x00000000, 0x18000000, 0x11801600, 0x10181060, 0x10601018, 0x16001180, 0x00001800, 0x00000000, // ICON_PLAYER_PLAY_BACK
+ 0x00000000, 0x00180000, 0x01880068, 0x18080608, 0x06081808, 0x00680188, 0x00000018, 0x00000000, // ICON_PLAYER_PLAY
+ 0x00000000, 0x1e780000, 0x12481248, 0x12481248, 0x12481248, 0x12481248, 0x00001e78, 0x00000000, // ICON_PLAYER_PAUSE
+ 0x00000000, 0x1ff80000, 0x10081008, 0x10081008, 0x10081008, 0x10081008, 0x00001ff8, 0x00000000, // ICON_PLAYER_STOP
+ 0x00000000, 0x1c180000, 0x14481428, 0x15081488, 0x14881508, 0x14281448, 0x00001c18, 0x00000000, // ICON_PLAYER_NEXT
+ 0x00000000, 0x03c00000, 0x08100420, 0x10081008, 0x10081008, 0x04200810, 0x000003c0, 0x00000000, // ICON_PLAYER_RECORD
+ 0x00000000, 0x0c3007e0, 0x13c81818, 0x14281668, 0x14281428, 0x1c381c38, 0x08102244, 0x00000000, // ICON_MAGNET
+ 0x07c00000, 0x08200820, 0x3ff80820, 0x23882008, 0x21082388, 0x20082108, 0x1ff02008, 0x00000000, // ICON_LOCK_CLOSE
+ 0x07c00000, 0x08000800, 0x3ff80800, 0x23882008, 0x21082388, 0x20082108, 0x1ff02008, 0x00000000, // ICON_LOCK_OPEN
+ 0x01c00000, 0x0c180770, 0x3086188c, 0x60832082, 0x60034781, 0x30062002, 0x0c18180c, 0x01c00770, // ICON_CLOCK
+ 0x0a200000, 0x1b201b20, 0x04200e20, 0x04200420, 0x04700420, 0x0e700e70, 0x0e700e70, 0x04200e70, // ICON_TOOLS
+ 0x01800000, 0x3bdc318c, 0x0ff01ff8, 0x7c3e1e78, 0x1e787c3e, 0x1ff80ff0, 0x318c3bdc, 0x00000180, // ICON_GEAR
+ 0x01800000, 0x3ffc318c, 0x1c381ff8, 0x781e1818, 0x1818781e, 0x1ff81c38, 0x318c3ffc, 0x00000180, // ICON_GEAR_BIG
+ 0x00000000, 0x08080ff8, 0x08081ffc, 0x0aa80aa8, 0x0aa80aa8, 0x0aa80aa8, 0x08080aa8, 0x00000ff8, // ICON_BIN
+ 0x00000000, 0x00000000, 0x20043ffc, 0x08043f84, 0x04040f84, 0x04040784, 0x000007fc, 0x00000000, // ICON_HAND_POINTER
+ 0x00000000, 0x24400400, 0x00001480, 0x6efe0e00, 0x00000e00, 0x24401480, 0x00000400, 0x00000000, // ICON_LASER
+ 0x00000000, 0x03c00000, 0x08300460, 0x11181118, 0x11181118, 0x04600830, 0x000003c0, 0x00000000, // ICON_COIN
+ 0x00000000, 0x10880080, 0x06c00810, 0x366c07e0, 0x07e00240, 0x00001768, 0x04200240, 0x00000000, // ICON_EXPLOSION
+ 0x00000000, 0x3d280000, 0x2528252c, 0x3d282528, 0x05280528, 0x05e80528, 0x00000000, 0x00000000, // ICON_1UP
+ 0x01800000, 0x03c003c0, 0x018003c0, 0x0ff007e0, 0x0bd00bd0, 0x0a500bd0, 0x02400240, 0x02400240, // ICON_PLAYER
+ 0x01800000, 0x03c003c0, 0x118013c0, 0x03c81ff8, 0x07c003c8, 0x04400440, 0x0c080478, 0x00000000, // ICON_PLAYER_JUMP
+ 0x3ff80000, 0x30183ff8, 0x30183018, 0x3ff83ff8, 0x03000300, 0x03c003c0, 0x03e00300, 0x000003e0, // ICON_KEY
+ 0x3ff80000, 0x3ff83ff8, 0x33983ff8, 0x3ff83398, 0x3ff83ff8, 0x00000540, 0x0fe00aa0, 0x00000fe0, // ICON_DEMON
+ 0x00000000, 0x0ff00000, 0x20041008, 0x25442004, 0x10082004, 0x06000bf0, 0x00000300, 0x00000000, // ICON_TEXT_POPUP
+ 0x00000000, 0x11440000, 0x07f00be8, 0x1c1c0e38, 0x1c1c0c18, 0x07f00e38, 0x11440be8, 0x00000000, // ICON_GEAR_EX
+ 0x00000000, 0x20080000, 0x0c601010, 0x07c00fe0, 0x07c007c0, 0x0c600fe0, 0x20081010, 0x00000000, // ICON_CRACK
+ 0x00000000, 0x20080000, 0x0c601010, 0x04400fe0, 0x04405554, 0x0c600fe0, 0x20081010, 0x00000000, // ICON_CRACK_POINTS
+ 0x00000000, 0x00800080, 0x01c001c0, 0x1ffc3ffe, 0x03e007f0, 0x07f003e0, 0x0c180770, 0x00000808, // ICON_STAR
+ 0x0ff00000, 0x08180810, 0x08100818, 0x0a100810, 0x08180810, 0x08100818, 0x08100810, 0x00001ff8, // ICON_DOOR
+ 0x0ff00000, 0x08100810, 0x08100810, 0x10100010, 0x4f902010, 0x10102010, 0x08100010, 0x00000ff0, // ICON_EXIT
+ 0x00040000, 0x001f000e, 0x0ef40004, 0x12f41284, 0x0ef41214, 0x10040004, 0x7ffc3004, 0x10003000, // ICON_MODE_2D
+ 0x78040000, 0x501f600e, 0x0ef44004, 0x12f41284, 0x0ef41284, 0x10140004, 0x7ffc300c, 0x10003000, // ICON_MODE_3D
+ 0x7fe00000, 0x50286030, 0x47fe4804, 0x44224402, 0x44224422, 0x241275e2, 0x0c06140a, 0x000007fe, // ICON_CUBE
+ 0x7fe00000, 0x5ff87ff0, 0x47fe4ffc, 0x44224402, 0x44224422, 0x241275e2, 0x0c06140a, 0x000007fe, // ICON_CUBE_FACE_TOP
+ 0x7fe00000, 0x50386030, 0x47c2483c, 0x443e443e, 0x443e443e, 0x241e75fe, 0x0c06140e, 0x000007fe, // ICON_CUBE_FACE_LEFT
+ 0x7fe00000, 0x50286030, 0x47fe4804, 0x47fe47fe, 0x47fe47fe, 0x27fe77fe, 0x0ffe17fe, 0x000007fe, // ICON_CUBE_FACE_FRONT
+ 0x7fe00000, 0x50286030, 0x47fe4804, 0x44224402, 0x44224422, 0x3bf27be2, 0x0bfe1bfa, 0x000007fe, // ICON_CUBE_FACE_BOTTOM
+ 0x7fe00000, 0x70286030, 0x7ffe7804, 0x7c227c02, 0x7c227c22, 0x3c127de2, 0x0c061c0a, 0x000007fe, // ICON_CUBE_FACE_RIGHT
+ 0x7fe00000, 0x6fe85ff0, 0x781e77e4, 0x7be27be2, 0x7be27be2, 0x24127be2, 0x0c06140a, 0x000007fe, // ICON_CUBE_FACE_BACK
+ 0x00000000, 0x2a0233fe, 0x22022602, 0x22022202, 0x2a022602, 0x00a033fe, 0x02080110, 0x00000000, // ICON_CAMERA
+ 0x00000000, 0x200c3ffc, 0x000c000c, 0x3ffc000c, 0x30003000, 0x30003000, 0x3ffc3004, 0x00000000, // ICON_SPECIAL
+ 0x00000000, 0x0022003e, 0x012201e2, 0x0100013e, 0x01000100, 0x79000100, 0x4f004900, 0x00007800, // ICON_LINK_NET
+ 0x00000000, 0x44007c00, 0x45004600, 0x00627cbe, 0x00620022, 0x45007cbe, 0x44004600, 0x00007c00, // ICON_LINK_BOXES
+ 0x00000000, 0x0044007c, 0x0010007c, 0x3f100010, 0x3f1021f0, 0x3f100010, 0x3f0021f0, 0x00000000, // ICON_LINK_MULTI
+ 0x00000000, 0x0044007c, 0x00440044, 0x0010007c, 0x00100010, 0x44107c10, 0x440047f0, 0x00007c00, // ICON_LINK
+ 0x00000000, 0x0044007c, 0x00440044, 0x0000007c, 0x00000010, 0x44007c10, 0x44004550, 0x00007c00, // ICON_LINK_BROKE
+ 0x02a00000, 0x22a43ffc, 0x20042004, 0x20042ff4, 0x20042ff4, 0x20042ff4, 0x20042004, 0x00003ffc, // ICON_TEXT_NOTES
+ 0x3ffc0000, 0x20042004, 0x245e27c4, 0x27c42444, 0x2004201e, 0x201e2004, 0x20042004, 0x00003ffc, // ICON_NOTEBOOK
+ 0x00000000, 0x07e00000, 0x04200420, 0x24243ffc, 0x24242424, 0x24242424, 0x3ffc2424, 0x00000000, // ICON_SUITCASE
+ 0x00000000, 0x0fe00000, 0x08200820, 0x40047ffc, 0x7ffc5554, 0x40045554, 0x7ffc4004, 0x00000000, // ICON_SUITCASE_ZIP
+ 0x00000000, 0x20043ffc, 0x3ffc2004, 0x13c81008, 0x100813c8, 0x10081008, 0x1ff81008, 0x00000000, // ICON_MAILBOX
+ 0x00000000, 0x40027ffe, 0x5ffa5ffa, 0x5ffa5ffa, 0x40025ffa, 0x03c07ffe, 0x1ff81ff8, 0x00000000, // ICON_MONITOR
+ 0x0ff00000, 0x6bfe7ffe, 0x7ffe7ffe, 0x68167ffe, 0x08106816, 0x08100810, 0x0ff00810, 0x00000000, // ICON_PRINTER
+ 0x3ff80000, 0xfffe2008, 0x870a8002, 0x904a888a, 0x904a904a, 0x870a888a, 0xfffe8002, 0x00000000, // ICON_PHOTO_CAMERA
+ 0x0fc00000, 0xfcfe0cd8, 0x8002fffe, 0x84428382, 0x84428442, 0x80028382, 0xfffe8002, 0x00000000, // ICON_PHOTO_CAMERA_FLASH
+ 0x00000000, 0x02400180, 0x08100420, 0x20041008, 0x23c42004, 0x22442244, 0x3ffc2244, 0x00000000, // ICON_HOUSE
+ 0x00000000, 0x1c700000, 0x3ff83ef8, 0x3ff83ff8, 0x0fe01ff0, 0x038007c0, 0x00000100, 0x00000000, // ICON_HEART
+ 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x80000000, 0xe000c000, // ICON_CORNER
+ 0x00000000, 0x14001c00, 0x15c01400, 0x15401540, 0x155c1540, 0x15541554, 0x1ddc1554, 0x00000000, // ICON_VERTICAL_BARS
+ 0x00000000, 0x03000300, 0x1b001b00, 0x1b601b60, 0x1b6c1b60, 0x1b6c1b6c, 0x1b6c1b6c, 0x00000000, // ICON_VERTICAL_BARS_FILL
+ 0x00000000, 0x00000000, 0x403e7ffe, 0x7ffe403e, 0x7ffe0000, 0x43fe43fe, 0x00007ffe, 0x00000000, // ICON_LIFE_BARS
+ 0x7ffc0000, 0x43844004, 0x43844284, 0x43844004, 0x42844284, 0x42844284, 0x40044384, 0x00007ffc, // ICON_INFO
+ 0x40008000, 0x10002000, 0x04000800, 0x01000200, 0x00400080, 0x00100020, 0x00040008, 0x00010002, // ICON_CROSSLINE
+ 0x00000000, 0x1ff01ff0, 0x18301830, 0x1f001830, 0x03001f00, 0x00000300, 0x03000300, 0x00000000, // ICON_HELP
+ 0x3ff00000, 0x2abc3550, 0x2aac3554, 0x2aac3554, 0x2aac3554, 0x2aac3554, 0x2aac3554, 0x00003ffc, // ICON_FILETYPE_ALPHA
+ 0x3ff00000, 0x201c2010, 0x22442184, 0x28142424, 0x29942814, 0x2ff42994, 0x20042004, 0x00003ffc, // ICON_FILETYPE_HOME
+ 0x07fe0000, 0x04020402, 0x7fe20402, 0x44224422, 0x44224422, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_LAYERS_VISIBLE
+ 0x07fe0000, 0x04020402, 0x7c020402, 0x44024402, 0x44024402, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_LAYERS
+ 0x00000000, 0x40027ffe, 0x7ffe4002, 0x40024002, 0x40024002, 0x40024002, 0x7ffe4002, 0x00000000, // ICON_WINDOW
+ 0x09100000, 0x09f00910, 0x09100910, 0x00000910, 0x24a2779e, 0x27a224a2, 0x709e20a2, 0x00000000, // ICON_HIDPI
+ 0x3ff00000, 0x201c2010, 0x2a842e84, 0x2e842a84, 0x2ba42004, 0x2aa42aa4, 0x20042ba4, 0x00003ffc, // ICON_FILETYPE_BINARY
+ 0x00000000, 0x00000000, 0x00120012, 0x4a5e4bd2, 0x485233d2, 0x00004bd2, 0x00000000, 0x00000000, // ICON_HEX
+ 0x01800000, 0x381c0660, 0x23c42004, 0x23c42044, 0x13c82204, 0x08101008, 0x02400420, 0x00000180, // ICON_SHIELD
+ 0x007e0000, 0x20023fc2, 0x40227fe2, 0x400a403a, 0x400a400a, 0x400a400a, 0x4008400e, 0x00007ff8, // ICON_FILE_NEW
+ 0x00000000, 0x0042007e, 0x40027fc2, 0x44024002, 0x5f024402, 0x44024402, 0x7ffe4002, 0x00000000, // ICON_FOLDER_ADD
+ 0x44220000, 0x12482244, 0xf3cf0000, 0x14280420, 0x48122424, 0x08100810, 0x1ff81008, 0x03c00420, // ICON_ALARM
+ 0x0aa00000, 0x1ff80aa0, 0x1068700e, 0x1008706e, 0x1008700e, 0x1008700e, 0x0aa01ff8, 0x00000aa0, // ICON_CPU
+ 0x07e00000, 0x04201db8, 0x04a01c38, 0x04a01d38, 0x04a01d38, 0x04a01d38, 0x04201d38, 0x000007e0, // ICON_ROM
+ 0x00000000, 0x03c00000, 0x3c382ff0, 0x3c04380c, 0x01800000, 0x03c003c0, 0x00000180, 0x00000000, // ICON_STEP_OVER
+ 0x01800000, 0x01800180, 0x01800180, 0x03c007e0, 0x00000180, 0x01800000, 0x03c003c0, 0x00000180, // ICON_STEP_INTO
+ 0x01800000, 0x07e003c0, 0x01800180, 0x01800180, 0x00000180, 0x01800000, 0x03c003c0, 0x00000180, // ICON_STEP_OUT
+ 0x00000000, 0x0ff003c0, 0x181c1c34, 0x303c301c, 0x30003000, 0x1c301800, 0x03c00ff0, 0x00000000, // ICON_RESTART
+ 0x00000000, 0x00000000, 0x07e003c0, 0x0ff00ff0, 0x0ff00ff0, 0x03c007e0, 0x00000000, 0x00000000, // ICON_BREAKPOINT_ON
+ 0x00000000, 0x00000000, 0x042003c0, 0x08100810, 0x08100810, 0x03c00420, 0x00000000, 0x00000000, // ICON_BREAKPOINT_OFF
+ 0x00000000, 0x00000000, 0x1ff81ff8, 0x1ff80000, 0x00001ff8, 0x1ff81ff8, 0x00000000, 0x00000000, // ICON_BURGER_MENU
+ 0x00000000, 0x00000000, 0x00880070, 0x0c880088, 0x1e8810f8, 0x3e881288, 0x00000000, 0x00000000, // ICON_CASE_SENSITIVE
+ 0x00000000, 0x02000000, 0x07000a80, 0x07001fc0, 0x02000a80, 0x00300030, 0x00000000, 0x00000000, // ICON_REG_EXP
+ 0x00000000, 0x0042007e, 0x40027fc2, 0x40024002, 0x40024002, 0x40024002, 0x7ffe4002, 0x00000000, // ICON_FOLDER
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x00003ffc, // ICON_FILE
+ 0x1ff00000, 0x20082008, 0x17d02fe8, 0x05400ba0, 0x09200540, 0x23881010, 0x2fe827c8, 0x00001ff0, // ICON_SAND_TIMER
+ 0x01800000, 0x02400240, 0x05a00420, 0x09900990, 0x11881188, 0x21842004, 0x40024182, 0x00003ffc, // ICON_WARNING
+ 0x7ffe0000, 0x4ff24002, 0x4c324ff2, 0x4f824c02, 0x41824f82, 0x41824002, 0x40024182, 0x00007ffe, // ICON_HELP_BOX
+ 0x7ffe0000, 0x41824002, 0x40024182, 0x41824182, 0x41824182, 0x41824182, 0x40024182, 0x00007ffe, // ICON_INFO_BOX
+ 0x01800000, 0x04200240, 0x10080810, 0x7bde2004, 0x0a500a50, 0x08500bd0, 0x08100850, 0x00000ff0, // ICON_PRIORITY
+ 0x01800000, 0x18180660, 0x80016006, 0x98196006, 0x99996666, 0x19986666, 0x01800660, 0x00000000, // ICON_LAYERS_ISO
+ 0x07fe0000, 0x1c020402, 0x74021402, 0x54025402, 0x54025402, 0x500857fe, 0x40205ff8, 0x00007fe0, // ICON_LAYERS2
+ 0x0ffe0000, 0x3ffa0802, 0x7fea200a, 0x402a402a, 0x422a422a, 0x422e422a, 0x40384e28, 0x00007fe0, // ICON_MLAYERS
+ 0x0ffe0000, 0x3ffa0802, 0x7fea200a, 0x402a402a, 0x5b2a512a, 0x512e552a, 0x40385128, 0x00007fe0, // ICON_MAPS
+ 0x04200000, 0x1cf00c60, 0x11f019f0, 0x0f3807b8, 0x1e3c0f3c, 0x1c1c1e1c, 0x1e3c1c1c, 0x00000f70, // ICON_HOT
+ 0x00000000, 0x20803f00, 0x2a202e40, 0x20082e10, 0x08021004, 0x02040402, 0x00900108, 0x00000060, // ICON_LABEL
+ 0x00000000, 0x042007e0, 0x47e27c3e, 0x4ffa4002, 0x47fa4002, 0x4ffa4002, 0x7ffe4002, 0x00000000, // ICON_NAME_ID
+ 0x7fe00000, 0x402e4020, 0x43ce5e0a, 0x40504078, 0x438e4078, 0x402e5e0a, 0x7fe04020, 0x00000000, // ICON_SLICING
+ 0x00000000, 0x40027ffe, 0x47c24002, 0x55425d42, 0x55725542, 0x50125552, 0x10105016, 0x00001ff0, // ICON_MANUAL_CONTROL
+ 0x7ffe0000, 0x43c24002, 0x48124422, 0x500a500a, 0x500a500a, 0x44224812, 0x400243c2, 0x00007ffe, // ICON_COLLISION
+ 0x03c00000, 0x10080c30, 0x21842184, 0x4ff24182, 0x41824ff2, 0x21842184, 0x0c301008, 0x000003c0, // ICON_CIRCLE_ADD
+ 0x03c00000, 0x1ff80ff0, 0x3e7c3e7c, 0x700e7e7e, 0x7e7e700e, 0x3e7c3e7c, 0x0ff01ff8, 0x000003c0, // ICON_CIRCLE_ADD_FILL
+ 0x03c00000, 0x10080c30, 0x21842184, 0x41824182, 0x40024182, 0x21842184, 0x0c301008, 0x000003c0, // ICON_CIRCLE_WARNING
+ 0x03c00000, 0x1ff80ff0, 0x3e7c3e7c, 0x7e7e7e7e, 0x7ffe7e7e, 0x3e7c3e7c, 0x0ff01ff8, 0x000003c0, // ICON_CIRCLE_WARNING_FILL
+ 0x00000000, 0x10041ffc, 0x10841004, 0x13e41084, 0x10841084, 0x10041004, 0x00001ffc, 0x00000000, // ICON_BOX_MORE
+ 0x00000000, 0x1ffc1ffc, 0x1f7c1ffc, 0x1c1c1f7c, 0x1f7c1f7c, 0x1ffc1ffc, 0x00001ffc, 0x00000000, // ICON_BOX_MORE_FILL
+ 0x00000000, 0x1ffc1ffc, 0x1ffc1ffc, 0x1c1c1ffc, 0x1ffc1ffc, 0x1ffc1ffc, 0x00001ffc, 0x00000000, // ICON_BOX_MINUS
+ 0x00000000, 0x10041ffc, 0x10041004, 0x13e41004, 0x10041004, 0x10041004, 0x00001ffc, 0x00000000, // ICON_BOX_MINUS_FILL
+ 0x07fe0000, 0x055606aa, 0x7ff606aa, 0x55766eba, 0x55766eaa, 0x55606ffe, 0x55606aa0, 0x00007fe0, // ICON_UNION
+ 0x07fe0000, 0x04020402, 0x7fe20402, 0x456246a2, 0x456246a2, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_INTERSECTION
+ 0x07fe0000, 0x055606aa, 0x7ff606aa, 0x4436442a, 0x4436442a, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_DIFFERENCE
+ 0x03c00000, 0x10080c30, 0x20042004, 0x60064002, 0x47e2581a, 0x20042004, 0x0c301008, 0x000003c0, // ICON_SPHERE
+ 0x03e00000, 0x08080410, 0x0c180808, 0x08080be8, 0x08080808, 0x08080808, 0x04100808, 0x000003e0, // ICON_CYLINDER
+ 0x00800000, 0x01400140, 0x02200220, 0x04100410, 0x08080808, 0x1c1c13e4, 0x08081004, 0x000007f0, // ICON_CONE
+ 0x00000000, 0x07e00000, 0x20841918, 0x40824082, 0x40824082, 0x19182084, 0x000007e0, 0x00000000, // ICON_ELLIPSOID
+ 0x00000000, 0x00000000, 0x20041ff8, 0x40024002, 0x40024002, 0x1ff82004, 0x00000000, 0x00000000, // ICON_CAPSULE
+ 0x3ff00000, 0x201c2010, 0x21042004, 0x22842384, 0x264426c4, 0x2c242fe4, 0x20043e74, 0x00003ffc, // ICON_FILETYPE_FONT
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x27742004, 0x29742944, 0x27742944, 0x20042004, 0x00003ffc, // ICON_FILETYPE_3D
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x24242244, 0x24242814, 0x20042244, 0x00003ffc, // ICON_FILETYPE_XML
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x21042f04, 0x21042104, 0x20042f04, 0x00003ffc, // ICON_FILETYPE_C
+ 0x3ff00000, 0x201c2010, 0x23842004, 0x2bf42a04, 0x2fd42814, 0x21c42054, 0x20042004, 0x00003ffc, // ICON_FILETYPE_PYTHON
+ 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x22842ee4, 0x28a42e84, 0x20042e64, 0x00003ffc, // ICON_FILETYPE_JS
+ 0x3ff00000, 0x241c2010, 0x2a8c3104, 0x28242454, 0x2ed42004, 0x2a542a54, 0x20042ed4, 0x00003ffc, // ICON_FILETYPE_ICON
+ 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, // ICON_257
+};
+
+// NOTE: A pointer to current icons array should be defined
+static unsigned int *guiIconsPtr = guiIcons;
+
+#endif // !RAYGUI_NO_ICONS && !RAYGUI_CUSTOM_ICONS
+
+#ifndef RAYGUI_ICON_SIZE
+ #define RAYGUI_ICON_SIZE 0
+#endif
+
+// WARNING: Those values define the total size of the style data array,
+// if changed, previous saved styles could become incompatible
+#define RAYGUI_MAX_CONTROLS 16 // Maximum number of controls
+#define RAYGUI_MAX_PROPS_BASE 16 // Maximum number of base properties
+#define RAYGUI_MAX_PROPS_EXTENDED 8 // Maximum number of extended properties
+
+//----------------------------------------------------------------------------------
+// Module Types and Structures Definition
+//----------------------------------------------------------------------------------
+// Gui control property style color element
+typedef enum { BORDER = 0, BASE, TEXT, OTHER } GuiPropertyElement;
+
+//----------------------------------------------------------------------------------
+// Global Variables Definition
+//----------------------------------------------------------------------------------
+static GuiState guiState = STATE_NORMAL; // Gui global state, if !STATE_NORMAL, forces defined state
+
+static Font guiFont = { 0 }; // Gui current font (WARNING: highly coupled to raylib)
+static char guiFontName[32] = { 0 }; // Gui font filename, can be loaded from .rgs (Version: >=600)
+static bool guiLocked = false; // Gui lock state (no inputs processed)
+static float guiAlpha = 1.0f; // Gui controls transparency
+
+static unsigned int guiIconScale = 1; // Gui icon default scale (if icons enabled)
+static unsigned int guiIconFontOffsetY = 0; // Gui icon font atlas offset (if icons backed)
+
+static bool guiTooltip = false; // Tooltip enabled/disabled
+static const char *guiTooltipPtr = NULL; // Tooltip string pointer (string provided by user)
+
+static bool guiControlExclusiveMode = false; // Gui control exclusive mode (no inputs processed except current control)
+static Rectangle guiControlExclusiveRec = { 0 }; // Gui control exclusive bounds rectangle, used as an unique identifier
+
+static int textBoxCursorIndex = 0; // Cursor index, shared by all GuiTextBox*()
+//static int blinkCursorFrameCounter = 0; // Frame counter for cursor blinking
+static int autoCursorCounter = 0; // Frame counter for automatic repeated cursor movement on key-down (cooldown and delay)
+
+//----------------------------------------------------------------------------------
+// Style data array for all gui style properties (allocated on data segment by default)
+//
+// NOTE 1: First set of BASE properties are generic to all controls but could be individually
+// overwritten per control, first set of EXTENDED properties are generic to all controls and
+// can not be overwritten individually but custom EXTENDED properties can be used by control
+//
+// NOTE 2: A new style set could be loaded over this array using GuiLoadStyle(),
+// but default gui style could always be recovered with GuiLoadStyleDefault()
+//
+// guiStyle size is by default: 16*(16 + 8) = 384*4 = 1536 bytes = 1.5 KB
+//----------------------------------------------------------------------------------
+static unsigned int guiStyle[RAYGUI_MAX_CONTROLS*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED)] = { 0 };
+
+static bool guiStyleLoaded = false; // Style loaded flag for lazy style initialization
+
+//----------------------------------------------------------------------------------
+// Standalone Mode Functions Declaration
+//
+// NOTE: raygui depend on some raylib input and drawing functions
+// To use raygui as standalone library, below functions must be defined by the user
+//----------------------------------------------------------------------------------
+#if defined(RAYGUI_STANDALONE)
+
+#define KEY_RIGHT 262
+#define KEY_LEFT 263
+#define KEY_DOWN 264
+#define KEY_UP 265
+#define KEY_BACKSPACE 259
+#define KEY_ENTER 257
+
+#define MOUSE_LEFT_BUTTON 0
+
+// Input required functions
+//-------------------------------------------------------------------------------
+static Vector2 GetMousePosition(void);
+static float GetMouseWheelMove(void);
+static bool IsMouseButtonDown(int button);
+static bool IsMouseButtonPressed(int button);
+static bool IsMouseButtonReleased(int button);
+
+static bool IsKeyDown(int key);
+static bool IsKeyPressed(int key);
+static int GetCharPressed(void); // -- GuiTextBox(), GuiValueBox()
+//-------------------------------------------------------------------------------
+
+// Drawing required functions
+//-------------------------------------------------------------------------------
+static void DrawRectangle(int x, int y, int width, int height, Color color); // -- GuiDrawRectangle()
+static void DrawRectangleGradientEx(Rectangle rec, Color col1, Color col2, Color col3, Color col4); // -- GuiColorPicker()
+//-------------------------------------------------------------------------------
+
+// Text required functions
+//-------------------------------------------------------------------------------
+static Font GetFontDefault(void); // -- GuiLoadStyleDefault()
+static Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount); // -- GuiLoadStyle(), load font
+
+static Texture2D LoadTextureFromImage(Image image); // -- GuiLoadStyle(), required to load texture from embedded font atlas image
+static void SetShapesTexture(Texture2D tex, Rectangle rec); // -- GuiLoadStyle(), required to set shapes rec to font white rec (optimization)
+
+static char *LoadFileText(const char *fileName); // -- GuiLoadStyle(), required to load charset data
+static void UnloadFileText(char *text); // -- GuiLoadStyle(), required to unload charset data
+
+static const char *GetDirectoryPath(const char *filePath); // -- GuiLoadStyle(), required to find charset/font file from text .rgs
+
+static int *LoadCodepoints(const char *text, int *count); // -- GuiLoadStyle(), required to load required font codepoints list
+static void UnloadCodepoints(int *codepoints); // -- GuiLoadStyle(), required to unload codepoints list
+
+static unsigned char *DecompressData(const unsigned char *compData, int compDataSize, int *dataSize); // -- GuiLoadStyle()
+
+static Vector2 MeasureTextEx(Font font, const char *text, float fontSize, float spacing); // Measure string size for Font
+//-------------------------------------------------------------------------------
+
+// raylib functions already implemented in raygui
+//-------------------------------------------------------------------------------
+static Color GetColor(int hexValue); // Returns a Color struct from hexadecimal value
+static Color Fade(Color color, float alpha); // Get color with alpha applied, alpha goes from 0.0f to 1.0f
+static int ColorToInt(Color color); // Returns hexadecimal value for a Color
+static bool CheckCollisionPointRec(Vector2 point, Rectangle rec); // Check if point is inside rectangle
+static const char *TextFormat(const char *text, ...); // Formatting of text with variables to 'embed'
+static int TextToInteger(const char *text); // Get integer value from text
+static float TextToFloat(const char *text); // Get float value from text
+
+static int GetCodepointNext(const char *text, int *codepointSize); // Get next codepoint in a UTF-8 encoded text
+static const char *CodepointToUTF8(int codepoint, int *byteSize); // Encode codepoint into UTF-8 text (char array size returned as parameter)
+
+static void DrawRectangleGradientV(int posX, int posY, int width, int height, Color color1, Color color2); // Draw rectangle vertical gradient
+//-------------------------------------------------------------------------------
+
+#endif // RAYGUI_STANDALONE
+
+//----------------------------------------------------------------------------------
+// Module Internal Functions Declaration
+//----------------------------------------------------------------------------------
+static int GetLineWidth(const char *text); // Get text line width (stops at '\n' or '\0')
+static Rectangle GetTextBounds(int control, Rectangle bounds); // Get text bounds considering control bounds
+static const char *GetTextIcon(const char *text, int *iconId); // Get text icon if provided and move text cursor
+
+static void GuiDrawText(const char *text, Rectangle textBounds, int alignment, Color tint); // Gui draw text using default font
+static void GuiDrawRectangle(Rectangle rec, int borderWidth, Color borderColor, Color color); // Gui draw rectangle using default raygui style
+
+static char **GuiTextSplit(const char *text, char delimiter, int *count); // Split controls text into multiple strings
+static Vector3 ConvertHSVtoRGB(Vector3 hsv); // Convert color data from HSV to RGB
+static Vector3 ConvertRGBtoHSV(Vector3 rgb); // Convert color data from RGB to HSV
+
+static void GuiTooltip(Rectangle controlRec); // Draw tooltip using control rec position
+static int GuiScrollBar(Rectangle bounds, int value, int minValue, int maxValue); // Scroll bar control, used by GuiScrollPanel()
+static int GuiFontIconBaking(Image *imFont, Font font, Rectangle *whiteRec); // Update font image atlas to append raygui icons
+
+static Color GuiFade(Color color, float alpha); // Fade color by an alpha factor
+
+//----------------------------------------------------------------------------------
+// Gui Setup Functions Definition
+//----------------------------------------------------------------------------------
+// Enable gui global state
+// NOTE: Checking for STATE_DISABLED to avoid messing custom global state setups
+void GuiEnable(void) { if (guiState == STATE_DISABLED) guiState = STATE_NORMAL; }
+
+// Disable gui global state
+// NOTE: Checking for STATE_NORMAL to avoid messing custom global state setups
+void GuiDisable(void) { if (guiState == STATE_NORMAL) guiState = STATE_DISABLED; }
+
+// Lock gui global state
+void GuiLock(void) { guiLocked = true; }
+
+// Unlock gui global state
+void GuiUnlock(void) { guiLocked = false; }
+
+// Check if gui is locked (global state)
+bool GuiIsLocked(void) { return guiLocked; }
+
+// Set gui controls alpha global state
+void GuiSetAlpha(float alpha)
+{
+ if (alpha < 0.0f) alpha = 0.0f;
+ else if (alpha > 1.0f) alpha = 1.0f;
+
+ guiAlpha = alpha;
+}
+
+// Set gui state (global state)
+void GuiSetState(int state) { guiState = (GuiState)state; }
+
+// Get gui state (global state)
+int GuiGetState(void) { return guiState; }
+
+// Set custom gui font
+// NOTE: Font loading/unloading is external to raygui
+void GuiSetFont(Font font)
+{
+ if (font.texture.id > 0)
+ {
+ // NOTE: If a font is tried to be set but default style has not been lazily loaded first,
+ // it will be overwritten, so default style loading needs to be forced first
+ if (!guiStyleLoaded) GuiLoadStyleDefault();
+
+ guiFont = font;
+ }
+}
+
+// Get custom gui font
+Font GuiGetFont(void)
+{
+ return guiFont;
+}
+
+// Set control style property value
+void GuiSetStyle(int control, int property, int value)
+{
+ if (!guiStyleLoaded) GuiLoadStyleDefault();
+ guiStyle[control*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED) + property] = value;
+
+ // Default properties are propagated to all controls
+ if ((control == 0) && (property < RAYGUI_MAX_PROPS_BASE))
+ {
+ for (int i = 1; i < RAYGUI_MAX_CONTROLS; i++) guiStyle[i*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED) + property] = value;
+ }
+}
+
+// Get control style property value
+int GuiGetStyle(int control, int property)
+{
+ if (!guiStyleLoaded) GuiLoadStyleDefault();
+ return guiStyle[control*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED) + property];
+}
+
+//----------------------------------------------------------------------------------
+// Gui Controls Functions Definition
+//----------------------------------------------------------------------------------
+
+// Window Box control
+int GuiWindowBox(Rectangle bounds, const char *title)
+{
+ // Window title bar height (including borders)
+ // NOTE: This define is also used by GuiMessageBox() and GuiTextInputBox()
+ #if !defined(RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT)
+ #define RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT 24
+ #endif
+
+ #if !defined(RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT)
+ #define RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT 18
+ #endif
+
+ int result = 0;
+ //GuiState state = guiState;
+
+ int statusBarHeight = RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT;
+ int statusBorderWidth = GuiGetStyle(STATUSBAR, BORDER_WIDTH);
+
+ Rectangle statusBar = { bounds.x, bounds.y, bounds.width, (float)statusBarHeight };
+ if (bounds.height < statusBarHeight*2.0f) bounds.height = statusBarHeight*2.0f;
+
+ const float vPadding = statusBarHeight/2.0f - RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT/2.0f;
+ Rectangle windowPanel = { bounds.x, bounds.y + (float)statusBarHeight - (float)statusBorderWidth, bounds.width, bounds.height - (float)statusBarHeight + (float)statusBorderWidth };
+ Rectangle closeButtonRec = { statusBar.x + statusBar.width - (float)statusBorderWidth - RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT - vPadding,
+ statusBar.y + vPadding, RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT, RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT };
+
+ // Update control
+ //--------------------------------------------------------------------
+ // NOTE: Logic is directly managed by button
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiPanel(windowPanel, NULL); // Draw window base
+ GuiStatusBar(statusBar, title); // Draw window header as status bar
+
+ // Draw window close button
+ int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH);
+ int tempTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT);
+ GuiSetStyle(BUTTON, BORDER_WIDTH, 1);
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+#if defined(RAYGUI_NO_ICONS)
+ result = GuiButton(closeButtonRec, "x");
+#else
+ result = GuiButton(closeButtonRec, GuiIconText(ICON_CROSS_SMALL, NULL));
+#endif
+ GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth);
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlignment);
+ //--------------------------------------------------------------------
+
+ return result; // Window close button clicked: result = 1
+}
+
+// Group Box control with text name
+int GuiGroupBox(Rectangle bounds, const char *text)
+{
+ #if !defined(RAYGUI_GROUPBOX_LINE_THICK)
+ #define RAYGUI_GROUPBOX_LINE_THICK 1
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y, RAYGUI_GROUPBOX_LINE_THICK, bounds.height }, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR)));
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height - 1, bounds.width, RAYGUI_GROUPBOX_LINE_THICK }, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR)));
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - 1, bounds.y, RAYGUI_GROUPBOX_LINE_THICK, bounds.height }, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR)));
+
+ GuiLine(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y - GuiGetStyle(DEFAULT, TEXT_SIZE)/2, bounds.width, (float)GuiGetStyle(DEFAULT, TEXT_SIZE) }, text);
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Line control
+int GuiLine(Rectangle bounds, const char *text)
+{
+ #if !defined(RAYGUI_LINE_MARGIN_TEXT)
+ #define RAYGUI_LINE_MARGIN_TEXT 12
+ #endif
+ #if !defined(RAYGUI_LINE_TEXT_PADDING)
+ #define RAYGUI_LINE_TEXT_PADDING 4
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+
+ Color color = GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR));
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (text == NULL) GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height/2, bounds.width, 1 }, 0, BLANK, color);
+ else
+ {
+ Rectangle textBounds = { 0 };
+ textBounds.width = (float)GuiGetTextWidth(text) + 2;
+ textBounds.height = bounds.height;
+ textBounds.x = bounds.x + RAYGUI_LINE_MARGIN_TEXT;
+ textBounds.y = bounds.y;
+
+ // Draw line with embedded text label: "--- text --------------"
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height/2, RAYGUI_LINE_MARGIN_TEXT - RAYGUI_LINE_TEXT_PADDING, 1 }, 0, BLANK, color);
+ GuiDrawText(text, textBounds, TEXT_ALIGN_LEFT, color);
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + 12 + textBounds.width + 4, bounds.y + bounds.height/2, bounds.width - textBounds.width - RAYGUI_LINE_MARGIN_TEXT - RAYGUI_LINE_TEXT_PADDING, 1 }, 0, BLANK, color);
+ }
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Panel control
+int GuiPanel(Rectangle bounds, const char *text)
+{
+ #if !defined(RAYGUI_PANEL_BORDER_WIDTH)
+ #define RAYGUI_PANEL_BORDER_WIDTH 1
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+
+ // Text will be drawn as a header bar (if provided)
+ Rectangle statusBar = { bounds.x, bounds.y, bounds.width, (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT };
+ if ((text != NULL) && (bounds.height < RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f)) bounds.height = RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f;
+
+ if (text != NULL)
+ {
+ // Move panel bounds after the header bar
+ bounds.y += (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 1;
+ bounds.height -= (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 1;
+ }
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (text != NULL) GuiStatusBar(statusBar, text); // Draw panel header as status bar
+
+ GuiDrawRectangle(bounds, RAYGUI_PANEL_BORDER_WIDTH, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR)),
+ GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BASE_COLOR_DISABLED : (int)BACKGROUND_COLOR)));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Scroll Panel control
+int GuiScrollPanel(Rectangle bounds, const char *text, Rectangle content, Vector2 *scroll, Rectangle *view)
+{
+ #define RAYGUI_MIN_SCROLLBAR_WIDTH 40
+ #define RAYGUI_MIN_SCROLLBAR_HEIGHT 40
+ #define RAYGUI_MIN_MOUSE_WHEEL_SPEED 20
+
+ int result = 0;
+ GuiState state = guiState;
+
+ Rectangle temp = { 0 };
+ if (view == NULL) view = &temp;
+
+ Vector2 scrollPos = { 0.0f, 0.0f };
+ if (scroll != NULL) scrollPos = *scroll;
+
+ // Text will be drawn as a header bar (if provided)
+ Rectangle statusBar = { bounds.x, bounds.y, bounds.width, (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT };
+ if (bounds.height < RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f) bounds.height = RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f;
+
+ if (text != NULL)
+ {
+ // Move panel bounds after the header bar
+ bounds.y += (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 1;
+ bounds.height -= (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT + 1;
+ }
+
+ bool hasHorizontalScrollBar = (content.width > bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH))? true : false;
+ bool hasVerticalScrollBar = (content.height > bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH))? true : false;
+
+ // Recheck to account for the other scrollbar being visible
+ if (!hasHorizontalScrollBar) hasHorizontalScrollBar = (hasVerticalScrollBar && (content.width > (bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH))))? true : false;
+ if (!hasVerticalScrollBar) hasVerticalScrollBar = (hasHorizontalScrollBar && (content.height > (bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH))))? true : false;
+
+ int horizontalScrollBarWidth = hasHorizontalScrollBar? GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH) : 0;
+ int verticalScrollBarWidth = hasVerticalScrollBar? GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH) : 0;
+ Rectangle horizontalScrollBar = {
+ (float)((GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)bounds.x + verticalScrollBarWidth : (float)bounds.x) + GuiGetStyle(DEFAULT, BORDER_WIDTH),
+ (float)bounds.y + bounds.height - horizontalScrollBarWidth - GuiGetStyle(DEFAULT, BORDER_WIDTH),
+ (float)bounds.width - verticalScrollBarWidth - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH),
+ (float)horizontalScrollBarWidth
+ };
+ Rectangle verticalScrollBar = {
+ (float)((GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)bounds.x + GuiGetStyle(DEFAULT, BORDER_WIDTH) : (float)bounds.x + bounds.width - verticalScrollBarWidth - GuiGetStyle(DEFAULT, BORDER_WIDTH)),
+ (float)bounds.y + GuiGetStyle(DEFAULT, BORDER_WIDTH),
+ (float)verticalScrollBarWidth,
+ (float)bounds.height - horizontalScrollBarWidth - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH)
+ };
+
+ // Make sure scroll bars have a minimum width/height
+ if (horizontalScrollBar.width < RAYGUI_MIN_SCROLLBAR_WIDTH) horizontalScrollBar.width = RAYGUI_MIN_SCROLLBAR_WIDTH;
+ if (verticalScrollBar.height < RAYGUI_MIN_SCROLLBAR_HEIGHT) verticalScrollBar.height = RAYGUI_MIN_SCROLLBAR_HEIGHT;
+
+ // Calculate view area (area without the scrollbars)
+ *view = (GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)?
+ RAYGUI_CLITERAL(Rectangle){ bounds.x + verticalScrollBarWidth + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.y + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth, bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth } :
+ RAYGUI_CLITERAL(Rectangle){ bounds.x + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.y + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth, bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth };
+
+ // Clip view area to the actual content size
+ if (view->width > content.width) view->width = content.width;
+ if (view->height > content.height) view->height = content.height;
+
+ float horizontalMin = hasHorizontalScrollBar? ((GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)-verticalScrollBarWidth : 0) - (float)GuiGetStyle(DEFAULT, BORDER_WIDTH) : (((float)GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)-verticalScrollBarWidth : 0) - (float)GuiGetStyle(DEFAULT, BORDER_WIDTH);
+ float horizontalMax = hasHorizontalScrollBar? content.width - bounds.width + (float)verticalScrollBarWidth + GuiGetStyle(DEFAULT, BORDER_WIDTH) - (((float)GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)verticalScrollBarWidth : 0) : (float)-GuiGetStyle(DEFAULT, BORDER_WIDTH);
+ float verticalMin = -(float)GuiGetStyle(DEFAULT, BORDER_WIDTH);
+ float verticalMax = hasVerticalScrollBar? content.height - bounds.height + (float)horizontalScrollBarWidth + (float)GuiGetStyle(DEFAULT, BORDER_WIDTH) : (float)-GuiGetStyle(DEFAULT, BORDER_WIDTH);
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ // Check button state
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else state = STATE_FOCUSED;
+
+#if defined(SUPPORT_SCROLLBAR_KEY_INPUT)
+ if (hasHorizontalScrollBar)
+ {
+ if (GUI_KEY_DOWN(KEY_RIGHT)) scrollPos.x -= GuiGetStyle(SCROLLBAR, SCROLL_SPEED);
+ if (GUI_KEY_DOWN(KEY_LEFT)) scrollPos.x += GuiGetStyle(SCROLLBAR, SCROLL_SPEED);
+ }
+
+ if (hasVerticalScrollBar)
+ {
+ if (GUI_KEY_DOWN(KEY_DOWN)) scrollPos.y -= GuiGetStyle(SCROLLBAR, SCROLL_SPEED);
+ if (GUI_KEY_DOWN(KEY_UP)) scrollPos.y += GuiGetStyle(SCROLLBAR, SCROLL_SPEED);
+ }
+#endif
+ float scrollDelta = GUI_SCROLL_DELTA;
+
+ // Set scrolling speed with mouse wheel based on ratio between bounds and content
+ Vector2 scrollSpeed = { content.width/bounds.width, content.height/bounds.height };
+ if (scrollSpeed.x < RAYGUI_MIN_MOUSE_WHEEL_SPEED) scrollSpeed.x = RAYGUI_MIN_MOUSE_WHEEL_SPEED;
+ if (scrollSpeed.y < RAYGUI_MIN_MOUSE_WHEEL_SPEED) scrollSpeed.y = RAYGUI_MIN_MOUSE_WHEEL_SPEED;
+
+ // Horizontal and vertical scrolling with mouse wheel
+ if (hasHorizontalScrollBar && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_LEFT_SHIFT))) scrollPos.x += scrollDelta*scrollSpeed.x;
+ else scrollPos.y += scrollDelta*scrollSpeed.y; // Vertical scroll
+ }
+ }
+
+ // Normalize scroll values
+ if (scrollPos.x > -horizontalMin) scrollPos.x = -horizontalMin;
+ if (scrollPos.x < -horizontalMax) scrollPos.x = -horizontalMax;
+ if (scrollPos.y > -verticalMin) scrollPos.y = -verticalMin;
+ if (scrollPos.y < -verticalMax) scrollPos.y = -verticalMax;
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (text != NULL) GuiStatusBar(statusBar, text); // Draw panel header as status bar
+
+ GuiDrawRectangle(bounds, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); // Draw background
+
+ // Save size of the scrollbar slider
+ const int slider = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE);
+
+ // Draw horizontal scrollbar if visible
+ if (hasHorizontalScrollBar)
+ {
+ // Change scrollbar slider size to show the diff in size between the content width and the widget width
+ GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, (int)(((bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth)/(int)content.width)*((int)bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth)));
+ scrollPos.x = (float)-GuiScrollBar(horizontalScrollBar, (int)-scrollPos.x, (int)horizontalMin, (int)horizontalMax);
+ }
+ else scrollPos.x = 0.0f;
+
+ // Draw vertical scrollbar if visible
+ if (hasVerticalScrollBar)
+ {
+ // Change scrollbar slider size to show the diff in size between the content height and the widget height
+ GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, (int)(((bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth)/(int)content.height)*((int)bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth)));
+ scrollPos.y = (float)-GuiScrollBar(verticalScrollBar, (int)-scrollPos.y, (int)verticalMin, (int)verticalMax);
+ }
+ else scrollPos.y = 0.0f;
+
+ // Draw detail corner rectangle if both scroll bars are visible
+ if (hasHorizontalScrollBar && hasVerticalScrollBar)
+ {
+ Rectangle corner = { (GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (bounds.x + GuiGetStyle(DEFAULT, BORDER_WIDTH) + 2) : (horizontalScrollBar.x + horizontalScrollBar.width + 2), verticalScrollBar.y + verticalScrollBar.height + 2, (float)horizontalScrollBarWidth - 4, (float)verticalScrollBarWidth - 4 };
+ GuiDrawRectangle(corner, 0, BLANK, GetColor(GuiGetStyle(LISTVIEW, TEXT + (state*3))));
+ }
+
+ // Draw scrollbar lines depending on current state
+ GuiDrawRectangle(bounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER + (state*3))), BLANK);
+
+ // Set scrollbar slider size back to the way it was before
+ GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, slider);
+ //--------------------------------------------------------------------
+
+ if (scroll != NULL) *scroll = scrollPos;
+
+ return result;
+}
+
+// Label control
+int GuiLabel(Rectangle bounds, const char *text)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ // Update control
+ //--------------------------------------------------------------------
+ //...
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawText(text, GetTextBounds(LABEL, bounds), GuiGetStyle(LABEL, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Button control, returns true when clicked
+int GuiButton(Rectangle bounds, const char *text)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ // Check button state
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else state = STATE_FOCUSED;
+
+ if (GUI_BUTTON_RELEASED) result = 1;
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, GuiGetStyle(BUTTON, BORDER_WIDTH), GetColor(GuiGetStyle(BUTTON, BORDER + (state*3))), GetColor(GuiGetStyle(BUTTON, BASE + (state*3))));
+ GuiDrawText(text, GetTextBounds(BUTTON, bounds), GuiGetStyle(BUTTON, TEXT_ALIGNMENT), GetColor(GuiGetStyle(BUTTON, TEXT + (state*3))));
+
+ if (state == STATE_FOCUSED) GuiTooltip(bounds);
+ //------------------------------------------------------------------
+
+ return result; // Button pressed: result = 1
+}
+
+// Label button control
+int GuiLabelButton(Rectangle bounds, const char *text)
+{
+ GuiState state = guiState;
+ bool pressed = false;
+
+ // NOTE: Force bounds.width to be all text
+ float textWidth = (float)GuiGetTextWidth(text);
+ if ((bounds.width - 2*GuiGetStyle(LABEL, BORDER_WIDTH) - 2*GuiGetStyle(LABEL, TEXT_PADDING)) < textWidth) bounds.width = textWidth + 2*GuiGetStyle(LABEL, BORDER_WIDTH) + 2*GuiGetStyle(LABEL, TEXT_PADDING) + 2;
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ // Check checkbox state
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else state = STATE_FOCUSED;
+
+ if (GUI_BUTTON_RELEASED) pressed = true;
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawText(text, GetTextBounds(LABEL, bounds), GuiGetStyle(LABEL, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ //--------------------------------------------------------------------
+
+ return pressed;
+}
+
+// Toggle Button control
+int GuiToggle(Rectangle bounds, const char *text, bool *active)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ bool temp = false;
+ if (active == NULL) active = &temp;
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ // Check toggle button state
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else if (GUI_BUTTON_RELEASED)
+ {
+ state = STATE_NORMAL;
+ *active = !(*active);
+ }
+ else state = STATE_FOCUSED;
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (state == STATE_NORMAL)
+ {
+ GuiDrawRectangle(bounds, GuiGetStyle(TOGGLE, BORDER_WIDTH), GetColor(GuiGetStyle(TOGGLE, ((*active)? BORDER_COLOR_PRESSED : (BORDER + state*3)))), GetColor(GuiGetStyle(TOGGLE, ((*active)? BASE_COLOR_PRESSED : (BASE + state*3)))));
+ GuiDrawText(text, GetTextBounds(TOGGLE, bounds), GuiGetStyle(TOGGLE, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TOGGLE, ((*active)? TEXT_COLOR_PRESSED : (TEXT + state*3)))));
+ }
+ else
+ {
+ GuiDrawRectangle(bounds, GuiGetStyle(TOGGLE, BORDER_WIDTH), GetColor(GuiGetStyle(TOGGLE, BORDER + state*3)), GetColor(GuiGetStyle(TOGGLE, BASE + state*3)));
+ GuiDrawText(text, GetTextBounds(TOGGLE, bounds), GuiGetStyle(TOGGLE, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TOGGLE, TEXT + state*3)));
+ }
+
+ if (state == STATE_FOCUSED) GuiTooltip(bounds);
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Toggle Group control
+int GuiToggleGroup(Rectangle bounds, const char *text, int *active)
+{
+ int result = 0;
+
+ #if !defined(RAYGUI_TOGGLEGROUP_MAX_ITEM_TEXT_SIZE)
+ #define RAYGUI_TOGGLEGROUP_MAX_ITEM_TEXT_SIZE 256
+ #endif
+
+ // One toggle group item text
+ static char itemText[RAYGUI_TOGGLEGROUP_MAX_ITEM_TEXT_SIZE] = { 0 };
+ memset(itemText, 0, RAYGUI_TOGGLEGROUP_MAX_ITEM_TEXT_SIZE);
+
+ int temp = 0;
+ if (active == NULL) active = &temp;
+
+ bool toggle = false; // Required for individual toggles
+
+ const char *textPtr = text;
+ bool itemReady = false;
+ float initBoundsX = bounds.x;
+ float initBoundsY = bounds.y;
+
+ if (GuiGetStyle(TOGGLE, GROUP_WIDTH_FULL))
+ {
+ // Calculate item width considring all horizontal items
+ // NOTE: bounds.height still considers individual items height
+ int itemCount = 1;
+ for (int c = 0; textPtr[c] != '\0'; c++) { if (textPtr[c] == ';') itemCount++; }
+ bounds.width /= itemCount;
+ }
+
+ // Text parsing needed to consider potential row and col entries (vertical/horizontal layout)
+ // when '\n' found move vertically next toggle, when ';' found move horizontally
+ for (int c = 0, k = 0, itemIndex = 0, row = 0, col = 0, exit = 0; exit == 0; c++)
+ {
+ // Process text to get items one by one
+ // NOTE: Setting columns and rows index properly
+ if (textPtr[c] == '\n')
+ {
+ row++;
+ col = 0;
+ itemReady = true;
+ }
+ else if (textPtr[c] == ';')
+ {
+ col++;
+ itemReady = true;
+ }
+ else if (textPtr[c] == '\0')
+ {
+ itemReady = true;
+ exit = 1;
+ }
+ else
+ {
+ itemText[k] = textPtr[c];
+ k++;
+ }
+
+ if (itemReady)
+ {
+ // When a next item is ready, draw its toggle
+ if (itemIndex == (*active))
+ {
+ toggle = true;
+ GuiToggle(bounds, itemText, &toggle);
+ }
+ else
+ {
+ toggle = false;
+ GuiToggle(bounds, itemText, &toggle);
+ if (toggle) *active = itemIndex;
+ }
+
+ // Calculate next item position
+ bounds.x = initBoundsX + col*(bounds.width + GuiGetStyle(TOGGLE, GROUP_PADDING));
+ bounds.y = initBoundsY + row*(bounds.height + GuiGetStyle(TOGGLE, GROUP_PADDING));
+
+ itemIndex++;
+ itemReady = false;
+ memset(itemText, 0, k + 1);
+ k = 0;
+ }
+ }
+
+ return result;
+}
+
+// Toggle Slider control extended
+int GuiToggleSlider(Rectangle bounds, const char *text, int *active)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ int temp = 0;
+ if (active == NULL) active = &temp;
+
+ // Get substrings items from text (items pointers)
+ int itemCount = 0;
+ char **items = NULL;
+
+ if (text != NULL) items = GuiTextSplit(text, ';', &itemCount);
+
+ Rectangle slider = {
+ 0, // Calculated later depending on the active toggle
+ bounds.y + GuiGetStyle(SLIDER, BORDER_WIDTH) + GuiGetStyle(SLIDER, SLIDER_PADDING),
+ (bounds.width - 2*GuiGetStyle(SLIDER, BORDER_WIDTH) - (itemCount + 1)*GuiGetStyle(SLIDER, SLIDER_PADDING))/itemCount,
+ bounds.height - 2*GuiGetStyle(SLIDER, BORDER_WIDTH) - 2*GuiGetStyle(SLIDER, SLIDER_PADDING) };
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else if (GUI_BUTTON_RELEASED)
+ {
+ state = STATE_PRESSED;
+ (*active)++;
+ result = 1;
+ }
+ else state = STATE_FOCUSED;
+ }
+
+ if ((*active) && (state != STATE_FOCUSED)) state = STATE_PRESSED;
+ }
+
+ if (*active >= itemCount) *active = 0;
+ slider.x = bounds.x + GuiGetStyle(SLIDER, BORDER_WIDTH) + (*active + 1)*GuiGetStyle(SLIDER, SLIDER_PADDING) + (*active)*slider.width;
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, GuiGetStyle(SLIDER, BORDER_WIDTH), GetColor(GuiGetStyle(TOGGLE, BORDER + (state*3))),
+ GetColor(GuiGetStyle(TOGGLE, BASE_COLOR_NORMAL)));
+
+ // Draw internal slider
+ if (state == STATE_NORMAL) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_PRESSED)));
+ else if (state == STATE_FOCUSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_FOCUSED)));
+ else if (state == STATE_PRESSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_PRESSED)));
+
+ // Draw text in slider
+ if (text != NULL)
+ {
+ Rectangle textBounds = { 0 };
+ textBounds.width = (float)GuiGetTextWidth(text);
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = slider.x + slider.width/2 - textBounds.width/2;
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+
+ GuiDrawText(items[*active], textBounds, GuiGetStyle(TOGGLE, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(TOGGLE, TEXT + (state*3))), guiAlpha));
+ }
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Check Box control, returns 1 when state changed
+int GuiCheckBox(Rectangle bounds, const char *text, bool *checked)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ bool temp = false;
+ if (checked == NULL) checked = &temp;
+
+ Rectangle textBounds = { 0 };
+
+ if (text != NULL)
+ {
+ textBounds.width = (float)GuiGetTextWidth(text) + 2;
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x + bounds.width + GuiGetStyle(CHECKBOX, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+ if (GuiGetStyle(CHECKBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(CHECKBOX, TEXT_PADDING);
+ }
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ Rectangle totalBounds = {
+ (GuiGetStyle(CHECKBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT)? textBounds.x : bounds.x,
+ bounds.y,
+ bounds.width + textBounds.width + GuiGetStyle(CHECKBOX, TEXT_PADDING),
+ bounds.height,
+ };
+
+ // Check checkbox state
+ if (CheckCollisionPointRec(mousePoint, totalBounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else state = STATE_FOCUSED;
+
+ if (GUI_BUTTON_RELEASED)
+ {
+ *checked = !(*checked);
+ result = 1;
+ }
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, GuiGetStyle(CHECKBOX, BORDER_WIDTH), GetColor(GuiGetStyle(CHECKBOX, BORDER + (state*3))), BLANK);
+
+ if (*checked)
+ {
+ Rectangle check = { bounds.x + GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING),
+ bounds.y + GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING),
+ bounds.width - 2*(GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING)),
+ bounds.height - 2*(GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING)) };
+ GuiDrawRectangle(check, 0, BLANK, GetColor(GuiGetStyle(CHECKBOX, TEXT + state*3)));
+ }
+
+ GuiDrawText(text, textBounds, (GuiGetStyle(CHECKBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Combo Box control
+int GuiComboBox(Rectangle bounds, const char *text, int *active)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ int temp = 0;
+ if (active == NULL) active = &temp;
+
+ bounds.width -= (GuiGetStyle(COMBOBOX, COMBO_BUTTON_WIDTH) + GuiGetStyle(COMBOBOX, COMBO_BUTTON_SPACING));
+
+ Rectangle selector = { (float)bounds.x + bounds.width + GuiGetStyle(COMBOBOX, COMBO_BUTTON_SPACING),
+ (float)bounds.y, (float)GuiGetStyle(COMBOBOX, COMBO_BUTTON_WIDTH), (float)bounds.height };
+
+ // Get substrings items from text (items pointers, lengths and count)
+ int itemCount = 0;
+ char **items = GuiTextSplit(text, ';', &itemCount);
+
+ if (*active < 0) *active = 0;
+ else if (*active > (itemCount - 1)) *active = itemCount - 1;
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && (itemCount > 1) && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (CheckCollisionPointRec(mousePoint, bounds) ||
+ CheckCollisionPointRec(mousePoint, selector))
+ {
+ if (GUI_BUTTON_PRESSED)
+ {
+ *active += 1;
+ if (*active >= itemCount) *active = 0; // Cyclic combobox
+ }
+
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else state = STATE_FOCUSED;
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ // Draw combo box main
+ GuiDrawRectangle(bounds, GuiGetStyle(COMBOBOX, BORDER_WIDTH), GetColor(GuiGetStyle(COMBOBOX, BORDER + (state*3))), GetColor(GuiGetStyle(COMBOBOX, BASE + (state*3))));
+ GuiDrawText(items[*active], GetTextBounds(COMBOBOX, bounds), GuiGetStyle(COMBOBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(COMBOBOX, TEXT + (state*3))));
+
+ // Draw selector using a custom button
+ // NOTE: BORDER_WIDTH and TEXT_ALIGNMENT forced values
+ int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH);
+ int tempTextAlign = GuiGetStyle(BUTTON, TEXT_ALIGNMENT);
+ GuiSetStyle(BUTTON, BORDER_WIDTH, 1);
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+
+ GuiButton(selector, TextFormat("%i/%i", *active + 1, itemCount));
+
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlign);
+ GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth);
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Dropdown Box control
+// NOTE: Returns mouse click
+int GuiDropdownBox(Rectangle bounds, const char *text, int *active, bool editMode)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ int temp = 0;
+ if (active == NULL) active = &temp;
+
+ int itemSelected = *active;
+ int itemFocused = -1;
+
+ int direction = 0; // Dropdown box open direction: down (default)
+ if (GuiGetStyle(DROPDOWNBOX, DROPDOWN_ROLL_UP) == 1) direction = 1; // Up
+
+ // Get substrings items from text (items pointers, lengths and count)
+ int itemCount = 0;
+ char **items = GuiTextSplit(text, ';', &itemCount);
+
+ Rectangle boundsOpen = bounds;
+ boundsOpen.height = (itemCount + 1)*(bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING));
+ if (direction == 1) boundsOpen.y -= itemCount*(bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)) + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING);
+
+ Rectangle itemBounds = bounds;
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && (editMode || !guiLocked) && (itemCount > 1) && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (editMode)
+ {
+ state = STATE_PRESSED;
+
+ // Check if mouse has been pressed or released outside limits
+ if (!CheckCollisionPointRec(mousePoint, boundsOpen))
+ {
+ if (GUI_BUTTON_PRESSED || GUI_BUTTON_RELEASED) result = 1;
+ }
+
+ // Check if already selected item has been pressed again
+ if (CheckCollisionPointRec(mousePoint, bounds) && GUI_BUTTON_PRESSED) result = 1;
+
+ // Check focused and selected item
+ for (int i = 0; i < itemCount; i++)
+ {
+ // Update item rectangle y position for next item
+ if (direction == 0) itemBounds.y += (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING));
+ else itemBounds.y -= (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING));
+
+ if (CheckCollisionPointRec(mousePoint, itemBounds))
+ {
+ itemFocused = i;
+ if (GUI_BUTTON_RELEASED)
+ {
+ itemSelected = i;
+ result = 1; // Item selected
+ }
+ break;
+ }
+ }
+
+ itemBounds = bounds;
+ }
+ else
+ {
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_PRESSED)
+ {
+ result = 1;
+ state = STATE_PRESSED;
+ }
+ else state = STATE_FOCUSED;
+ }
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (editMode) GuiPanel(boundsOpen, NULL);
+
+ GuiDrawRectangle(bounds, GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH), GetColor(GuiGetStyle(DROPDOWNBOX, BORDER + state*3)), GetColor(GuiGetStyle(DROPDOWNBOX, BASE + state*3)));
+ GuiDrawText(items[itemSelected], GetTextBounds(DROPDOWNBOX, bounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + state*3)));
+
+ if (editMode)
+ {
+ // Draw visible items
+ for (int i = 0; i < itemCount; i++)
+ {
+ // Update item rectangle y position for next item
+ if (direction == 0) itemBounds.y += (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING));
+ else itemBounds.y -= (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING));
+
+ if (i == itemSelected)
+ {
+ GuiDrawRectangle(itemBounds, GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH), GetColor(GuiGetStyle(DROPDOWNBOX, BORDER_COLOR_PRESSED)), GetColor(GuiGetStyle(DROPDOWNBOX, BASE_COLOR_PRESSED)));
+ GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_PRESSED)));
+ }
+ else if (i == itemFocused)
+ {
+ GuiDrawRectangle(itemBounds, GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH), GetColor(GuiGetStyle(DROPDOWNBOX, BORDER_COLOR_FOCUSED)), GetColor(GuiGetStyle(DROPDOWNBOX, BASE_COLOR_FOCUSED)));
+ GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_FOCUSED)));
+ }
+ else GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_NORMAL)));
+ }
+ }
+
+ if (!GuiGetStyle(DROPDOWNBOX, DROPDOWN_ARROW_HIDDEN))
+ {
+ // Draw arrows (using icon if available)
+#if defined(RAYGUI_NO_ICONS)
+ GuiDrawText("v", RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - GuiGetStyle(DROPDOWNBOX, ARROW_PADDING), bounds.y + bounds.height/2 - 2, 10, 10 },
+ TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3))));
+#else
+ GuiDrawText(direction? GuiIconText(ICON_ARROW_UP_FILL, NULL) : GuiIconText(ICON_ARROW_DOWN_FILL, NULL),
+ RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - GuiGetStyle(DROPDOWNBOX, ARROW_PADDING), bounds.y + bounds.height/2 - 6, 10, 10 },
+ TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3)))); // ICON_ARROW_DOWN_FILL
+#endif
+ }
+ //--------------------------------------------------------------------
+
+ *active = itemSelected;
+
+ // TODO: Use result to return more internal states: mouse-press out-of-bounds, mouse-press over selected-item...
+ return result; // Mouse click: result = 1
+}
+
+// Text Box control
+// NOTE: Returns true on ENTER pressed (useful for data validation)
+int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode)
+{
+ #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN)
+ #define RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN 20 // Frames to wait for autocursor movement
+ #endif
+ #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY)
+ #define RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY 1 // Frames delay for autocursor movement
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+
+ bool multiline = false;
+ int wrapMode = GuiGetStyle(DEFAULT, TEXT_WRAP_MODE);
+
+ Rectangle textBounds = GetTextBounds(TEXTBOX, bounds);
+ int textLength = (text != NULL)? (int)strlen(text) : 0; // Get current text length
+ int thisCursorIndex = textBoxCursorIndex;
+ if (thisCursorIndex > textLength) thisCursorIndex = textLength;
+ int textWidth = GuiGetTextWidth(text) - GuiGetTextWidth(text + thisCursorIndex);
+ int textIndexOffset = 0; // Text index offset to start drawing in the box
+
+ // Cursor rectangle
+ // NOTE: Position X value should be updated
+ Rectangle cursor = {
+ textBounds.x + textWidth + GuiGetStyle(DEFAULT, TEXT_SPACING),
+ textBounds.y + textBounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE),
+ 2,
+ (float)GuiGetStyle(DEFAULT, TEXT_SIZE)*2
+ };
+
+ if (cursor.height >= bounds.height) cursor.height = bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH)*2;
+ if (cursor.y < (bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH))) cursor.y = bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH);
+
+ // Mouse cursor rectangle
+ // NOTE: Initialized outside of screen
+ Rectangle mouseCursor = cursor;
+ mouseCursor.x = -1;
+ mouseCursor.width = 1;
+
+ // Blink-cursor frame counter
+ //if (!autoCursorMode) blinkCursorFrameCounter++;
+ //else blinkCursorFrameCounter = 0;
+
+ // Update control
+ //--------------------------------------------------------------------
+ // WARNING: Text editing is only supported under certain conditions:
+ if ((state != STATE_DISABLED) && // Control not disabled
+ !GuiGetStyle(TEXTBOX, TEXT_READONLY) && // TextBox not on read-only mode
+ !guiLocked && // Gui not locked
+ !guiControlExclusiveMode && // No gui slider on dragging
+ (wrapMode == TEXT_WRAP_NONE)) // No wrap mode
+ {
+ Vector2 mousePosition = GUI_POINTER_POSITION;
+
+ if (editMode)
+ {
+ // GLOBAL: Auto-cursor movement logic
+ // NOTE: Keystrokes are handled repeatedly when button is held down for some time
+ if (GUI_KEY_DOWN(KEY_LEFT) || GUI_KEY_DOWN(KEY_RIGHT) || GUI_KEY_DOWN(KEY_UP) || GUI_KEY_DOWN(KEY_DOWN) || GUI_KEY_DOWN(KEY_BACKSPACE) || GUI_KEY_DOWN(KEY_DELETE)) autoCursorCounter++;
+ else autoCursorCounter = 0;
+
+ bool autoCursorShouldTrigger = (autoCursorCounter > RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN) && ((autoCursorCounter % RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) == 0);
+
+ state = STATE_PRESSED;
+
+ if (textBoxCursorIndex > textLength) textBoxCursorIndex = textLength;
+
+ // If text does not fit in the textbox and current cursor position is out of bounds,
+ // adding an index offset to text for drawing only what requires depending on cursor
+ while (textWidth >= textBounds.width)
+ {
+ int nextCodepointSize = 0;
+ GetCodepointNext(text + textIndexOffset, &nextCodepointSize);
+
+ textIndexOffset += nextCodepointSize;
+
+ textWidth = GuiGetTextWidth(text + textIndexOffset) - GuiGetTextWidth(text + textBoxCursorIndex);
+ }
+
+ int codepoint = GUI_INPUT_KEY; // Get Unicode codepoint
+ if (multiline && GUI_KEY_PRESSED(KEY_ENTER)) codepoint = (int)'\n';
+
+ // Encode codepoint as UTF-8
+ int codepointSize = 0;
+ const char *charEncoded = CodepointToUTF8(codepoint, &codepointSize);
+
+ // Handle text paste action
+ if (GUI_KEY_PRESSED(KEY_V) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL)))
+ {
+ const char *pasteText = GetClipboardText();
+ if (pasteText != NULL)
+ {
+ int pasteLength = 0;
+ int pasteCodepoint;
+ int pasteCodepointSize;
+
+ // Count how many codepoints to copy, stopping at the first unwanted control character
+ while (true)
+ {
+ pasteCodepoint = GetCodepointNext(pasteText + pasteLength, &pasteCodepointSize);
+ if (textLength + pasteLength + pasteCodepointSize >= textSize) break;
+ if (!(multiline && (pasteCodepoint == (int)'\n')) && !(pasteCodepoint >= 32)) break;
+ pasteLength += pasteCodepointSize;
+ }
+
+ if (pasteLength > 0)
+ {
+ // Move forward data from cursor position
+ for (int i = textLength + pasteLength; i > textBoxCursorIndex; i--) text[i] = text[i - pasteLength];
+
+ // Paste data in at cursor
+ for (int i = 0; i < pasteLength; i++) text[textBoxCursorIndex + i] = pasteText[i];
+
+ textBoxCursorIndex += pasteLength;
+ textLength += pasteLength;
+ text[textLength] = '\0';
+ }
+ }
+ }
+ else if (((multiline && (codepoint == (int)'\n')) || (codepoint >= 32)) && ((textLength + codepointSize) < textSize))
+ {
+ // Adding codepoint to text, at current cursor position
+
+ // Move forward data from cursor position
+ for (int i = (textLength + codepointSize); i > textBoxCursorIndex; i--) text[i] = text[i - codepointSize];
+
+ // Add new codepoint in current cursor position
+ for (int i = 0; i < codepointSize; i++) text[textBoxCursorIndex + i] = charEncoded[i];
+
+ textBoxCursorIndex += codepointSize;
+ textLength += codepointSize;
+
+ // Make sure text last character is EOL
+ text[textLength] = '\0';
+ }
+
+ // Move cursor to start
+ if ((textLength > 0) && GUI_KEY_PRESSED(KEY_HOME)) textBoxCursorIndex = 0;
+
+ // Move cursor to end
+ if ((textLength > textBoxCursorIndex) && GUI_KEY_PRESSED(KEY_END)) textBoxCursorIndex = textLength;
+
+ // Delete related codepoints from text, after current cursor position
+ if ((textLength > textBoxCursorIndex) && GUI_KEY_PRESSED(KEY_DELETE) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL)))
+ {
+ int offset = textBoxCursorIndex;
+ int accCodepointSize = 0;
+ int nextCodepointSize;
+ int nextCodepoint;
+
+ // Check characters of the same type to delete (either ASCII punctuation or anything non-whitespace)
+ // Not using isalnum() since it only works on ASCII characters
+ nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
+ bool puctuation = ispunct(nextCodepoint & 0xff);
+ while (offset < textLength)
+ {
+ if ((puctuation && !ispunct(nextCodepoint & 0xff)) || (!puctuation && (isspace(nextCodepoint & 0xff) || ispunct(nextCodepoint & 0xff))))
+ break;
+ offset += nextCodepointSize;
+ accCodepointSize += nextCodepointSize;
+ nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
+ }
+
+ // Check whitespace to delete (ASCII only)
+ while (offset < textLength)
+ {
+ if (!isspace(nextCodepoint & 0xff)) break;
+
+ offset += nextCodepointSize;
+ accCodepointSize += nextCodepointSize;
+ nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
+ }
+
+ // Move text after cursor forward (including final null terminator)
+ for (int i = offset; i <= textLength; i++) text[i - accCodepointSize] = text[i];
+
+ textLength -= accCodepointSize;
+ }
+ else if ((textLength > textBoxCursorIndex) && (GUI_KEY_PRESSED(KEY_DELETE) || (GUI_KEY_DOWN(KEY_DELETE) && autoCursorShouldTrigger)))
+ {
+ // Delete single codepoint from text, after current cursor position
+
+ int nextCodepointSize = 0;
+ GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize);
+
+ // Move text after cursor forward (including final null terminator)
+ for (int i = textBoxCursorIndex + nextCodepointSize; i <= textLength; i++) text[i - nextCodepointSize] = text[i];
+
+ textLength -= nextCodepointSize;
+ }
+
+ // Delete related codepoints from text, before current cursor position
+ if ((textBoxCursorIndex > 0) && GUI_KEY_PRESSED(KEY_BACKSPACE) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL)))
+ {
+ int offset = textBoxCursorIndex;
+ int accCodepointSize = 0;
+ int prevCodepointSize = 0;
+ int prevCodepoint = 0;
+
+ // Check whitespace to delete (ASCII only)
+ while (offset > 0)
+ {
+ prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
+ if (!isspace(prevCodepoint & 0xff)) break;
+
+ offset -= prevCodepointSize;
+ accCodepointSize += prevCodepointSize;
+ }
+
+ // Check characters of the same type to delete (either ASCII punctuation or anything non-whitespace)
+ // Not using isalnum() since it only works on ASCII characters
+ bool puctuation = ispunct(prevCodepoint & 0xff);
+ while (offset > 0)
+ {
+ prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
+ if ((puctuation && !ispunct(prevCodepoint & 0xff)) || (!puctuation && (isspace(prevCodepoint & 0xff) || ispunct(prevCodepoint & 0xff)))) break;
+
+ offset -= prevCodepointSize;
+ accCodepointSize += prevCodepointSize;
+ }
+
+ // Move text after cursor forward (including final null terminator)
+ for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - accCodepointSize] = text[i];
+
+ textLength -= accCodepointSize;
+ textBoxCursorIndex -= accCodepointSize;
+ }
+ else if ((textBoxCursorIndex > 0) && (GUI_KEY_PRESSED(KEY_BACKSPACE) || (GUI_KEY_DOWN(KEY_BACKSPACE) && autoCursorShouldTrigger)))
+ {
+ // Delete single codepoint from text, before current cursor position
+
+ int prevCodepointSize = 0;
+
+ GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize);
+
+ // Move text after cursor forward (including final null terminator)
+ for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - prevCodepointSize] = text[i];
+
+ textLength -= prevCodepointSize;
+ textBoxCursorIndex -= prevCodepointSize;
+ }
+
+ // Move cursor position with keys
+ if ((textBoxCursorIndex > 0) && GUI_KEY_PRESSED(KEY_LEFT) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL)))
+ {
+ int offset = textBoxCursorIndex;
+ //int accCodepointSize = 0;
+ int prevCodepointSize = 0;
+ int prevCodepoint = 0;
+
+ // Check whitespace to skip (ASCII only)
+ while (offset > 0)
+ {
+ prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
+ if (!isspace(prevCodepoint & 0xff)) break;
+
+ offset -= prevCodepointSize;
+ //accCodepointSize += prevCodepointSize;
+ }
+
+ // Check characters of the same type to skip (either ASCII punctuation or anything non-whitespace)
+ // Not using isalnum() since it only works on ASCII characters
+ bool puctuation = ispunct(prevCodepoint & 0xff);
+ while (offset > 0)
+ {
+ prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize);
+ if ((puctuation && !ispunct(prevCodepoint & 0xff)) || (!puctuation && (isspace(prevCodepoint & 0xff) || ispunct(prevCodepoint & 0xff)))) break;
+
+ offset -= prevCodepointSize;
+ //accCodepointSize += prevCodepointSize;
+ }
+
+ textBoxCursorIndex = offset;
+ }
+ else if ((textBoxCursorIndex > 0) && (GUI_KEY_PRESSED(KEY_LEFT) || (GUI_KEY_DOWN(KEY_LEFT) && autoCursorShouldTrigger)))
+ {
+ int prevCodepointSize = 0;
+ GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize);
+
+ textBoxCursorIndex -= prevCodepointSize;
+ }
+ else if ((textLength > textBoxCursorIndex) && GUI_KEY_PRESSED(KEY_RIGHT) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL)))
+ {
+ int offset = textBoxCursorIndex;
+ //int accCodepointSize = 0;
+ int nextCodepointSize;
+ int nextCodepoint;
+
+ // Check characters of the same type to skip (either ASCII punctuation or anything non-whitespace)
+ // Not using isalnum() since it only works on ASCII characters
+ nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
+ bool puctuation = ispunct(nextCodepoint & 0xff);
+ while (offset < textLength)
+ {
+ if ((puctuation && !ispunct(nextCodepoint & 0xff)) || (!puctuation && (isspace(nextCodepoint & 0xff) || ispunct(nextCodepoint & 0xff)))) break;
+
+ offset += nextCodepointSize;
+ //accCodepointSize += nextCodepointSize;
+ nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
+ }
+
+ // Check whitespace to skip (ASCII only)
+ while (offset < textLength)
+ {
+ if (!isspace(nextCodepoint & 0xff)) break;
+
+ offset += nextCodepointSize;
+ //accCodepointSize += nextCodepointSize;
+ nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize);
+ }
+
+ textBoxCursorIndex = offset;
+ }
+ else if ((textLength > textBoxCursorIndex) && (GUI_KEY_PRESSED(KEY_RIGHT) || (GUI_KEY_DOWN(KEY_RIGHT) && autoCursorShouldTrigger)))
+ {
+ int nextCodepointSize = 0;
+ GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize);
+
+ textBoxCursorIndex += nextCodepointSize;
+ }
+
+ // Move cursor position with mouse
+ if (CheckCollisionPointRec(mousePosition, textBounds)) // Mouse hover text
+ {
+ float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/(float)guiFont.baseSize;
+ int codepointIndex = 0;
+ float glyphWidth = 0.0f;
+ float widthToMouseX = 0;
+ int mouseCursorIndex = 0;
+
+ for (int i = textIndexOffset; i < textLength; i += codepointSize)
+ {
+ codepoint = GetCodepointNext(&text[i], &codepointSize);
+ codepointIndex = GetGlyphIndex(guiFont, codepoint);
+
+ if (guiFont.glyphs[codepointIndex].advanceX == 0) glyphWidth = ((float)guiFont.recs[codepointIndex].width*scaleFactor);
+ else glyphWidth = ((float)guiFont.glyphs[codepointIndex].advanceX*scaleFactor);
+
+ if (mousePosition.x <= (textBounds.x + (widthToMouseX + glyphWidth/2)))
+ {
+ mouseCursor.x = textBounds.x + widthToMouseX;
+ mouseCursorIndex = i;
+ break;
+ }
+
+ widthToMouseX += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
+ }
+
+ // Check if mouse cursor is at the last position
+ int textEndWidth = GuiGetTextWidth(text + textIndexOffset);
+ if (GUI_POINTER_POSITION.x >= (textBounds.x + textEndWidth - glyphWidth/2))
+ {
+ mouseCursor.x = textBounds.x + textEndWidth;
+ mouseCursorIndex = textLength;
+ }
+
+ // Place cursor at required index on mouse click
+ if ((mouseCursor.x >= 0) && GUI_BUTTON_PRESSED)
+ {
+ cursor.x = mouseCursor.x;
+ textBoxCursorIndex = mouseCursorIndex;
+ }
+ }
+ else mouseCursor.x = -1;
+
+ // Recalculate cursor position.y depending on textBoxCursorIndex
+ cursor.x = bounds.x + GuiGetStyle(TEXTBOX, TEXT_PADDING) + GuiGetTextWidth(text + textIndexOffset) - GuiGetTextWidth(text + textBoxCursorIndex) + GuiGetStyle(DEFAULT, TEXT_SPACING);
+ //if (multiline) cursor.y = GetTextLines()
+
+ // Finish text editing on ENTER or mouse click outside bounds
+ if ((!multiline && GUI_KEY_PRESSED(KEY_ENTER)) ||
+ (!CheckCollisionPointRec(mousePosition, bounds) && GUI_BUTTON_PRESSED))
+ {
+ textBoxCursorIndex = 0; // GLOBAL: Reset the shared cursor index
+ autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes
+ result = 1;
+ }
+ }
+ else
+ {
+ if (CheckCollisionPointRec(mousePosition, bounds))
+ {
+ state = STATE_FOCUSED;
+
+ if (GUI_BUTTON_PRESSED)
+ {
+ textBoxCursorIndex = textLength; // GLOBAL: Place cursor index to the end of current text
+ autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes
+ result = 1;
+ }
+ }
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (state == STATE_PRESSED)
+ {
+ GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED)));
+ }
+ else if (state == STATE_DISABLED)
+ {
+ GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_DISABLED)));
+ }
+ else GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), BLANK);
+
+ // Draw text considering index offset if required
+ // NOTE: Text index offset depends on cursor position
+ GuiDrawText(text + textIndexOffset, textBounds, GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TEXTBOX, TEXT + (state*3))));
+
+ // Draw cursor
+ if (editMode && !GuiGetStyle(TEXTBOX, TEXT_READONLY))
+ {
+ //if (autoCursorMode || ((blinkCursorFrameCounter/40)%2 == 0))
+ GuiDrawRectangle(cursor, 0, BLANK, GetColor(GuiGetStyle(TEXTBOX, BORDER_COLOR_PRESSED)));
+
+ // Draw mouse position cursor (if required)
+ if (mouseCursor.x >= 0) GuiDrawRectangle(mouseCursor, 0, BLANK, GetColor(GuiGetStyle(TEXTBOX, BORDER_COLOR_PRESSED)));
+ }
+ else if (state == STATE_FOCUSED) GuiTooltip(bounds);
+ //--------------------------------------------------------------------
+
+ return result; // Mouse button pressed: result = 1
+}
+
+/*
+// Text Box control with multiple lines and word-wrap
+// NOTE: This text-box is readonly, no editing supported by default
+bool GuiTextBoxMulti(Rectangle bounds, char *text, int textSize, bool editMode)
+{
+ bool pressed = false;
+
+ GuiSetStyle(TEXTBOX, TEXT_READONLY, 1);
+ GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, TEXT_WRAP_WORD); // WARNING: If wrap mode enabled, text editing is not supported
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_TOP);
+
+ // TODO: Implement methods to calculate cursor position properly
+ pressed = GuiTextBox(bounds, text, textSize, editMode);
+
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_MIDDLE);
+ GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, TEXT_WRAP_NONE);
+ GuiSetStyle(TEXTBOX, TEXT_READONLY, 0);
+
+ return pressed;
+}
+*/
+
+// Spinner control, returns selected value
+int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
+{
+ int result = 1;
+ GuiState state = guiState;
+
+ int tempValue = *value;
+
+ Rectangle valueBoxBounds = {
+ bounds.x + GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH) + GuiGetStyle(VALUEBOX, SPINNER_BUTTON_SPACING),
+ bounds.y,
+ bounds.width - 2*(GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH) + GuiGetStyle(VALUEBOX, SPINNER_BUTTON_SPACING)), bounds.height };
+ Rectangle leftButtonBound = { (float)bounds.x, (float)bounds.y, (float)GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH), (float)bounds.height };
+ Rectangle rightButtonBound = { (float)bounds.x + bounds.width - GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH), (float)bounds.y,
+ (float)GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH), (float)bounds.height };
+
+ Rectangle textBounds = { 0 };
+ if (text != NULL)
+ {
+ textBounds.width = (float)GuiGetTextWidth(text) + 2;
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x + bounds.width + GuiGetStyle(VALUEBOX, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+ if (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(VALUEBOX, TEXT_PADDING);
+ }
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ // Check spinner state
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else state = STATE_FOCUSED;
+ }
+ }
+
+#if defined(RAYGUI_NO_ICONS)
+ if (GuiButton(leftButtonBound, "<")) tempValue--;
+ if (GuiButton(rightButtonBound, ">")) tempValue++;
+#else
+ if (GuiButton(leftButtonBound, GuiIconText(ICON_ARROW_LEFT_FILL, NULL))) tempValue--;
+ if (GuiButton(rightButtonBound, GuiIconText(ICON_ARROW_RIGHT_FILL, NULL))) tempValue++;
+#endif
+
+ if (!editMode)
+ {
+ if (tempValue < minValue) tempValue = minValue;
+ if (tempValue > maxValue) tempValue = maxValue;
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ result = GuiValueBox(valueBoxBounds, NULL, &tempValue, minValue, maxValue, editMode);
+
+ // Draw value selector custom buttons
+ // NOTE: BORDER_WIDTH and TEXT_ALIGNMENT forced values
+ int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH);
+ int tempTextAlign = GuiGetStyle(BUTTON, TEXT_ALIGNMENT);
+ GuiSetStyle(BUTTON, BORDER_WIDTH, GuiGetStyle(VALUEBOX, BORDER_WIDTH));
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlign);
+ GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth);
+
+ // Draw text label if provided
+ GuiDrawText(text, textBounds, (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ //--------------------------------------------------------------------
+
+ *value = tempValue;
+ return result;
+}
+
+// Value Box control, updates input text with numbers
+int GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
+{
+ #if !defined(RAYGUI_VALUEBOX_MAX_CHARS)
+ #define RAYGUI_VALUEBOX_MAX_CHARS 32
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+
+ char textValue[RAYGUI_VALUEBOX_MAX_CHARS + 1] = { 0 };
+ snprintf(textValue, RAYGUI_VALUEBOX_MAX_CHARS + 1, "%i", *value);
+
+ Rectangle textBounds = { 0 };
+ if (text != NULL)
+ {
+ textBounds.width = (float)GuiGetTextWidth(text) + 2;
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x + bounds.width + GuiGetStyle(VALUEBOX, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+ if (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(VALUEBOX, TEXT_PADDING);
+ }
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+ bool valueHasChanged = false;
+
+ if (editMode)
+ {
+ state = STATE_PRESSED;
+
+ int keyCount = (int)strlen(textValue);
+
+ // Add or remove minus symbol
+ if (GUI_KEY_PRESSED(KEY_MINUS))
+ {
+ if (textValue[0] == '-')
+ {
+ for (int i = 0 ; i < keyCount; i++) textValue[i] = textValue[i + 1];
+
+ keyCount--;
+ valueHasChanged = true;
+ }
+ else if (keyCount < RAYGUI_VALUEBOX_MAX_CHARS)
+ {
+ if (keyCount == 0)
+ {
+ textValue[0] = '0';
+ textValue[1] = '\0';
+ keyCount++;
+ }
+
+ for (int i = keyCount ; i > -1; i--) textValue[i + 1] = textValue[i];
+
+ textValue[0] = '-';
+ keyCount++;
+ valueHasChanged = true;
+ }
+ }
+
+ // Add new digit to text value
+ if ((keyCount >= 0) && (keyCount < RAYGUI_VALUEBOX_MAX_CHARS) && (GuiGetTextWidth(textValue) < bounds.width))
+ {
+ int key = GUI_INPUT_KEY;
+
+ // Only allow keys in range [48..57]
+ if ((key >= 48) && (key <= 57))
+ {
+ textValue[keyCount] = (char)key;
+ keyCount++;
+ valueHasChanged = true;
+ }
+ }
+
+ // Delete text
+ if ((keyCount > 0) && GUI_KEY_PRESSED(KEY_BACKSPACE))
+ {
+ keyCount--;
+ textValue[keyCount] = '\0';
+ valueHasChanged = true;
+ }
+
+ if (valueHasChanged) *value = TextToInteger(textValue);
+
+ // NOTE: Values are not clamped until user input finishes
+ //if (*value > maxValue) *value = maxValue;
+ //else if (*value < minValue) *value = minValue;
+
+ if ((GUI_KEY_PRESSED(KEY_ENTER) || GUI_KEY_PRESSED(KEY_KP_ENTER)) || (!CheckCollisionPointRec(mousePoint, bounds) && GUI_BUTTON_PRESSED))
+ {
+ if (*value > maxValue) *value = maxValue;
+ else if (*value < minValue) *value = minValue;
+
+ result = 1;
+ }
+ }
+ else
+ {
+ if (*value > maxValue) *value = maxValue;
+ else if (*value < minValue) *value = minValue;
+
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ state = STATE_FOCUSED;
+ if (GUI_BUTTON_PRESSED) result = 1;
+ }
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ Color baseColor = BLANK;
+ if (state == STATE_PRESSED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_PRESSED));
+ else if (state == STATE_DISABLED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_DISABLED));
+
+ GuiDrawRectangle(bounds, GuiGetStyle(VALUEBOX, BORDER_WIDTH), GetColor(GuiGetStyle(VALUEBOX, BORDER + (state*3))), baseColor);
+ GuiDrawText(textValue, GetTextBounds(VALUEBOX, bounds), TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(VALUEBOX, TEXT + (state*3))));
+
+ // Draw cursor rectangle
+ if (editMode)
+ {
+ // NOTE: ValueBox internal text is always centered
+ Rectangle cursor = { bounds.x + GuiGetTextWidth(textValue)/2 + bounds.width/2 + 1,
+ bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH) + 2,
+ 2, bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH)*2 - 4 };
+ if (cursor.height > bounds.height) cursor.height = bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH)*2;
+ GuiDrawRectangle(cursor, 0, BLANK, GetColor(GuiGetStyle(VALUEBOX, BORDER_COLOR_PRESSED)));
+ }
+
+ // Draw text label if provided
+ GuiDrawText(text, textBounds, (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Floating point Value Box control, updates input val_str with numbers
+int GuiValueBoxFloat(Rectangle bounds, const char *text, char *textValue, float *value, bool editMode)
+{
+ #if !defined(RAYGUI_VALUEBOX_MAX_CHARS)
+ #define RAYGUI_VALUEBOX_MAX_CHARS 32
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+
+ //char textValue[RAYGUI_VALUEBOX_MAX_CHARS + 1] = "\0";
+ //snprintf(textValue, sizeof(textValue), "%2.2f", *value);
+
+ Rectangle textBounds = { 0 };
+ if (text != NULL)
+ {
+ textBounds.width = (float)GuiGetTextWidth(text) + 2;
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x + bounds.width + GuiGetStyle(VALUEBOX, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+ if (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(VALUEBOX, TEXT_PADDING);
+ }
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ bool valueHasChanged = false;
+
+ if (editMode)
+ {
+ state = STATE_PRESSED;
+
+ int keyCount = (int)strlen(textValue);
+
+ // Add or remove minus symbol
+ if (GUI_KEY_PRESSED(KEY_MINUS))
+ {
+ if (textValue[0] == '-')
+ {
+ for (int i = 0; i < keyCount; i++) textValue[i] = textValue[i + 1];
+
+ keyCount--;
+ valueHasChanged = true;
+ }
+ else if (keyCount < (RAYGUI_VALUEBOX_MAX_CHARS - 1))
+ {
+ if (keyCount == 0)
+ {
+ textValue[0] = '0';
+ textValue[1] = '\0';
+ keyCount++;
+ }
+
+ for (int i = keyCount; i > -1; i--) textValue[i + 1] = textValue[i];
+
+ textValue[0] = '-';
+ keyCount++;
+ valueHasChanged = true;
+ }
+ }
+
+ // Only allow keys in range [48..57]
+ if (keyCount < RAYGUI_VALUEBOX_MAX_CHARS)
+ {
+ if (GuiGetTextWidth(textValue) < bounds.width)
+ {
+ int key = GUI_INPUT_KEY;
+ if (((key >= 48) && (key <= 57)) ||
+ (key == '.') ||
+ ((keyCount == 0) && (key == '+')) || // NOTE: Sign can only be in first position
+ ((keyCount == 0) && (key == '-')))
+ {
+ textValue[keyCount] = (char)key;
+ keyCount++;
+
+ valueHasChanged = true;
+ }
+ }
+ }
+
+ // Pressed backspace
+ if (GUI_KEY_PRESSED(KEY_BACKSPACE))
+ {
+ if (keyCount > 0)
+ {
+ keyCount--;
+ textValue[keyCount] = '\0';
+ valueHasChanged = true;
+ }
+ }
+
+ if (valueHasChanged) *value = TextToFloat(textValue);
+
+ if ((GUI_KEY_PRESSED(KEY_ENTER) || GUI_KEY_PRESSED(KEY_KP_ENTER)) || (!CheckCollisionPointRec(mousePoint, bounds) && GUI_BUTTON_PRESSED)) result = 1;
+ }
+ else
+ {
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ state = STATE_FOCUSED;
+ if (GUI_BUTTON_PRESSED) result = 1;
+ }
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ Color baseColor = BLANK;
+ if (state == STATE_PRESSED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_PRESSED));
+ else if (state == STATE_DISABLED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_DISABLED));
+
+ GuiDrawRectangle(bounds, GuiGetStyle(VALUEBOX, BORDER_WIDTH), GetColor(GuiGetStyle(VALUEBOX, BORDER + (state*3))), baseColor);
+ GuiDrawText(textValue, GetTextBounds(VALUEBOX, bounds), TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(VALUEBOX, TEXT + (state*3))));
+
+ // Draw cursor
+ if (editMode)
+ {
+ // NOTE: ValueBox internal text is always centered
+ Rectangle cursor = {bounds.x + GuiGetTextWidth(textValue)/2 + bounds.width/2 + 1,
+ bounds.y + 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), 4,
+ bounds.height - 4*GuiGetStyle(VALUEBOX, BORDER_WIDTH)};
+ GuiDrawRectangle(cursor, 0, BLANK, GetColor(GuiGetStyle(VALUEBOX, BORDER_COLOR_PRESSED)));
+ }
+
+ // Draw text label if provided
+ GuiDrawText(text, textBounds,
+ (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT,
+ GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Slider control with pro parameters
+// NOTE: Other GuiSlider*() controls use this one
+int GuiSlider(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ float temp = (maxValue - minValue)/2.0f;
+ if (value == NULL) value = &temp;
+ float oldValue = *value;
+
+ int sliderWidth = GuiGetStyle(SLIDER, SLIDER_WIDTH);
+
+ Rectangle slider = { bounds.x, bounds.y + GuiGetStyle(SLIDER, BORDER_WIDTH) + GuiGetStyle(SLIDER, SLIDER_PADDING),
+ 0, bounds.height - 2*GuiGetStyle(SLIDER, BORDER_WIDTH) - 2*GuiGetStyle(SLIDER, SLIDER_PADDING) };
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec))
+ {
+ state = STATE_PRESSED;
+ // Get equivalent value and slider position from mousePosition.x
+ *value = (maxValue - minValue)*((mousePoint.x - bounds.x - sliderWidth/2)/(bounds.width - sliderWidth)) + minValue;
+ }
+ }
+ else
+ {
+ guiControlExclusiveMode = false;
+ guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 };
+ }
+ }
+ else if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ state = STATE_PRESSED;
+ guiControlExclusiveMode = true;
+ guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts
+
+ if (!CheckCollisionPointRec(mousePoint, slider))
+ {
+ // Get equivalent value and slider position from mousePosition.x
+ *value = (maxValue - minValue)*((mousePoint.x - bounds.x - sliderWidth/2)/(bounds.width - sliderWidth)) + minValue;
+ }
+ }
+ else state = STATE_FOCUSED;
+ }
+
+ if (*value > maxValue) *value = maxValue;
+ else if (*value < minValue) *value = minValue;
+ }
+
+ // Control value change check
+ if (oldValue == *value) result = 0;
+ else result = 1;
+
+ // Slider bar limits check
+ float sliderValue = (((*value - minValue)/(maxValue - minValue))*(bounds.width - sliderWidth - 2*GuiGetStyle(SLIDER, BORDER_WIDTH)));
+ if (sliderWidth > 0) // Slider
+ {
+ slider.x += sliderValue;
+ slider.width = (float)sliderWidth;
+ if (slider.x <= (bounds.x + GuiGetStyle(SLIDER, BORDER_WIDTH))) slider.x = bounds.x + GuiGetStyle(SLIDER, BORDER_WIDTH);
+ else if ((slider.x + slider.width) >= (bounds.x + bounds.width)) slider.x = bounds.x + bounds.width - slider.width - GuiGetStyle(SLIDER, BORDER_WIDTH);
+ }
+ else if (sliderWidth == 0) // SliderBar
+ {
+ slider.x += GuiGetStyle(SLIDER, BORDER_WIDTH);
+ slider.width = sliderValue;
+ if (slider.width > bounds.width) slider.width = bounds.width - 2*GuiGetStyle(SLIDER, BORDER_WIDTH);
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, GuiGetStyle(SLIDER, BORDER_WIDTH), GetColor(GuiGetStyle(SLIDER, BORDER + (state*3))), GetColor(GuiGetStyle(SLIDER, (state != STATE_DISABLED)? BASE_COLOR_NORMAL : BASE_COLOR_DISABLED)));
+
+ // Draw slider internal bar (depends on state)
+ if (state == STATE_NORMAL) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_PRESSED)));
+ else if (state == STATE_FOCUSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, TEXT_COLOR_FOCUSED)));
+ else if (state == STATE_PRESSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, TEXT_COLOR_PRESSED)));
+ else if (state == STATE_DISABLED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, TEXT_COLOR_DISABLED)));
+
+ // Draw left/right text if provided
+ if (textLeft != NULL)
+ {
+ Rectangle textBounds = { 0 };
+ textBounds.width = (float)GuiGetTextWidth(textLeft);
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x - textBounds.width - GuiGetStyle(SLIDER, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+
+ GuiDrawText(textLeft, textBounds, TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ }
+
+ if (textRight != NULL)
+ {
+ Rectangle textBounds = { 0 };
+ textBounds.width = (float)GuiGetTextWidth(textRight);
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x + bounds.width + GuiGetStyle(SLIDER, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+
+ GuiDrawText(textRight, textBounds, TEXT_ALIGN_LEFT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ }
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Slider Bar control extended, returns selected value
+int GuiSliderBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue)
+{
+ int result = 0;
+ int preSliderWidth = GuiGetStyle(SLIDER, SLIDER_WIDTH);
+ GuiSetStyle(SLIDER, SLIDER_WIDTH, 0);
+ result = GuiSlider(bounds, textLeft, textRight, value, minValue, maxValue);
+ GuiSetStyle(SLIDER, SLIDER_WIDTH, preSliderWidth);
+
+ return result;
+}
+
+// Progress Bar control extended, shows current progress value
+int GuiProgressBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ float temp = (maxValue - minValue)/2.0f;
+ if (value == NULL) value = &temp;
+
+ // Progress bar
+ Rectangle progress = { bounds.x + GuiGetStyle(PROGRESSBAR, BORDER_WIDTH),
+ bounds.y + GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) + GuiGetStyle(PROGRESSBAR, PROGRESS_PADDING), 0,
+ bounds.height - GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) - 2*GuiGetStyle(PROGRESSBAR, PROGRESS_PADDING) -1 };
+
+ // Update control
+ //--------------------------------------------------------------------
+ if (*value > maxValue) *value = maxValue;
+
+ // WARNING: Working with floats could lead to rounding issues
+ if ((state != STATE_DISABLED)) progress.width = ((float)*value/(maxValue - minValue))*(bounds.width - 2*GuiGetStyle(PROGRESSBAR, BORDER_WIDTH));
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (state == STATE_DISABLED)
+ {
+ GuiDrawRectangle(bounds, GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), GetColor(GuiGetStyle(PROGRESSBAR, BORDER + (state*3))), BLANK);
+ }
+ else
+ {
+ if (*value > minValue)
+ {
+ // Draw progress bar with colored border, more visual
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y, (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED)));
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + 1, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height - 2 }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED)));
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height - 1, (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED)));
+ }
+ else GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height+GuiGetStyle(PROGRESSBAR, BORDER_WIDTH)-1 }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL)));
+
+ if (*value >= maxValue) GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height+GuiGetStyle(PROGRESSBAR, BORDER_WIDTH)-1}, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED)));
+ else
+ {
+ // Draw borders not yet reached by value
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y, bounds.width - (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) - (int)progress.width - 1, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL)));
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y + bounds.height - 1, bounds.width - (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) - (int)progress.width - 1, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL)));
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height+GuiGetStyle(PROGRESSBAR, BORDER_WIDTH)-1 }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL)));
+ }
+
+ // Draw slider internal progress bar (depends on state)
+ if (GuiGetStyle(PROGRESSBAR, PROGRESS_SIDE) == 0) // Left-->Right
+ {
+ GuiDrawRectangle(progress, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BASE_COLOR_PRESSED)));
+ }
+ else // Right-->Left
+ {
+ progress.x = bounds.x + bounds.width - progress.width - GuiGetStyle(PROGRESSBAR, BORDER_WIDTH);
+ GuiDrawRectangle(progress, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BASE_COLOR_PRESSED)));
+ }
+ }
+
+ // Draw left/right text if provided
+ if (textLeft != NULL)
+ {
+ Rectangle textBounds = { 0 };
+ textBounds.width = (float)GuiGetTextWidth(textLeft);
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x - textBounds.width - GuiGetStyle(PROGRESSBAR, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+
+ GuiDrawText(textLeft, textBounds, TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ }
+
+ if (textRight != NULL)
+ {
+ Rectangle textBounds = { 0 };
+ textBounds.width = (float)GuiGetTextWidth(textRight);
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ textBounds.x = bounds.x + bounds.width + GuiGetStyle(PROGRESSBAR, TEXT_PADDING);
+ textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+
+ GuiDrawText(textRight, textBounds, TEXT_ALIGN_LEFT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3))));
+ }
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Status Bar control
+int GuiStatusBar(Rectangle bounds, const char *text)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, GuiGetStyle(STATUSBAR, BORDER_WIDTH), GetColor(GuiGetStyle(STATUSBAR, BORDER + (state*3))), GetColor(GuiGetStyle(STATUSBAR, BASE + (state*3))));
+ GuiDrawText(text, GetTextBounds(STATUSBAR, bounds), GuiGetStyle(STATUSBAR, TEXT_ALIGNMENT), GetColor(GuiGetStyle(STATUSBAR, TEXT + (state*3))));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Dummy rectangle control, intended for placeholding
+int GuiDummyRec(Rectangle bounds, const char *text)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ // Check button state
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN) state = STATE_PRESSED;
+ else state = STATE_FOCUSED;
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state != STATE_DISABLED)? BASE_COLOR_NORMAL : BASE_COLOR_DISABLED)));
+ GuiDrawText(text, GetTextBounds(DEFAULT, bounds), TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(BUTTON, (state != STATE_DISABLED)? TEXT_COLOR_NORMAL : TEXT_COLOR_DISABLED)));
+ //------------------------------------------------------------------
+
+ return result;
+}
+
+// List View control
+int GuiListView(Rectangle bounds, const char *text, int *scrollIndex, int *active)
+{
+ int result = 0;
+ int itemCount = 0;
+ char **items = NULL;
+
+ if (text != NULL) items = GuiTextSplit(text, ';', &itemCount);
+
+ result = GuiListViewEx(bounds, items, itemCount, scrollIndex, active, NULL);
+
+ return result;
+}
+
+// List View control using text entries list and returning focus entry
+int GuiListViewEx(Rectangle bounds, char **text, int count, int *scrollIndex, int *active, int *focus)
+{
+ int result = 0;
+ GuiState state = guiState;
+
+ int itemFocused = (focus == NULL)? -1 : *focus;
+ int itemSelected = (active == NULL)? -1 : *active;
+
+ // Check if scroll bar is needed
+ bool useScrollBar = false;
+ if ((GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING))*count > bounds.height) useScrollBar = true;
+
+ // Define base item rectangle [0]
+ Rectangle itemBounds = { 0 };
+ itemBounds.x = bounds.x + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING);
+ itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING) + GuiGetStyle(DEFAULT, BORDER_WIDTH);
+ itemBounds.width = bounds.width - 2*GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING) - GuiGetStyle(DEFAULT, BORDER_WIDTH);
+ itemBounds.height = (float)GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT);
+ if (useScrollBar) itemBounds.width -= GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH);
+
+ // Get items on the list
+ int visibleItems = (int)bounds.height/(GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING));
+ if (visibleItems > count) visibleItems = count;
+
+ int startIndex = (scrollIndex == NULL)? 0 : *scrollIndex;
+ if ((startIndex < 0) || (startIndex > (count - visibleItems))) startIndex = 0;
+ int endIndex = startIndex + visibleItems;
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ // Check mouse inside list view
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ state = STATE_FOCUSED;
+
+ // Check focused and selected item
+ for (int i = 0; i < visibleItems; i++)
+ {
+ if (CheckCollisionPointRec(mousePoint, itemBounds))
+ {
+ itemFocused = startIndex + i;
+ if (GUI_BUTTON_PRESSED)
+ {
+ if (itemSelected == (startIndex + i)) itemSelected = -1;
+ else itemSelected = startIndex + i;
+ }
+ break;
+ }
+
+ // Update item rectangle y position for next item
+ itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING));
+ }
+
+ if (useScrollBar)
+ {
+ float scrollDelta = GUI_SCROLL_DELTA;
+ startIndex -= (int)scrollDelta;
+
+ if (startIndex < 0) startIndex = 0;
+ else if (startIndex > (count - visibleItems)) startIndex = count - visibleItems;
+
+ endIndex = startIndex + visibleItems;
+ if (endIndex > count) endIndex = count;
+ }
+ }
+ else itemFocused = -1;
+
+ // Reset item rectangle y to [0]
+ itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING) + GuiGetStyle(DEFAULT, BORDER_WIDTH);
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER + state*3)), GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); // Draw background
+
+ // Draw visible items
+ for (int i = 0; ((i < visibleItems) && (text != NULL)); i++)
+ {
+ if (GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_NORMAL)) GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_NORMAL)), BLANK);
+
+ if (state == STATE_DISABLED)
+ {
+ if ((startIndex + i) == itemSelected) GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_DISABLED)), GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_DISABLED)));
+
+ GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_DISABLED)));
+ }
+ else
+ {
+ if (((startIndex + i) == itemSelected) && (active != NULL))
+ {
+ // Draw item selected
+ GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_PRESSED)), GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_PRESSED)));
+ GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_PRESSED)));
+ }
+ else if (((startIndex + i) == itemFocused)) // && (focus != NULL)) // NOTE: Items focused, despite not returned
+ {
+ // Draw item focused
+ GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_FOCUSED)), GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_FOCUSED)));
+ GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_FOCUSED)));
+ }
+ else
+ {
+ // Draw item normal (no rectangle)
+ GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_NORMAL)));
+ }
+ }
+
+ // Update item rectangle y position for next item
+ itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING));
+ }
+
+ if (useScrollBar)
+ {
+ Rectangle scrollBarBounds = {
+ bounds.x + bounds.width - GuiGetStyle(LISTVIEW, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH),
+ bounds.y + GuiGetStyle(LISTVIEW, BORDER_WIDTH), (float)GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH),
+ bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH)
+ };
+
+ // Calculate percentage of visible items and apply same percentage to scrollbar
+ float percentVisible = (float)(endIndex - startIndex)/count;
+ float sliderSize = bounds.height*percentVisible;
+
+ int prevSliderSize = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE); // Save default slider size
+ int prevScrollSpeed = GuiGetStyle(SCROLLBAR, SCROLL_SPEED); // Save default scroll speed
+ GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, (int)sliderSize); // Change slider size
+ GuiSetStyle(SCROLLBAR, SCROLL_SPEED, count - visibleItems); // Change scroll speed
+
+ startIndex = GuiScrollBar(scrollBarBounds, startIndex, 0, count - visibleItems);
+
+ GuiSetStyle(SCROLLBAR, SCROLL_SPEED, prevScrollSpeed); // Reset scroll speed to default
+ GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, prevSliderSize); // Reset slider size to default
+ }
+ //--------------------------------------------------------------------
+
+ if (active != NULL) *active = itemSelected;
+ if (focus != NULL) *focus = itemFocused;
+ if (scrollIndex != NULL) *scrollIndex = startIndex;
+
+ return result;
+}
+
+// Tab Bar control
+int GuiTabBar(Rectangle bounds, const char *text, int *hscroll, int *active)
+{
+ int result = 0;
+ int itemCount = 0;
+ char **items = NULL;
+
+ if (text != NULL) items = GuiTextSplit(text, ';', &itemCount);
+
+ result = GuiTabBarEx(bounds, items, itemCount, hscroll, active, NULL);
+
+ return result;
+}
+
+// Tab Bar control, using text entries list and returning focus entry
+// TODO: Remove using GuiToggle() for the TABS
+int GuiTabBarEx(Rectangle bounds, char **text, int count, int *hscroll, int *active, int *focus)
+{
+ int result = -1;
+ //GuiState state = guiState;
+
+ int tabItemsWidth = GuiGetStyle(TABBAR, TAB_ITEMS_WIDTH);
+ Rectangle tabBounds = { bounds.x, bounds.y, (float)tabItemsWidth, bounds.height };
+
+ if (*active < 0) *active = 0;
+ else if (*active > count - 1) *active = count - 1;
+
+ int offsetX = 0; // Required in case tabs go out of screen
+ offsetX = (*active*tabItemsWidth) - GetScreenWidth();
+ if (offsetX < 0) offsetX = 0;
+
+ bool toggle = false; // Required for individual toggles
+
+ // Draw control
+ //--------------------------------------------------------------------
+ for (int i = 0; i < count; i++)
+ {
+ tabBounds.x = bounds.x + (tabItemsWidth + 4)*i + offsetX;
+
+ if (tabBounds.x < GetScreenWidth())
+ {
+ // Draw tabs as toggle controls
+ int textAlignment = GuiGetStyle(TOGGLE, TEXT_ALIGNMENT);
+ int textPadding = GuiGetStyle(TOGGLE, TEXT_PADDING);
+ GuiSetStyle(TOGGLE, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+ GuiSetStyle(TOGGLE, TEXT_PADDING, 8);
+
+ if (i == (*active))
+ {
+ toggle = true;
+ GuiToggle(tabBounds, text[i], &toggle);
+ }
+ else
+ {
+ toggle = false;
+ GuiToggle(tabBounds, text[i], &toggle);
+ if (toggle) *active = i;
+ }
+
+ // Close tab with middle mouse button pressed
+ if (CheckCollisionPointRec(GUI_POINTER_POSITION, tabBounds) && GUI_BUTTON_PRESSED_MID) result = i;
+
+ GuiSetStyle(TOGGLE, TEXT_PADDING, textPadding);
+ GuiSetStyle(TOGGLE, TEXT_ALIGNMENT, textAlignment);
+
+ if (GuiGetStyle(TABBAR, TAB_CLOSE_BUTTON))
+ {
+ // Draw tab close button
+ // NOTE: Only draw close button for current tab: if (CheckCollisionPointRec(mousePosition, tabBounds))
+ int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH);
+ int tempTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT);
+ GuiSetStyle(BUTTON, BORDER_WIDTH, 1);
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+#if defined(RAYGUI_NO_ICONS)
+ if (GuiButton(RAYGUI_CLITERAL(Rectangle){ tabBounds.x + tabBounds.width - 14 - 5, tabBounds.y + 5, 14, 14 }, "x")) result = i;
+#else
+ if (GuiButton(RAYGUI_CLITERAL(Rectangle){ tabBounds.x + tabBounds.width - 14 - 5, tabBounds.y + 5, 14, 14 },
+ GuiIconText(ICON_CROSS_SMALL, NULL))) result = i;
+#endif
+ GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth);
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlignment);
+ }
+ }
+ }
+
+ // Draw tab-bar bottom line
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height - 1, bounds.width, 1 }, 0, BLANK, GetColor(GuiGetStyle(TABBAR, BORDER_COLOR_NORMAL)));
+ //--------------------------------------------------------------------
+
+ return result; // Return as result the current TAB closing requested
+}
+
+// Color Panel control
+int GuiColorPanel(Rectangle bounds, const char *text, Color *color)
+{
+ int result = 0;
+
+ Vector3 vcolor = { (float)color->r/255.0f, (float)color->g/255.0f, (float)color->b/255.0f };
+ Vector3 hsv = ConvertRGBtoHSV(vcolor);
+ Vector3 prevHsv = hsv; // NOTE: Workaround to see if GuiColorPanelHSV() modifies the hsv
+
+ GuiColorPanelHSV(bounds, text, &hsv);
+
+ // Check if the hsv was changed, only then change the color
+ // This is required, because the Color->HSV->Color conversion has precision errors
+ // Thus the assignment from HSV to Color should only be made, if the HSV has a new user-entered value
+ // Otherwise GuiColorPanel would often modify it's color without user input
+ // TODO: GuiColorPanelHSV() could return 1 if the slider was dragged, to simplify this check
+ if ((hsv.x != prevHsv.x) || (hsv.y != prevHsv.y) || (hsv.z != prevHsv.z))
+ {
+ Vector3 rgb = ConvertHSVtoRGB(hsv);
+ *color = RAYGUI_CLITERAL(Color){ (unsigned char)(255.0f*rgb.x), (unsigned char)(255.0f*rgb.y), (unsigned char)(255.0f*rgb.z), color->a };
+ }
+
+ return result;
+}
+
+// Color Bar Alpha control
+// NOTE: Returns alpha value normalized [0..1]
+int GuiColorBarAlpha(Rectangle bounds, const char *text, float *alpha)
+{
+ #if !defined(RAYGUI_COLORBARALPHA_CHECKED_SIZE)
+ #define RAYGUI_COLORBARALPHA_CHECKED_SIZE 10
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+ Rectangle selector = { (float)bounds.x + (*alpha)*bounds.width - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT)/2,
+ (float)bounds.y - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW),
+ (float)GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT),
+ (float)bounds.height + GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW)*2 };
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec))
+ {
+ state = STATE_PRESSED;
+
+ *alpha = (mousePoint.x - bounds.x)/bounds.width;
+ if (*alpha <= 0.0f) *alpha = 0.0f;
+ if (*alpha >= 1.0f) *alpha = 1.0f;
+ }
+ }
+ else
+ {
+ guiControlExclusiveMode = false;
+ guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 };
+ }
+ }
+ else if (CheckCollisionPointRec(mousePoint, bounds) || CheckCollisionPointRec(mousePoint, selector))
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ state = STATE_PRESSED;
+ guiControlExclusiveMode = true;
+ guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts
+
+ *alpha = (mousePoint.x - bounds.x)/bounds.width;
+ if (*alpha <= 0.0f) *alpha = 0.0f;
+ if (*alpha >= 1.0f) *alpha = 1.0f;
+ //selector.x = bounds.x + (int)(((alpha - 0)/(100 - 0))*(bounds.width - 2*GuiGetStyle(SLIDER, BORDER_WIDTH))) - selector.width/2;
+ }
+ else state = STATE_FOCUSED;
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ // Draw alpha bar: checked background
+ if (state != STATE_DISABLED)
+ {
+ int checksX = (int)bounds.width/RAYGUI_COLORBARALPHA_CHECKED_SIZE;
+ int checksY = (int)bounds.height/RAYGUI_COLORBARALPHA_CHECKED_SIZE;
+
+ for (int x = 0; x < checksX; x++)
+ {
+ for (int y = 0; y < checksY; y++)
+ {
+ Rectangle check = { bounds.x + x*RAYGUI_COLORBARALPHA_CHECKED_SIZE, bounds.y + y*RAYGUI_COLORBARALPHA_CHECKED_SIZE, RAYGUI_COLORBARALPHA_CHECKED_SIZE, RAYGUI_COLORBARALPHA_CHECKED_SIZE };
+ GuiDrawRectangle(check, 0, BLANK, ((x + y)%2)? Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), 0.4f) : Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.4f));
+ }
+ }
+
+ DrawRectangleGradientEx(bounds, RAYGUI_CLITERAL(Color){ 255, 255, 255, 0 }, RAYGUI_CLITERAL(Color){ 255, 255, 255, 0 }, Fade(RAYGUI_CLITERAL(Color){ 0, 0, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 0, 0, 255 }, guiAlpha));
+ }
+ else DrawRectangleGradientEx(bounds, Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), guiAlpha), Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), guiAlpha));
+
+ GuiDrawRectangle(bounds, GuiGetStyle(COLORPICKER, BORDER_WIDTH), GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)), BLANK);
+
+ // Draw alpha bar: selector
+ GuiDrawRectangle(selector, 0, BLANK, GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Color Bar Hue control
+// Returns hue value normalized [0..1]
+// NOTE: Other similar bars (for reference):
+// Color GuiColorBarSat() [WHITE->color]
+// Color GuiColorBarValue() [BLACK->color], HSV/HSL
+// float GuiColorBarLuminance() [BLACK->WHITE]
+int GuiColorBarHue(Rectangle bounds, const char *text, float *hue)
+{
+ int result = 0;
+ GuiState state = guiState;
+ Rectangle selector = { (float)bounds.x - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW), (float)bounds.y + (*hue)/360.0f*bounds.height - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT)/2, (float)bounds.width + GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW)*2, (float)GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT) };
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec))
+ {
+ state = STATE_PRESSED;
+
+ *hue = (mousePoint.y - bounds.y)*360/bounds.height;
+ if (*hue <= 0.0f) *hue = 0.0f;
+ if (*hue >= 359.0f) *hue = 359.0f;
+ }
+ }
+ else
+ {
+ guiControlExclusiveMode = false;
+ guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 };
+ }
+ }
+ else if (CheckCollisionPointRec(mousePoint, bounds) || CheckCollisionPointRec(mousePoint, selector))
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ state = STATE_PRESSED;
+ guiControlExclusiveMode = true;
+ guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts
+
+ *hue = (mousePoint.y - bounds.y)*360/bounds.height;
+ if (*hue <= 0.0f) *hue = 0.0f;
+ if (*hue >= 359.0f) *hue = 359.0f;
+
+ }
+ else state = STATE_FOCUSED;
+
+ /*if (GUI_KEY_DOWN(KEY_UP))
+ {
+ hue -= 2.0f;
+ if (hue <= 0.0f) hue = 0.0f;
+ }
+ else if (GUI_KEY_DOWN(KEY_DOWN))
+ {
+ hue += 2.0f;
+ if (hue >= 360.0f) hue = 360.0f;
+ }*/
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (state != STATE_DISABLED)
+ {
+ // Draw hue bar:color bars
+ // TODO: Use directly DrawRectangleGradientEx(bounds, color1, color2, color2, color1);
+ DrawRectangleGradientV((int)bounds.x, (int)(bounds.y), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 255, 255, 0, 255 }, guiAlpha));
+ DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + bounds.height/6), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 255, 255, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 0, 255 }, guiAlpha));
+ DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 2*(bounds.height/6)), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 255, 255 }, guiAlpha));
+ DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 3*(bounds.height/6)), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 255, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 0, 255, 255 }, guiAlpha));
+ DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 4*(bounds.height/6)), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 0, 0, 255, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 255, 255 }, guiAlpha));
+ DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 5*(bounds.height/6)), (int)bounds.width, (int)(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 255, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 0, 255 }, guiAlpha));
+ }
+ else DrawRectangleGradientV((int)bounds.x, (int)bounds.y, (int)bounds.width, (int)bounds.height, Fade(Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), guiAlpha), Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), guiAlpha));
+
+ GuiDrawRectangle(bounds, GuiGetStyle(COLORPICKER, BORDER_WIDTH), GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)), BLANK);
+
+ // Draw hue bar: selector
+ GuiDrawRectangle(selector, 0, BLANK, GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)));
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Color Picker control
+// NOTE: It's divided in multiple controls:
+// Color GuiColorPanel(Rectangle bounds, Color color)
+// float GuiColorBarAlpha(Rectangle bounds, float alpha)
+// float GuiColorBarHue(Rectangle bounds, float value)
+// NOTE: bounds define GuiColorPanel() size
+// NOTE: this picker converts RGB to HSV, which can cause the Hue control to jump. If you have this problem, consider using the HSV variant instead
+int GuiColorPicker(Rectangle bounds, const char *text, Color *color)
+{
+ int result = 0;
+
+ Color temp = { 200, 0, 0, 255 };
+ if (color == NULL) color = &temp;
+
+ GuiColorPanel(bounds, NULL, color);
+
+ Rectangle boundsHue = { (float)bounds.x + bounds.width + GuiGetStyle(COLORPICKER, HUEBAR_PADDING), (float)bounds.y, (float)GuiGetStyle(COLORPICKER, HUEBAR_WIDTH), (float)bounds.height };
+ //Rectangle boundsAlpha = { bounds.x, bounds.y + bounds.height + GuiGetStyle(COLORPICKER, BARS_PADDING), bounds.width, GuiGetStyle(COLORPICKER, BARS_THICK) };
+
+ // NOTE: this conversion can cause low hue-resolution, if the r, g and b value are very similar, which causes the hue bar to shift around when only the GuiColorPanel is used
+ Vector3 hsv = ConvertRGBtoHSV(RAYGUI_CLITERAL(Vector3){ (*color).r/255.0f, (*color).g/255.0f, (*color).b/255.0f });
+
+ GuiColorBarHue(boundsHue, NULL, &hsv.x);
+
+ //color.a = (unsigned char)(GuiColorBarAlpha(boundsAlpha, (float)color.a/255.0f)*255.0f);
+ Vector3 rgb = ConvertHSVtoRGB(hsv);
+
+ *color = RAYGUI_CLITERAL(Color){ (unsigned char)roundf(rgb.x*255.0f), (unsigned char)roundf(rgb.y*255.0f), (unsigned char)roundf(rgb.z*255.0f), (*color).a };
+
+ return result;
+}
+
+// Color Picker control that avoids conversion to RGB and back to HSV on each call, thus avoiding jittering
+// The user can call ConvertHSVtoRGB() to convert *colorHsv value to RGB
+// NOTE: It's divided in multiple controls:
+// int GuiColorPanelHSV(Rectangle bounds, const char *text, Vector3 *colorHsv)
+// int GuiColorBarAlpha(Rectangle bounds, const char *text, float *alpha)
+// float GuiColorBarHue(Rectangle bounds, float value)
+// NOTE: bounds define GuiColorPanelHSV() size
+int GuiColorPickerHSV(Rectangle bounds, const char *text, Vector3 *colorHsv)
+{
+ int result = 0;
+
+ Vector3 tempHsv = { 0 };
+
+ if (colorHsv == NULL)
+ {
+ const Vector3 tempColor = { 200.0f/255.0f, 0.0f, 0.0f };
+ tempHsv = ConvertRGBtoHSV(tempColor);
+ colorHsv = &tempHsv;
+ }
+
+ GuiColorPanelHSV(bounds, NULL, colorHsv);
+
+ Rectangle boundsHue = { (float)bounds.x + bounds.width + GuiGetStyle(COLORPICKER, HUEBAR_PADDING), (float)bounds.y, (float)GuiGetStyle(COLORPICKER, HUEBAR_WIDTH), (float)bounds.height };
+
+ GuiColorBarHue(boundsHue, NULL, &colorHsv->x);
+
+ return result;
+}
+
+// Color Panel control - HSV variant
+int GuiColorPanelHSV(Rectangle bounds, const char *text, Vector3 *colorHsv)
+{
+ int result = 0;
+ GuiState state = guiState;
+ Vector2 pickerSelector = { 0 };
+
+ const Color colWhite = { 255, 255, 255, 255 };
+ const Color colBlack = { 0, 0, 0, 255 };
+
+ pickerSelector.x = bounds.x + (float)colorHsv->y*bounds.width; // HSV: Saturation
+ pickerSelector.y = bounds.y + (1.0f - (float)colorHsv->z)*bounds.height; // HSV: Value
+
+ Vector3 maxHue = { colorHsv->x, 1.0f, 1.0f };
+ Vector3 rgbHue = ConvertHSVtoRGB(maxHue);
+ Color maxHueCol = { (unsigned char)(255.0f*rgbHue.x), (unsigned char)(255.0f*rgbHue.y), (unsigned char)(255.0f*rgbHue.z), 255 };
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec))
+ {
+ pickerSelector = mousePoint;
+
+ if (pickerSelector.x < bounds.x) pickerSelector.x = bounds.x;
+ if (pickerSelector.x > bounds.x + bounds.width) pickerSelector.x = bounds.x + bounds.width;
+ if (pickerSelector.y < bounds.y) pickerSelector.y = bounds.y;
+ if (pickerSelector.y > bounds.y + bounds.height) pickerSelector.y = bounds.y + bounds.height;
+
+ // Calculate color from picker
+ Vector2 colorPick = { pickerSelector.x - bounds.x, pickerSelector.y - bounds.y };
+
+ colorPick.x /= (float)bounds.width; // Get normalized value on x
+ colorPick.y /= (float)bounds.height; // Get normalized value on y
+
+ colorHsv->y = colorPick.x;
+ colorHsv->z = 1.0f - colorPick.y;
+ }
+ }
+ else
+ {
+ guiControlExclusiveMode = false;
+ guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 };
+ }
+ }
+ else if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ if (GUI_BUTTON_DOWN)
+ {
+ state = STATE_PRESSED;
+ guiControlExclusiveMode = true;
+ guiControlExclusiveRec = bounds;
+ pickerSelector = mousePoint;
+
+ // Calculate color from picker
+ Vector2 colorPick = { pickerSelector.x - bounds.x, pickerSelector.y - bounds.y };
+
+ colorPick.x /= (float)bounds.width; // Get normalized value on x
+ colorPick.y /= (float)bounds.height; // Get normalized value on y
+
+ colorHsv->y = colorPick.x;
+ colorHsv->z = 1.0f - colorPick.y;
+ }
+ else state = STATE_FOCUSED;
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (state != STATE_DISABLED)
+ {
+ DrawRectangleGradientEx(bounds, Fade(colWhite, guiAlpha), Fade(colWhite, guiAlpha), Fade(maxHueCol, guiAlpha), Fade(maxHueCol, guiAlpha));
+ DrawRectangleGradientEx(bounds, Fade(colBlack, 0), Fade(colBlack, guiAlpha), Fade(colBlack, guiAlpha), Fade(colBlack, 0));
+
+ // Draw color picker: selector
+ Rectangle selector = { pickerSelector.x - GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE)/2, pickerSelector.y - GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE)/2, (float)GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE), (float)GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE) };
+ GuiDrawRectangle(selector, 0, BLANK, colWhite);
+ }
+ else
+ {
+ DrawRectangleGradientEx(bounds, Fade(Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), guiAlpha), Fade(Fade(colBlack, 0.6f), guiAlpha), Fade(Fade(colBlack, 0.6f), guiAlpha), Fade(Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), 0.6f), guiAlpha));
+ }
+
+ GuiDrawRectangle(bounds, GuiGetStyle(COLORPICKER, BORDER_WIDTH), GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)), BLANK);
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Message Box control
+int GuiMessageBox(Rectangle bounds, const char *title, const char *message, const char *buttons)
+{
+ #if !defined(RAYGUI_MESSAGEBOX_BUTTON_HEIGHT)
+ #define RAYGUI_MESSAGEBOX_BUTTON_HEIGHT 24
+ #endif
+ #if !defined(RAYGUI_MESSAGEBOX_BUTTON_PADDING)
+ #define RAYGUI_MESSAGEBOX_BUTTON_PADDING 12
+ #endif
+
+ int result = -1; // Returns clicked button from buttons list, 0 refers to closed window button
+
+ int buttonCount = 0;
+ char **buttonsText = GuiTextSplit(buttons, ';', &buttonCount);
+ Rectangle buttonBounds = { 0 };
+ buttonBounds.x = bounds.x + RAYGUI_MESSAGEBOX_BUTTON_PADDING;
+ buttonBounds.y = bounds.y + bounds.height - RAYGUI_MESSAGEBOX_BUTTON_HEIGHT - RAYGUI_MESSAGEBOX_BUTTON_PADDING;
+ buttonBounds.width = (bounds.width - RAYGUI_MESSAGEBOX_BUTTON_PADDING*(buttonCount + 1))/buttonCount;
+ buttonBounds.height = RAYGUI_MESSAGEBOX_BUTTON_HEIGHT;
+
+ //int textWidth = GuiGetTextWidth(message) + 2;
+
+ Rectangle textBounds = { 0 };
+ textBounds.x = bounds.x + RAYGUI_MESSAGEBOX_BUTTON_PADDING;
+ textBounds.y = bounds.y + RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT + RAYGUI_MESSAGEBOX_BUTTON_PADDING;
+ textBounds.width = bounds.width - RAYGUI_MESSAGEBOX_BUTTON_PADDING*2;
+ textBounds.height = bounds.height - RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 3*RAYGUI_MESSAGEBOX_BUTTON_PADDING - RAYGUI_MESSAGEBOX_BUTTON_HEIGHT;
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (GuiWindowBox(bounds, title)) result = 0;
+
+ int prevTextAlignment = GuiGetStyle(LABEL, TEXT_ALIGNMENT);
+ GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ GuiLabel(textBounds, message);
+ GuiSetStyle(LABEL, TEXT_ALIGNMENT, prevTextAlignment);
+
+ prevTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT);
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+
+ for (int i = 0; i < buttonCount; i++)
+ {
+ if (GuiButton(buttonBounds, buttonsText[i])) result = i + 1;
+ buttonBounds.x += (buttonBounds.width + RAYGUI_MESSAGEBOX_BUTTON_PADDING);
+ }
+
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, prevTextAlignment);
+ //--------------------------------------------------------------------
+
+ return result;
+}
+
+// Text Input Box control, ask for text
+int GuiTextInputBox(Rectangle bounds, const char *title, const char *message, const char *buttons, char *text, int textMaxSize, bool *secretViewActive)
+{
+ #if !defined(RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT)
+ #define RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT 24
+ #endif
+ #if !defined(RAYGUI_TEXTINPUTBOX_BUTTON_PADDING)
+ #define RAYGUI_TEXTINPUTBOX_BUTTON_PADDING 12
+ #endif
+ #if !defined(RAYGUI_TEXTINPUTBOX_HEIGHT)
+ #define RAYGUI_TEXTINPUTBOX_HEIGHT 26
+ #endif
+
+ // Used to enable text edit mode
+ // WARNING: No more than one GuiTextInputBox() should be open at the same time
+ static bool textEditMode = false;
+
+ int result = -1;
+
+ int buttonCount = 0;
+ char **buttonsText = GuiTextSplit(buttons, ';', &buttonCount);
+ Rectangle buttonBounds = { 0 };
+ buttonBounds.x = bounds.x + RAYGUI_TEXTINPUTBOX_BUTTON_PADDING;
+ buttonBounds.y = bounds.y + bounds.height - RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT - RAYGUI_TEXTINPUTBOX_BUTTON_PADDING;
+ buttonBounds.width = (bounds.width - RAYGUI_TEXTINPUTBOX_BUTTON_PADDING*(buttonCount + 1))/buttonCount;
+ buttonBounds.height = RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT;
+
+ int messageInputHeight = (int)bounds.height - RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - GuiGetStyle(STATUSBAR, BORDER_WIDTH) - RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT - 2*RAYGUI_TEXTINPUTBOX_BUTTON_PADDING;
+
+ Rectangle textBounds = { 0 };
+ if (message != NULL)
+ {
+ int textSize = GuiGetTextWidth(message) + 2;
+
+ textBounds.x = bounds.x + bounds.width/2 - textSize/2;
+ textBounds.y = bounds.y + RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT + messageInputHeight/4 - (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/2;
+ textBounds.width = (float)textSize;
+ textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+ }
+
+ Rectangle textBoxBounds = { 0 };
+ textBoxBounds.x = bounds.x + RAYGUI_TEXTINPUTBOX_BUTTON_PADDING;
+ textBoxBounds.y = bounds.y + RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - RAYGUI_TEXTINPUTBOX_HEIGHT/2;
+ if (message == NULL) textBoxBounds.y = bounds.y + 24 + RAYGUI_TEXTINPUTBOX_BUTTON_PADDING;
+ else textBoxBounds.y += (messageInputHeight/2 + messageInputHeight/4);
+ textBoxBounds.width = bounds.width - RAYGUI_TEXTINPUTBOX_BUTTON_PADDING*2;
+ textBoxBounds.height = RAYGUI_TEXTINPUTBOX_HEIGHT;
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (GuiWindowBox(bounds, title)) result = 0;
+
+ // Draw message if available
+ if (message != NULL)
+ {
+ int prevTextAlignment = GuiGetStyle(LABEL, TEXT_ALIGNMENT);
+ GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ GuiLabel(textBounds, message);
+ GuiSetStyle(LABEL, TEXT_ALIGNMENT, prevTextAlignment);
+ }
+
+ int prevTextBoxAlignment = GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT);
+ GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+
+ if (secretViewActive != NULL)
+ {
+ static char stars[] = "****************";
+ if (GuiTextBox(RAYGUI_CLITERAL(Rectangle){ textBoxBounds.x, textBoxBounds.y, textBoxBounds.width - 4 - RAYGUI_TEXTINPUTBOX_HEIGHT, textBoxBounds.height },
+ ((*secretViewActive == 1) || textEditMode)? text : stars, textMaxSize, textEditMode)) textEditMode = !textEditMode;
+
+#if defined(RAYGUI_NO_ICONS)
+ GuiToggle(RAYGUI_CLITERAL(Rectangle){ textBoxBounds.x + textBoxBounds.width - RAYGUI_TEXTINPUTBOX_HEIGHT, textBoxBounds.y, RAYGUI_TEXTINPUTBOX_HEIGHT, RAYGUI_TEXTINPUTBOX_HEIGHT },
+ (*secretViewActive == 1)? "O" : "*", secretViewActive);
+#else
+ GuiToggle(RAYGUI_CLITERAL(Rectangle){ textBoxBounds.x + textBoxBounds.width - RAYGUI_TEXTINPUTBOX_HEIGHT, textBoxBounds.y, RAYGUI_TEXTINPUTBOX_HEIGHT, RAYGUI_TEXTINPUTBOX_HEIGHT },
+ (*secretViewActive == 1)? GuiIconText(ICON_EYE_ON, NULL) : GuiIconText(ICON_EYE_OFF, NULL), secretViewActive);
+#endif
+ }
+ else
+ {
+ if (GuiTextBox(textBoxBounds, text, textMaxSize, textEditMode)) textEditMode = !textEditMode;
+ }
+
+ GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT, prevTextBoxAlignment);
+
+ int prevBtnTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT);
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+
+ for (int i = 0; i < buttonCount; i++)
+ {
+ if (GuiButton(buttonBounds, buttonsText[i])) result = i + 1;
+ buttonBounds.x += (buttonBounds.width + RAYGUI_MESSAGEBOX_BUTTON_PADDING);
+ }
+
+ if (result >= 0) textEditMode = false;
+
+ GuiSetStyle(BUTTON, TEXT_ALIGNMENT, prevBtnTextAlignment);
+ //--------------------------------------------------------------------
+
+ return result; // Result is the pressed button index
+}
+
+// Grid control
+// NOTE: Returns grid mouse-hover selected cell
+// About drawing lines at subpixel spacing, simple put, not easy solution:
+// REF: https://stackoverflow.com/questions/4435450/2d-opengl-drawing-lines-that-dont-exactly-fit-pixel-raster
+int GuiGrid(Rectangle bounds, const char *text, float spacing, int subdivs, Vector2 *mouseCell)
+{
+ // Grid lines alpha amount
+ #if !defined(RAYGUI_GRID_ALPHA)
+ #define RAYGUI_GRID_ALPHA 0.15f
+ #endif
+
+ int result = 0;
+ GuiState state = guiState;
+
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+ Vector2 currentMouseCell = { -1, -1 };
+
+ float spaceWidth = spacing/(float)subdivs;
+ int linesV = (int)(bounds.width/spaceWidth) + 1;
+ int linesH = (int)(bounds.height/spaceWidth) + 1;
+
+ int color = GuiGetStyle(DEFAULT, LINE_COLOR);
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode)
+ {
+ if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ // NOTE: Cell values must be the upper left of the cell the mouse is in
+ currentMouseCell.x = floorf((mousePoint.x - bounds.x)/spacing);
+ currentMouseCell.y = floorf((mousePoint.y - bounds.y)/spacing);
+ }
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ if (state == STATE_DISABLED) color = GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED);
+
+ if (subdivs > 0)
+ {
+ // Draw vertical grid lines
+ for (int i = 0; i < linesV; i++)
+ {
+ Rectangle lineV = { bounds.x + spacing*i/subdivs, bounds.y, 1, bounds.height + 1 };
+ GuiDrawRectangle(lineV, 0, BLANK, ((i%subdivs) == 0)? GuiFade(GetColor(color), RAYGUI_GRID_ALPHA*4) : GuiFade(GetColor(color), RAYGUI_GRID_ALPHA));
+ }
+
+ // Draw horizontal grid lines
+ for (int i = 0; i < linesH; i++)
+ {
+ Rectangle lineH = { bounds.x, bounds.y + spacing*i/subdivs, bounds.width + 1, 1 };
+ GuiDrawRectangle(lineH, 0, BLANK, ((i%subdivs) == 0)? GuiFade(GetColor(color), RAYGUI_GRID_ALPHA*4) : GuiFade(GetColor(color), RAYGUI_GRID_ALPHA));
+ }
+ }
+
+ if (mouseCell != NULL) *mouseCell = currentMouseCell;
+ return result;
+}
+
+//----------------------------------------------------------------------------------
+// Tooltip management functions
+// NOTE: Tooltips requires some global variables: tooltipPtr
+//----------------------------------------------------------------------------------
+// Enable gui tooltips (global state)
+void GuiEnableTooltip(void) { guiTooltip = true; }
+
+// Disable gui tooltips (global state)
+void GuiDisableTooltip(void) { guiTooltip = false; }
+
+// Set tooltip string
+void GuiSetTooltip(const char *tooltip) { guiTooltipPtr = tooltip; }
+
+//----------------------------------------------------------------------------------
+// Styles loading functions
+//----------------------------------------------------------------------------------
+
+// Load raygui style file (.rgs)
+// NOTE: By default a binary file is expected, that file could contain a custom font,
+// in that case, custom font image atlas is GRAY+ALPHA and pixel data can be compressed (DEFLATE)
+void GuiLoadStyle(const char *fileName)
+{
+ #define MAX_LINE_BUFFER_SIZE 256
+
+ bool tryBinary = false;
+ if (!guiStyleLoaded) GuiLoadStyleDefault();
+
+ // Try reading the files as text file first
+ FILE *rgsFile = fopen(fileName, "rt");
+
+ if (rgsFile != NULL)
+ {
+ char buffer[MAX_LINE_BUFFER_SIZE] = { 0 };
+ fgets(buffer, MAX_LINE_BUFFER_SIZE, rgsFile);
+
+ if (buffer[0] == '#')
+ {
+ int controlId = 0;
+ int propertyId = 0;
+ unsigned int propertyValue = 0;
+
+ while (!feof(rgsFile))
+ {
+ switch (buffer[0])
+ {
+ case 'p':
+ {
+ // Style property: p <control_id> <property_id> <property_value> <property_name>
+
+ sscanf(buffer, "p %d %d 0x%x", &controlId, &propertyId, &propertyValue);
+ GuiSetStyle(controlId, propertyId, (int)propertyValue);
+
+ } break;
+ case 'f':
+ {
+ // Style font: f <gen_font_size> <charmap_file> <font_file>
+
+ int fontSize = 0;
+ char charmapFileName[256] = { 0 };
+ char fontFileName[256] = { 0 };
+ sscanf(buffer, "f %d %s %[^\r\n]s", &fontSize, charmapFileName, fontFileName);
+
+ // GLOBAL: Copy font file name into guiFontName
+ memcpy(guiFontName, fontFileName, 31);
+ guiFontName[31] = '\0';
+
+ Font font = { 0 };
+ int *codepoints = NULL;
+ int codepointCount = 0;
+
+ if (charmapFileName[0] != '0')
+ {
+ // Load text data from file
+ // NOTE: Expected an UTF-8 array of codepoints, no separation
+ char *textData = LoadFileText(TextFormat("%s/%s", GetDirectoryPath(fileName), charmapFileName));
+ codepoints = LoadCodepoints(textData, &codepointCount);
+ UnloadFileText(textData);
+ }
+
+ if (fontFileName[0] != '\0')
+ {
+ // In case a font is already loaded and it is not default internal font, unload it
+ if (font.texture.id != GetFontDefault().texture.id) UnloadTexture(font.texture);
+
+ if (codepointCount > 0) font = LoadFontEx(TextFormat("%s/%s", GetDirectoryPath(fileName), fontFileName), fontSize, codepoints, codepointCount);
+ else font = LoadFontEx(TextFormat("%s/%s", GetDirectoryPath(fileName), fontFileName), fontSize, NULL, 0); // Default to 95 standard codepoints
+ }
+
+ // If font texture not properly loaded, revert to default font and size/spacing
+ if (font.texture.id == 0)
+ {
+ font = GetFontDefault();
+ GuiSetStyle(DEFAULT, TEXT_SIZE, 10);
+ GuiSetStyle(DEFAULT, TEXT_SPACING, 1);
+ }
+
+ UnloadCodepoints(codepoints);
+
+ if ((font.texture.id > 0) && (font.glyphCount > 0)) GuiSetFont(font);
+
+ } break;
+ default: break;
+ }
+
+ fgets(buffer, MAX_LINE_BUFFER_SIZE, rgsFile);
+ }
+ }
+ else tryBinary = true;
+
+ fclose(rgsFile);
+ }
+
+ if (tryBinary)
+ {
+ rgsFile = fopen(fileName, "rb");
+
+ if (rgsFile != NULL)
+ {
+ fseek(rgsFile, 0, SEEK_END);
+ int fileDataSize = ftell(rgsFile);
+ fseek(rgsFile, 0, SEEK_SET);
+
+ if (fileDataSize > 0)
+ {
+ unsigned char *fileData = (unsigned char *)RAYGUI_CALLOC(fileDataSize, sizeof(unsigned char));
+ if (fileData != NULL)
+ {
+ fread(fileData, sizeof(unsigned char), fileDataSize, rgsFile);
+
+ GuiLoadStyleFromMemory(fileData, fileDataSize);
+
+ RAYGUI_FREE(fileData);
+ }
+ }
+
+ fclose(rgsFile);
+ }
+ }
+}
+
+// Load style from memory
+// WARNING: Binary files only
+void GuiLoadStyleFromMemory(const unsigned char *fileData, int dataSize)
+{
+ // Style File Structure (.rgs)
+ // ------------------------------------------------------
+ // Offset | Size | Type | Description
+ // ------------------------------------------------------
+ // 0 | 4 | char | Signature: "rGS "
+ // 4 | 2 | short | Version: 200, 400, 600
+ // 6 | 2 | short | reserved
+ // 8 | 4 | int | Num properties (only changed ones from default style)
+
+ // Properties Data (8 bytes per property)
+ // WARNING: Only properties required that differ from default (light) internal style
+ // foreach (property)
+ // {
+ // 8+8*i | 2 | short | ControlId
+ // 8+8*i | 2 | short | PropertyId
+ // 8+8*i | 4 | int | PropertyValue
+ // }
+
+ // Custom Font Data : Parameters (64 bytes)
+ // ... | 4 | int | Font data size (0 - no font, no more fields added!)
+ // ... | 32 | char | Font filename (with extension) - VERSION: >=600
+ // ... | 4 | int | Font base size
+ // ... | 4 | int | Font glyph count [glyphCount]
+ // ... | 4 | int | Font type (0-NORMAL, 1-SDF)
+ // ... | 16 | Rectangle | Font white rectangle
+
+ // Custom Font Data : Image (20 bytes + imData)
+ // NOTE: Font image atlas is always converted to GRAY+ALPHA
+ // and atlas image data can be compressed (DEFLATE)
+ // ... | 4 | int | Image data size (uncompressed)
+ // ... | 4 | int | Image data size (compressed)
+ // ... | 4 | int | Image width
+ // ... | 4 | int | Image height
+ // ... | 4 | int | Image format: GRAY+ALPHA (expected)
+ // ... | imSize | byte | Image data (comp or uncomp)
+
+ // Custom Font Data : Recs (32 bytes*glyphCount)
+ // NOTE: Font recs data can be compressed (DEFLATE)
+ // ... | 4 | int | Recs data compressed size (0 - not compressed, 1-compressed) - VERSION: >=400
+ //
+ // if (compRecsSize == 0)
+ // {
+ // NOTE: Uncompressed size can be calculated: (glyphCount*16 byte)
+ // foreach (glyph)
+ // {
+ // ... | 16 | Rectangle | Glyph rectangle (in image)
+ // }
+ // }
+ // else
+ // {
+ // ... | compRecsSize | byte | Rectangles data
+ // }
+ //
+
+ // Custom Font Data : Glyph Info (32 bytes*glyphCount)
+ // NOTE: Font glyphs info data can be compressed (DEFLATE)
+ // ... | 4 | int | Glyphs data compressed size (0 - not compressed) - VERSION: >=400
+ //
+ // if (compGlyphsSize == 0)
+ // {
+ // NOTE: Uncompressed size can be calculated: (glyphCount*16 byte)
+ // foreach (glyph)
+ // {
+ // ... | 4 | int | Glyph value
+ // ... | 4 | int | Glyph offset X
+ // ... | 4 | int | Glyph offset Y
+ // ... | 4 | int | Glyph advance X
+ // }
+ // }
+ // else
+ // {
+ // ... | compGlyphsSize | byte | Glyphs data
+ // }
+ // ------------------------------------------------------
+
+ unsigned char *fileDataPtr = (unsigned char *)fileData;
+
+ char signature[5] = { 0 };
+ short version = 0;
+ short reserved = 0;
+ int propertyCount = 0;
+
+ memcpy(signature, fileDataPtr, 4);
+ memcpy(&version, fileDataPtr + 4, sizeof(short));
+ memcpy(&reserved, fileDataPtr + 4 + 2, sizeof(short));
+ memcpy(&propertyCount, fileDataPtr + 4 + 2 + 2, sizeof(int));
+ fileDataPtr += 12;
+
+ if ((signature[0] == 'r') &&
+ (signature[1] == 'G') &&
+ (signature[2] == 'S') &&
+ (signature[3] == ' '))
+ {
+ short controlId = 0;
+ short propertyId = 0;
+ unsigned int propertyValue = 0;
+
+ for (int i = 0; i < propertyCount; i++)
+ {
+ memcpy(&controlId, fileDataPtr, sizeof(short));
+ memcpy(&propertyId, fileDataPtr + 2, sizeof(short));
+ memcpy(&propertyValue, fileDataPtr + 2 + 2, sizeof(unsigned int));
+ fileDataPtr += 8;
+
+ if (controlId == 0) // DEFAULT control
+ {
+ // If a DEFAULT property is loaded, it is propagated to all controls
+ // NOTE: All DEFAULT properties should be defined first in the file
+ GuiSetStyle(0, (int)propertyId, propertyValue);
+
+ if (propertyId < RAYGUI_MAX_PROPS_BASE) for (int j = 1; j < RAYGUI_MAX_CONTROLS; j++) GuiSetStyle(j, (int)propertyId, propertyValue);
+ }
+ else GuiSetStyle((int)controlId, (int)propertyId, propertyValue);
+ }
+
+ // Load custom font if available
+ // NOTE: Font texture loading requires raylib
+ int fontDataSize = 0;
+ memcpy(&fontDataSize, fileDataPtr, sizeof(int));
+ fileDataPtr += 4;
+
+ if (fontDataSize > 0)
+ {
+ Font font = { 0 };
+ int fontType = 0; // 0-Normal, 1-SDF
+
+ // WARNING: Version 600 adds 32 bytes for the font filename (with extension)
+ if (version >= 600)
+ {
+ // GLOBAL: Copy font file name into guiFontName
+ memcpy(guiFontName, fileDataPtr, 32);
+ fileDataPtr += 32;
+ }
+ else memset(guiFontName, 0, 32);
+
+ memcpy(&font.baseSize, fileDataPtr, sizeof(int));
+ memcpy(&font.glyphCount, fileDataPtr + 4, sizeof(int));
+ memcpy(&fontType, fileDataPtr + 4 + 4, sizeof(int));
+ fileDataPtr += 12;
+
+ // Load font white rectangle
+ Rectangle fontWhiteRec = { 0 };
+ memcpy(&fontWhiteRec, fileDataPtr, sizeof(Rectangle));
+ fileDataPtr += 16;
+
+ // Load font image parameters
+ int fontImageUncompSize = 0;
+ int fontImageCompSize = 0;
+ memcpy(&fontImageUncompSize, fileDataPtr, sizeof(int));
+ memcpy(&fontImageCompSize, fileDataPtr + 4, sizeof(int));
+ fileDataPtr += 8;
+
+ Image imFont = { 0 };
+ imFont.mipmaps = 1;
+ memcpy(&imFont.width, fileDataPtr, sizeof(int));
+ memcpy(&imFont.height, fileDataPtr + 4, sizeof(int));
+ memcpy(&imFont.format, fileDataPtr + 4 + 4, sizeof(int));
+ fileDataPtr += 12;
+
+ if ((fontImageCompSize > 0) && (fontImageCompSize != fontImageUncompSize))
+ {
+ // Compressed font atlas image data (DEFLATE), it requires DecompressData()
+ int dataUncompSize = 0;
+ unsigned char *compData = (unsigned char *)RAYGUI_CALLOC(fontImageCompSize, sizeof(unsigned char));
+ memcpy(compData, fileDataPtr, fontImageCompSize);
+ fileDataPtr += fontImageCompSize;
+
+ imFont.data = DecompressData(compData, fontImageCompSize, &dataUncompSize);
+
+ // Security check, dataUncompSize must match the provided fontImageUncompSize
+ if (dataUncompSize != fontImageUncompSize) RAYGUI_LOG("WARNING: Uncompressed font atlas image data could be corrupted");
+
+ RAYGUI_FREE(compData);
+ }
+ else
+ {
+ // Font atlas image data is not compressed
+ imFont.data = (unsigned char *)RAYGUI_CALLOC(fontImageUncompSize, sizeof(unsigned char));
+ memcpy(imFont.data, fileDataPtr, fontImageUncompSize);
+ fileDataPtr += fontImageUncompSize;
+ }
+
+ // Load font recs data (glyphs position and size in the image atlas)
+ int recsDataSize = font.glyphCount*sizeof(Rectangle);
+ int recsDataCompressedSize = 0;
+
+ // WARNING: Version 400 adds the compression size parameter
+ if (version >= 400)
+ {
+ // RGS files version 400 support compressed recs data
+ memcpy(&recsDataCompressedSize, fileDataPtr, sizeof(int));
+ fileDataPtr += sizeof(int);
+ }
+
+ if ((recsDataCompressedSize > 0) && (recsDataCompressedSize != recsDataSize))
+ {
+ // Recs data is compressed, uncompress it
+ unsigned char *recsDataCompressed = (unsigned char *)RAYGUI_CALLOC(recsDataCompressedSize, sizeof(unsigned char));
+
+ memcpy(recsDataCompressed, fileDataPtr, recsDataCompressedSize);
+ fileDataPtr += recsDataCompressedSize;
+
+ int recsDataUncompSize = 0;
+ font.recs = (Rectangle *)DecompressData(recsDataCompressed, recsDataCompressedSize, &recsDataUncompSize);
+
+ // Security check, data uncompressed size must match the expected original data size
+ if (recsDataUncompSize != recsDataSize) RAYGUI_LOG("WARNING: Uncompressed font recs data could be corrupted");
+
+ RAYGUI_FREE(recsDataCompressed);
+ }
+ else
+ {
+ // Recs data is uncompressed
+ font.recs = (Rectangle *)RAYGUI_CALLOC(font.glyphCount, sizeof(Rectangle));
+ for (int i = 0; i < font.glyphCount; i++)
+ {
+ memcpy(&font.recs[i], fileDataPtr, sizeof(Rectangle));
+ fileDataPtr += sizeof(Rectangle);
+ }
+ }
+
+ // Load font glyphs info data
+ int glyphsDataSize = font.glyphCount*16; // 16 bytes data per glyph
+ int glyphsDataCompressedSize = 0;
+
+ // WARNING: Version 400 adds the compression size parameter
+ if (version >= 400)
+ {
+ // RGS files version 400 support compressed glyphs data
+ memcpy(&glyphsDataCompressedSize, fileDataPtr, sizeof(int));
+ fileDataPtr += sizeof(int);
+ }
+
+ // Allocate required glyphs space to fill with data
+ font.glyphs = (GlyphInfo *)RAYGUI_CALLOC(font.glyphCount, sizeof(GlyphInfo));
+
+ if ((glyphsDataCompressedSize > 0) && (glyphsDataCompressedSize != glyphsDataSize))
+ {
+ // Glyphs data is compressed, uncompress it
+ unsigned char *glypsDataCompressed = (unsigned char *)RAYGUI_CALLOC(glyphsDataCompressedSize, sizeof(unsigned char));
+
+ memcpy(glypsDataCompressed, fileDataPtr, glyphsDataCompressedSize);
+ fileDataPtr += glyphsDataCompressedSize;
+
+ int glyphsDataUncompSize = 0;
+ unsigned char *glyphsDataUncomp = DecompressData(glypsDataCompressed, glyphsDataCompressedSize, &glyphsDataUncompSize);
+
+ // Security check, data uncompressed size must match the expected original data size
+ if (glyphsDataUncompSize != glyphsDataSize) RAYGUI_LOG("WARNING: Uncompressed font glyphs data could be corrupted");
+
+ unsigned char *glyphsDataUncompPtr = glyphsDataUncomp;
+
+ for (int i = 0; i < font.glyphCount; i++)
+ {
+ memcpy(&font.glyphs[i].value, glyphsDataUncompPtr, sizeof(int));
+ memcpy(&font.glyphs[i].offsetX, glyphsDataUncompPtr + 4, sizeof(int));
+ memcpy(&font.glyphs[i].offsetY, glyphsDataUncompPtr + 8, sizeof(int));
+ memcpy(&font.glyphs[i].advanceX, glyphsDataUncompPtr + 12, sizeof(int));
+ glyphsDataUncompPtr += 16;
+ }
+
+ RAYGUI_FREE(glypsDataCompressed);
+ RAYGUI_FREE(glyphsDataUncomp);
+ }
+ else
+ {
+ // Glyphs data is uncompressed
+ for (int i = 0; i < font.glyphCount; i++)
+ {
+ memcpy(&font.glyphs[i].value, fileDataPtr, sizeof(int));
+ memcpy(&font.glyphs[i].offsetX, fileDataPtr + 4, sizeof(int));
+ memcpy(&font.glyphs[i].offsetY, fileDataPtr + 8, sizeof(int));
+ memcpy(&font.glyphs[i].advanceX, fileDataPtr + 12, sizeof(int));
+ fileDataPtr += 16;
+ }
+ }
+
+#if defined(RAYGUI_FONT_ICONS_BAKING)
+ // Font atlas image icons baking
+ Rectangle updatedWhiteRec = { 0 };
+ guiIconFontOffsetY = GuiFontIconBaking(&imFont, font, &updatedWhiteRec);
+ if (guiIconFontOffsetY > 0) fontWhiteRec = updatedWhiteRec;
+#endif
+
+#if !defined(RAYGUI_STANDALONE)
+ // Load texture from image
+ if (font.texture.id != GetFontDefault().texture.id) UnloadTexture(font.texture);
+ font.texture = LoadTextureFromImage(imFont);
+
+ // Fallback to default raylib texture if font texture loading fails
+ if (font.texture.id != 0)
+ {
+ // Set font texture source rectangle to be used as white texture to draw shapes
+ // NOTE: It makes possible to draw shapes and text (full UI) in a single draw call
+ if ((fontWhiteRec.x > 0) &&
+ (fontWhiteRec.y > 0) &&
+ (fontWhiteRec.width > 0) &&
+ (fontWhiteRec.height > 0)) SetShapesTexture(font.texture, fontWhiteRec);
+ }
+ else font = GetFontDefault();
+
+ GuiSetFont(font);
+#endif
+ RAYGUI_FREE(imFont.data);
+ }
+ }
+}
+
+// Load style default over global style
+void GuiLoadStyleDefault(void)
+{
+ // Setting this flag first to avoid cyclic function calls
+ // when calling GuiSetStyle() and GuiGetStyle()
+ guiStyleLoaded = true;
+
+ // Initialize default LIGHT style property values
+ // WARNING: Default value are applied to all controls on set but
+ // they can be overwritten later on for every custom control
+ GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, 0x838383ff);
+ GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, 0xc9c9c9ff);
+ GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, 0x686868ff);
+ GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, 0x5bb2d9ff);
+ GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, 0xc9effeff);
+ GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, 0x6c9bbcff);
+ GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, 0x0492c7ff);
+ GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, 0x97e8ffff);
+ GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, 0x368bafff);
+ GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, 0xb5c1c2ff);
+ GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, 0xe6e9e9ff);
+ GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, 0xaeb7b8ff);
+ GuiSetStyle(DEFAULT, BORDER_WIDTH, 1);
+ GuiSetStyle(DEFAULT, TEXT_PADDING, 0);
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+
+ // Initialize default extended property values
+ // NOTE: By default, extended property values are initialized to 0
+ GuiSetStyle(DEFAULT, TEXT_SIZE, 10); // DEFAULT, shared by all controls
+ GuiSetStyle(DEFAULT, TEXT_SPACING, 1); // DEFAULT, shared by all controls
+ GuiSetStyle(DEFAULT, LINE_COLOR, 0x90abb5ff); // DEFAULT specific property
+ GuiSetStyle(DEFAULT, BACKGROUND_COLOR, 0xf5f5f5ff); // DEFAULT specific property
+ GuiSetStyle(DEFAULT, TEXT_LINE_SPACING, 12); // DEFAULT, pixels between lines, from bottom of first line to top of second
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_MIDDLE); // DEFAULT, text aligned vertically to middle of text-bounds
+
+ // Initialize control-specific property values
+ // NOTE: Those properties are in default list but require specific values by control type
+ GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+ GuiSetStyle(BUTTON, BORDER_WIDTH, 2);
+ GuiSetStyle(SLIDER, TEXT_PADDING, 4);
+ GuiSetStyle(PROGRESSBAR, TEXT_PADDING, 4);
+ GuiSetStyle(CHECKBOX, TEXT_PADDING, 4);
+ GuiSetStyle(CHECKBOX, TEXT_ALIGNMENT, TEXT_ALIGN_RIGHT);
+ GuiSetStyle(DROPDOWNBOX, TEXT_PADDING, 0);
+ GuiSetStyle(DROPDOWNBOX, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ GuiSetStyle(TEXTBOX, TEXT_PADDING, 4);
+ GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+ GuiSetStyle(VALUEBOX, TEXT_PADDING, 0);
+ GuiSetStyle(VALUEBOX, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+ GuiSetStyle(STATUSBAR, TEXT_PADDING, 8);
+ GuiSetStyle(STATUSBAR, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+ GuiSetStyle(TABBAR, TAB_ITEMS_WIDTH, 160);
+
+ // Initialize extended property values
+ // NOTE: By default, extended property values are initialized to 0
+ GuiSetStyle(TOGGLE, GROUP_PADDING, 2);
+ GuiSetStyle(SLIDER, SLIDER_WIDTH, 16);
+ GuiSetStyle(SLIDER, SLIDER_PADDING, 1);
+ GuiSetStyle(PROGRESSBAR, PROGRESS_PADDING, 1);
+ GuiSetStyle(CHECKBOX, CHECK_PADDING, 1);
+ GuiSetStyle(COMBOBOX, COMBO_BUTTON_WIDTH, 32);
+ GuiSetStyle(COMBOBOX, COMBO_BUTTON_SPACING, 2);
+ GuiSetStyle(DROPDOWNBOX, ARROW_PADDING, 16);
+ GuiSetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING, 2);
+ GuiSetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH, 24);
+ GuiSetStyle(VALUEBOX, SPINNER_BUTTON_SPACING, 2);
+ GuiSetStyle(SCROLLBAR, BORDER_WIDTH, 0);
+ GuiSetStyle(SCROLLBAR, ARROWS_VISIBLE, 0);
+ GuiSetStyle(SCROLLBAR, ARROWS_SIZE, 6);
+ GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING, 0);
+ GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, 16);
+ GuiSetStyle(SCROLLBAR, SCROLL_PADDING, 0);
+ GuiSetStyle(SCROLLBAR, SCROLL_SPEED, 12);
+ GuiSetStyle(LISTVIEW, LIST_ITEMS_HEIGHT, 28);
+ GuiSetStyle(LISTVIEW, LIST_ITEMS_SPACING, 2);
+ GuiSetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH, 1);
+ GuiSetStyle(LISTVIEW, SCROLLBAR_WIDTH, 12);
+ GuiSetStyle(LISTVIEW, SCROLLBAR_SIDE, SCROLLBAR_RIGHT_SIDE);
+ GuiSetStyle(COLORPICKER, COLOR_SELECTOR_SIZE, 8);
+ GuiSetStyle(COLORPICKER, HUEBAR_WIDTH, 16);
+ GuiSetStyle(COLORPICKER, HUEBAR_PADDING, 8);
+ GuiSetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT, 8);
+ GuiSetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW, 2);
+
+ if (guiFont.texture.id != GetFontDefault().texture.id)
+ {
+ // Unload previous font texture
+ UnloadTexture(guiFont.texture);
+ RAYGUI_FREE(guiFont.recs);
+ RAYGUI_FREE(guiFont.glyphs);
+ guiFont.recs = NULL;
+ guiFont.glyphs = NULL;
+
+ // Setup default raylib font
+ guiFont = GetFontDefault();
+
+ // NOTE: Default raylib font character 95 is a white square
+ Rectangle whiteChar = guiFont.recs[95];
+
+ // NOTE: Setting up a 1px padding on char rectangle to avoid pixel bleeding on MSAA filtering
+ SetShapesTexture(guiFont.texture, RAYGUI_CLITERAL(Rectangle){ whiteChar.x + 1, whiteChar.y + 1, whiteChar.width - 2, whiteChar.height - 2 });
+
+ // Reset baked icons offset in font
+ guiIconFontOffsetY = 0;
+ }
+}
+
+// Get text with icon id prepended
+// NOTE: Useful to add icons by name id (enum) instead of
+// a number that can change between ricon versions
+const char *GuiIconText(int iconId, const char *text)
+{
+#if defined(RAYGUI_NO_ICONS)
+ return NULL;
+#else
+ static char buffer[1024] = { 0 };
+ static char iconBuffer[16] = { 0 };
+
+ if (text != NULL)
+ {
+ memset(buffer, 0, 1024);
+ snprintf(buffer, 1024, "#%03i#", iconId);
+
+ for (int i = 5; i < 1024; i++)
+ {
+ buffer[i] = text[i - 5];
+ if (text[i - 5] == '\0') break;
+ }
+
+ return buffer;
+ }
+ else
+ {
+ snprintf(iconBuffer, 16, "#%03i#", iconId);
+
+ return iconBuffer;
+ }
+#endif
+}
+
+#if !defined(RAYGUI_NO_ICONS)
+// Get full icons data pointer
+unsigned int *GuiGetIcons(void) { return guiIconsPtr; }
+
+// Load raygui icons file (.rgi)
+// WARNING: guiIconsName[]][] memory should be manually freed!
+char **GuiLoadIcons(const char *fileName, bool loadIconsName)
+{
+ FILE *rgiFile = fopen(fileName, "rb");
+ char **guiIconsName = NULL;
+
+ if (rgiFile != NULL)
+ {
+ unsigned char *fileData = NULL;
+ int dataSize = 0;
+
+ fseek(rgiFile, 0, SEEK_END);
+ int size = (int)ftell(rgiFile);
+ fseek(rgiFile, 0, SEEK_SET);
+
+ if (size > 0)
+ {
+ fileData = (unsigned char *)RL_CALLOC(size, sizeof(unsigned char));
+ // WARNING: File can be partially loaded but ignoring it for simplicity
+ dataSize = (int)fread(fileData, sizeof(unsigned char), size, rgiFile);
+
+ guiIconsName = GuiLoadIconsFromMemory(fileData, dataSize, loadIconsName);
+ }
+
+ fclose(rgiFile);
+ }
+
+ return guiIconsName;
+}
+
+// Load icons from memory
+// GLOBAL: Updates global variable: guiIconsPtr
+char **GuiLoadIconsFromMemory(const unsigned char *fileData, int dataSize, bool loadIconsName)
+{
+ // Icon File Structure (.rgi)
+ // ------------------------------------------------------
+ // Offset | Size | Type | Description
+ // ------------------------------------------------------
+ // 0 | 4 | char | Signature: "rGI "
+ // 4 | 2 | short | Version: 100, 500
+ // 6 | 2 | short | reserved
+
+ // 8 | 2 | short | Num icons (N)
+ // 10 | 2 | short | Icons Size (Options: 16, 32, 64)
+
+ // Icons name id (32 bytes per name id)
+ // foreach (icon)
+ // {
+ // 12+32*i | 32 | char | Icon NameId
+ // }
+
+ // Icons data: One bit per pixel, stored as unsigned int array (depends on icon size)
+ // Size*Size pixels/32bit per unsigned int = K unsigned int per icon
+ // foreach (icon)
+ // {
+ // ... | K | unsigned int | Icon Data
+ // }
+ // ------------------------------------------------------
+
+ unsigned char *fileDataPtr = (unsigned char *)fileData;
+ char **guiIconsName = NULL;
+
+ char signature[5] = { 0 };
+ short version = 0;
+ short reserved = 0;
+ short iconCount = 0;
+ short iconSize = 0;
+
+ memcpy(signature, fileDataPtr, 4);
+ memcpy(&version, fileDataPtr + 4, sizeof(short));
+ memcpy(&reserved, fileDataPtr + 4 + 2, sizeof(short));
+ memcpy(&iconCount, fileDataPtr + 4 + 2 + 2, sizeof(short));
+ memcpy(&iconSize, fileDataPtr + 4 + 2 + 2 + 2, sizeof(short));
+ fileDataPtr += 12;
+
+ if ((signature[0] == 'r') &&
+ (signature[1] == 'G') &&
+ (signature[2] == 'I') &&
+ (signature[3] == ' '))
+ {
+ if (loadIconsName)
+ {
+ // NOTE: Always allocating RAYGUI_ICON_MAX_ICONS names slots
+ guiIconsName = (char **)RAYGUI_CALLOC(RAYGUI_ICON_MAX_ICONS, sizeof(char *));
+ for (int i = 0; i < iconCount; i++)
+ {
+ guiIconsName[i] = (char *)RAYGUI_CALLOC(RAYGUI_ICON_MAX_NAME_LENGTH, sizeof(char));
+ memcpy(guiIconsName[i], fileDataPtr, RAYGUI_ICON_MAX_NAME_LENGTH);
+ fileDataPtr += RAYGUI_ICON_MAX_NAME_LENGTH;
+ }
+ }
+ else
+ {
+ // Skip icon name data if not required
+ fileDataPtr += iconCount*RAYGUI_ICON_MAX_NAME_LENGTH;
+ }
+
+ int iconDataSize = iconCount*((int)iconSize*(int)iconSize/32)*(int)sizeof(unsigned int);
+ guiIconsPtr = (unsigned int *)RAYGUI_CALLOC(iconDataSize, 1);
+
+ memcpy(guiIconsPtr, fileDataPtr, iconDataSize);
+ }
+
+ return guiIconsName;
+}
+
+// Draw selected icon using rectangles pixel-by-pixel
+void GuiDrawIcon(int iconId, int posX, int posY, int pixelSize, Color color)
+{
+ if ((guiIconFontOffsetY > 0) && (iconId < RAYGUI_ICON_MAX_FONT_BACKED))
+ {
+ int maxIconsPerLine = guiFont.texture.width/(RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING);
+ int x = iconId%maxIconsPerLine;
+ int y = iconId/maxIconsPerLine;
+
+ Rectangle srcRec = { (float)x*(RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING) + RAYGUI_ICON_FONT_ATLAS_PADDING,
+ guiIconFontOffsetY + (float)y*(RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING) + RAYGUI_ICON_FONT_ATLAS_PADDING,
+ RAYGUI_ICON_SIZE, RAYGUI_ICON_SIZE };
+ Rectangle dstRec = { (float)posX, (float)posY, (float)pixelSize*RAYGUI_ICON_SIZE, (float)pixelSize*RAYGUI_ICON_SIZE };
+
+ DrawTexturePro(guiFont.texture, srcRec, dstRec, (Vector2){ 0, 0 }, 0.0f, color);
+ }
+ else
+ {
+ #define BIT_CHECK(a,b) ((a) & (1u<<(b)))
+
+ for (int i = 0, y = 0; i < RAYGUI_ICON_SIZE*RAYGUI_ICON_SIZE/32; i++)
+ {
+ for (int k = 0; k < 32; k++)
+ {
+ if (BIT_CHECK(guiIconsPtr[iconId*RAYGUI_ICON_DATA_ELEMENTS + i], k))
+ {
+ GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ (float)posX + (k%RAYGUI_ICON_SIZE)*pixelSize,
+ (float)posY + y*pixelSize, (float)pixelSize, (float)pixelSize }, 0, BLANK, color);
+ }
+
+ if ((k == 15) || (k == 31)) y++;
+ }
+ }
+ }
+}
+
+// Set icon drawing size
+void GuiSetIconScale(int scale)
+{
+ if (scale >= 1) guiIconScale = scale;
+}
+
+// Get text width considering gui style and icon size (if required).
+// For multi-line text (containing '\n'), returns the width of the widest line.
+int GuiGetTextWidth(const char *text)
+{
+ if (text == NULL) return 0;
+
+ int maxWidth = 0;
+ const char *linePtr = text;
+
+ while ((linePtr[0] != '\0') && ((linePtr - text) < MAX_LINE_BUFFER_SIZE))
+ {
+ int lineWidth = GetLineWidth(linePtr);
+ if (lineWidth > maxWidth) maxWidth = lineWidth;
+
+ // Skip to the next '\n' (or end of string/buffer)
+ while ((linePtr[0] != '\0') && (linePtr[0] != '\n') && ((linePtr - text) < MAX_LINE_BUFFER_SIZE))
+ {
+ linePtr++;
+ }
+ // Advance past the '\n' delimiter to the start of the next line
+ if (linePtr[0] == '\n') linePtr++;
+ }
+
+ return maxWidth;
+}
+
+#endif // !RAYGUI_NO_ICONS
+
+//----------------------------------------------------------------------------------
+// Module Internal Functions Definition
+//----------------------------------------------------------------------------------
+// Get text line width (stops at '\n' or '\0')
+// NOTE: Considers icon marker '#NNN#'
+static int GetLineWidth(const char *text)
+{
+#if !defined(RAYGUI_ICON_TEXT_PADDING)
+ #define RAYGUI_ICON_TEXT_PADDING 4
+#endif
+
+ Vector2 textSize = { 0 };
+ int textIconOffset = 0;
+
+ if ((text != NULL) && (text[0] != '\0'))
+ {
+ // Icon marker: '#' + 1..3 digits + '#' (matches GetTextIcon())
+ if (text[0] == '#')
+ {
+ int pos = 1;
+ while ((pos < 4) && (text[pos] >= '0') && (text[pos] <= '9')) pos++;
+ if (text[pos] == '#') textIconOffset = pos + 1;
+ }
+
+ text += textIconOffset;
+
+ // Make sure guiFont is set, GuiGetStyle() initializes it lazynessly
+ float fontSize = (float)GuiGetStyle(DEFAULT, TEXT_SIZE);
+
+ // Custom MeasureText() implementation -- single line only
+ if ((guiFont.texture.id > 0) && (text != NULL))
+ {
+ // Get size in bytes of the line, considering end of line and line break
+ int size = 0;
+ for (int i = 0; i < MAX_LINE_BUFFER_SIZE; i++)
+ {
+ if ((text[i] != '\0') && (text[i] != '\n')) size++;
+ else break;
+ }
+
+ float scaleFactor = fontSize/(float)guiFont.baseSize;
+ textSize.y = (float)guiFont.baseSize*scaleFactor;
+ float glyphWidth = 0.0f;
+
+ for (int i = 0, codepointSize = 0; i < size; i += codepointSize)
+ {
+ int codepoint = GetCodepointNext(&text[i], &codepointSize);
+ int codepointIndex = GetGlyphIndex(guiFont, codepoint);
+
+ if (guiFont.glyphs[codepointIndex].advanceX == 0) glyphWidth = ((float)guiFont.recs[codepointIndex].width*scaleFactor);
+ else glyphWidth = ((float)guiFont.glyphs[codepointIndex].advanceX*scaleFactor);
+
+ textSize.x += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
+ }
+ }
+
+ if (textIconOffset > 0) textSize.x += (RAYGUI_ICON_SIZE + RAYGUI_ICON_TEXT_PADDING);
+ }
+
+ return (int)textSize.x;
+}
+
+// Get text bounds considering control bounds
+static Rectangle GetTextBounds(int control, Rectangle bounds)
+{
+ Rectangle textBounds = bounds;
+
+ textBounds.x = bounds.x + GuiGetStyle(control, BORDER_WIDTH);
+ textBounds.y = bounds.y + GuiGetStyle(control, BORDER_WIDTH) + GuiGetStyle(control, TEXT_PADDING);
+ textBounds.width = bounds.width - 2*GuiGetStyle(control, BORDER_WIDTH) - 2*GuiGetStyle(control, TEXT_PADDING);
+ textBounds.height = bounds.height - 2*GuiGetStyle(control, BORDER_WIDTH) - 2*GuiGetStyle(control, TEXT_PADDING); // NOTE: Text is processed line per line!
+
+ // Depending on control, TEXT_PADDING and TEXT_ALIGNMENT properties could affect the text-bounds
+ switch (control)
+ {
+ case COMBOBOX:
+ case DROPDOWNBOX:
+ case LISTVIEW:
+ // TODO: Special cases (no label): COMBOBOX, DROPDOWNBOX, LISTVIEW
+ case SLIDER:
+ case CHECKBOX:
+ case VALUEBOX:
+ case TABBAR:
+ // TODO: More special cases (label on side): SLIDER, CHECKBOX, VALUEBOX, SPINNER
+ default:
+ {
+ // TODO: WARNING: TEXT_ALIGNMENT is already considered in GuiDrawText()
+ if (GuiGetStyle(control, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT) textBounds.x -= GuiGetStyle(control, TEXT_PADDING);
+ else textBounds.x += GuiGetStyle(control, TEXT_PADDING);
+ }
+ break;
+ }
+
+ return textBounds;
+}
+
+// Get text icon if provided and move text cursor
+// NOTE: Up to RAYGUI_ICON_MAX_ICONS supported for iconId
+static const char *GetTextIcon(const char *text, int *iconId)
+{
+#if !defined(RAYGUI_NO_ICONS)
+ *iconId = -1;
+ if (text[0] == '#') // Maybe an icon, if it starts with # but an ending # must be found
+ {
+ char iconValue[4] = { 0 }; // Maximum length for icon value: 3 digits + '\0'
+
+ int pos = 1;
+ while ((pos < 4) && (text[pos] >= '0') && (text[pos] <= '9'))
+ {
+ iconValue[pos - 1] = text[pos];
+ pos++;
+ }
+
+ if (text[pos] == '#')
+ {
+ int rawIconId = TextToInteger(iconValue);
+ if (rawIconId < RAYGUI_ICON_MAX_ICONS)
+ {
+ *iconId = rawIconId;
+
+ // Move text pointer after icon
+ // WARNING: If only icon provided, it could point to EOL character: '\0'
+ if (*iconId >= 0) text += (pos + 1);
+ }
+ }
+ }
+#endif
+
+ return text;
+}
+
+// Get text divided into lines (by line-breaks '\n')
+// WARNING: It returns pointers to new lines but it does not add NULL ('\0') terminator!
+static const char **GetTextLines(const char *text, int *count)
+{
+ #define RAYGUI_MAX_TEXT_LINES 128
+
+ static const char *lines[RAYGUI_MAX_TEXT_LINES] = { 0 };
+ for (int i = 0; i < RAYGUI_MAX_TEXT_LINES; i++) lines[i] = NULL; // Init NULL pointers to substrings
+
+ int textLength = (int)strlen(text);
+
+ lines[0] = text;
+ *count = 1;
+
+ for (int i = 0; (i < textLength) && (*count < RAYGUI_MAX_TEXT_LINES); i++)
+ {
+ if ((text[i] == '\n') && ((i + 1) < textLength))
+ {
+ lines[*count] = &text[i + 1];
+ *count += 1;
+ }
+ }
+
+ return lines;
+}
+
+// Get text width to next space for provided string
+static float GetNextSpaceWidth(const char *text, int *nextSpaceIndex)
+{
+ float width = 0;
+ int codepointByteCount = 0;
+ int codepoint = 0;
+ int index = 0;
+ float glyphWidth = 0;
+ float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/guiFont.baseSize;
+
+ for (int i = 0; text[i] != '\0'; i++)
+ {
+ if (text[i] != ' ')
+ {
+ codepoint = GetCodepoint(&text[i], &codepointByteCount);
+ index = GetGlyphIndex(guiFont, codepoint);
+ glyphWidth = (guiFont.glyphs[index].advanceX == 0)? guiFont.recs[index].width*scaleFactor : guiFont.glyphs[index].advanceX*scaleFactor;
+ width += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
+ }
+ else
+ {
+ *nextSpaceIndex = i;
+ break;
+ }
+ }
+
+ return width;
+}
+
+// Gui draw text using default font
+static void GuiDrawText(const char *text, Rectangle textBounds, int alignment, Color tint)
+{
+ #define TEXT_VALIGN_PIXEL_OFFSET(h) ((int)h%2) // Vertical alignment for pixel perfect
+
+ #if !defined(RAYGUI_ICON_TEXT_PADDING)
+ #define RAYGUI_ICON_TEXT_PADDING 4
+ #endif
+
+ if ((text == NULL) || (text[0] == '\0')) return; // Security check
+
+ // PROCEDURE:
+ // - Text is processed line per line
+ // - For every line, horizontal alignment is defined
+ // - For all text, vertical alignment is defined (multiline text only)
+ // - For every line, wordwrap mode is checked (useful for GuitextBox(), read-only)
+
+ // Get text lines (using '\n' as delimiter) to be processed individually
+ // WARNING: GuiTextSplit() function can't be used now because it can have already been used
+ // before the GuiDrawText() call and its buffer is static, it would be overriden :(
+ int lineCount = 0;
+ const char **lines = GetTextLines(text, &lineCount);
+
+ // Text style variables
+ //int alignment = GuiGetStyle(DEFAULT, TEXT_ALIGNMENT);
+ int alignmentVertical = GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL);
+ int wrapMode = GuiGetStyle(DEFAULT, TEXT_WRAP_MODE); // Wrap-mode only available in read-only mode, no for text editing
+
+ // TODO: WARNING: This totalHeight is not valid for vertical alignment in case of word-wrap
+ float totalHeight = (float)(lineCount*GuiGetStyle(DEFAULT, TEXT_SIZE) + (lineCount - 1)*GuiGetStyle(DEFAULT, TEXT_LINE_SPACING));
+ float posOffsetY = 0.0f;
+
+ for (int i = 0; i < lineCount; i++)
+ {
+ int iconId = 0;
+ lines[i] = GetTextIcon(lines[i], &iconId); // Check text for icon and move cursor
+
+ // Get text position depending on alignment and iconId
+ //---------------------------------------------------------------------------------
+ Vector2 textBoundsPosition = { textBounds.x, textBounds.y };
+ float textBoundsWidthOffset = 0.0f;
+
+ // NOTE: Icon was already stripped above by GetTextIcon(); GetLineWidth()
+ // takes no icon path here and returns only the glyph width of this line.
+ int textSizeX = GetLineWidth(lines[i]);
+
+ // If text requires an icon, add size to measure
+ if (iconId >= 0)
+ {
+ textSizeX += RAYGUI_ICON_SIZE*guiIconScale;
+
+ // WARNING: If only icon provided, text could be pointing to EOF character: '\0'
+#if !defined(RAYGUI_NO_ICONS)
+ if ((lines[i] != NULL) && (lines[i][0] != '\0')) textSizeX += RAYGUI_ICON_TEXT_PADDING;
+#endif
+ }
+
+ // Check guiTextAlign global variables
+ switch (alignment)
+ {
+ case TEXT_ALIGN_LEFT: textBoundsPosition.x = textBounds.x; break;
+ case TEXT_ALIGN_CENTER: textBoundsPosition.x = textBounds.x + textBounds.width/2 - textSizeX/2; break;
+ case TEXT_ALIGN_RIGHT: textBoundsPosition.x = textBounds.x + textBounds.width - textSizeX; break;
+ default: break;
+ }
+
+ if (textSizeX > textBounds.width && (lines[i] != NULL) && (lines[i][0] != '\0')) textBoundsPosition.x = textBounds.x;
+
+ switch (alignmentVertical)
+ {
+ // Only valid in case of wordWrap = 0;
+ case TEXT_ALIGN_TOP: textBoundsPosition.y = textBounds.y + posOffsetY; break;
+ case TEXT_ALIGN_MIDDLE: textBoundsPosition.y = textBounds.y + posOffsetY + textBounds.height/2 - totalHeight/2 + TEXT_VALIGN_PIXEL_OFFSET(textBounds.height); break;
+ case TEXT_ALIGN_BOTTOM: textBoundsPosition.y = textBounds.y + posOffsetY + textBounds.height - totalHeight + TEXT_VALIGN_PIXEL_OFFSET(textBounds.height); break;
+ default: break;
+ }
+
+ // NOTE: Make sure getting pixel-perfect coordinates,
+ // In case of decimals, it could result in text positioning artifacts
+ textBoundsPosition.x = (float)((int)textBoundsPosition.x);
+ textBoundsPosition.y = (float)((int)textBoundsPosition.y);
+ //---------------------------------------------------------------------------------
+
+ // Draw text (with icon if available)
+ //---------------------------------------------------------------------------------
+#if !defined(RAYGUI_NO_ICONS)
+ if (iconId >= 0)
+ {
+ // NOTE: Considering icon height, probably different than text size
+ GuiDrawIcon(iconId, (int)textBoundsPosition.x, (int)(textBounds.y + textBounds.height/2 - RAYGUI_ICON_SIZE*guiIconScale/2 + TEXT_VALIGN_PIXEL_OFFSET(textBounds.height)), guiIconScale, tint);
+ textBoundsPosition.x += (float)(RAYGUI_ICON_SIZE*guiIconScale + RAYGUI_ICON_TEXT_PADDING);
+ textBoundsWidthOffset = (float)(RAYGUI_ICON_SIZE*guiIconScale + RAYGUI_ICON_TEXT_PADDING);
+ }
+#endif
+ // Get size in bytes of text,
+ // considering end of line and line break
+ int lineSize = 0;
+ for (int c = 0; (lines[i][c] != '\0') && (lines[i][c] != '\n') && (lines[i][c] != '\r'); c++, lineSize++){ }
+ float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/guiFont.baseSize;
+
+ int lastSpaceIndex = 0;
+ bool tempWrapCharMode = false;
+
+ int textOffsetY = 0;
+ float textOffsetX = 0.0f;
+ float glyphWidth = 0;
+
+ int ellipsisWidth = GuiGetTextWidth("...");
+ bool textOverflow = false;
+ for (int c = 0, codepointSize = 0; c < lineSize; c += codepointSize)
+ {
+ int codepoint = GetCodepointNext(&lines[i][c], &codepointSize);
+ int index = GetGlyphIndex(guiFont, codepoint);
+
+ // NOTE: Normally, exiting the decoding sequence as soon as a bad byte is found (and return 0x3f)
+ // but all of the bad bytes need to be drawn using the '?' symbol, moving one byte
+ if (codepoint == 0x3f) codepointSize = 1; // TODO: Review not recognized codepoints size
+
+ // Get glyph width to check if it goes out of bounds
+ if (guiFont.glyphs[index].advanceX == 0) glyphWidth = ((float)guiFont.recs[index].width*scaleFactor);
+ else glyphWidth = (float)guiFont.glyphs[index].advanceX*scaleFactor;
+
+ // Wrap mode text measuring, to validate if
+ // it can be drawn or a new line is required
+ if (wrapMode == TEXT_WRAP_CHAR)
+ {
+ // Jump to next line if current character reach end of the box limits
+ if ((textOffsetX + glyphWidth) > textBounds.width - textBoundsWidthOffset)
+ {
+ textOffsetX = 0.0f;
+ textOffsetY += (GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING));
+
+ if (tempWrapCharMode) // Wrap at char level when too long words
+ {
+ wrapMode = TEXT_WRAP_WORD;
+ tempWrapCharMode = false;
+ }
+ }
+ }
+ else if (wrapMode == TEXT_WRAP_WORD)
+ {
+ if (codepoint == 32) lastSpaceIndex = c;
+
+ // Get width to next space in line
+ int nextSpaceIndex = 0;
+ float nextSpaceWidth = GetNextSpaceWidth(lines[i] + c, &nextSpaceIndex);
+
+ int nextSpaceIndex2 = 0;
+ float nextWordSize = GetNextSpaceWidth(lines[i] + lastSpaceIndex + 1, &nextSpaceIndex2);
+
+ if (nextWordSize > textBounds.width - textBoundsWidthOffset)
+ {
+ // Considering the case the next word is longer than bounds
+ tempWrapCharMode = true;
+ wrapMode = TEXT_WRAP_CHAR;
+ }
+ else if ((textOffsetX + nextSpaceWidth) > textBounds.width - textBoundsWidthOffset)
+ {
+ textOffsetX = 0.0f;
+ textOffsetY += (GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING));
+ }
+ }
+
+ if (codepoint == '\n') break; // WARNING: Lines are already processed manually, no need to keep drawing after this codepoint
+ else
+ {
+ // TODO: There are multiple types of spaces in Unicode,
+ // maybe it's a good idea to add support for more: http://jkorpela.fi/chars/spaces.html
+ if ((codepoint != ' ') && (codepoint != '\t')) // Do not draw codepoints with no glyph
+ {
+ if (wrapMode == TEXT_WRAP_NONE)
+ {
+ // Draw only required text glyphs fitting the textBounds.width
+ if (textSizeX > textBounds.width)
+ {
+ if (textOffsetX <= (textBounds.width - glyphWidth - textBoundsWidthOffset - ellipsisWidth))
+ {
+ DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha));
+ }
+ else if (!textOverflow)
+ {
+ textOverflow = true;
+
+ for (int j = 0; j < ellipsisWidth; j += ellipsisWidth/3)
+ {
+ DrawTextCodepoint(guiFont, '.', RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX + j, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha));
+ }
+ }
+ }
+ else
+ {
+ DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha));
+ }
+ }
+ else if ((wrapMode == TEXT_WRAP_CHAR) || (wrapMode == TEXT_WRAP_WORD))
+ {
+ // Draw only glyphs inside the bounds
+ if ((textBoundsPosition.y + textOffsetY) <= (textBounds.y + textBounds.height - GuiGetStyle(DEFAULT, TEXT_SIZE)))
+ {
+ DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha));
+ }
+ }
+ }
+
+ if (guiFont.glyphs[index].advanceX == 0) textOffsetX += ((float)guiFont.recs[index].width*scaleFactor + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
+ else textOffsetX += ((float)guiFont.glyphs[index].advanceX*scaleFactor + (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
+ }
+ }
+
+ if (wrapMode == TEXT_WRAP_NONE) posOffsetY += (float)(GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING));
+ else if ((wrapMode == TEXT_WRAP_CHAR) || (wrapMode == TEXT_WRAP_WORD))
+ posOffsetY += (textOffsetY + GuiGetStyle(DEFAULT, TEXT_SIZE));
+ //---------------------------------------------------------------------------------
+ }
+
+#if defined(RAYGUI_DEBUG_TEXT_BOUNDS)
+ GuiDrawRectangle(textBounds, 0, WHITE, Fade(BLUE, 0.4f));
+#endif
+}
+
+// Gui draw rectangle using default raygui plain style with borders
+static void GuiDrawRectangle(Rectangle rec, int borderWidth, Color borderColor, Color color)
+{
+ if (color.a > 0)
+ {
+ // Draw rectangle filled with color
+ DrawRectangle((int)rec.x, (int)rec.y, (int)rec.width, (int)rec.height, GuiFade(color, guiAlpha));
+ }
+
+ if (borderWidth > 0)
+ {
+ // Draw rectangle border lines with color
+ DrawRectangle((int)rec.x, (int)rec.y, (int)rec.width, borderWidth, GuiFade(borderColor, guiAlpha));
+ DrawRectangle((int)rec.x, (int)rec.y + borderWidth, borderWidth, (int)rec.height - 2*borderWidth, GuiFade(borderColor, guiAlpha));
+ DrawRectangle((int)rec.x + (int)rec.width - borderWidth, (int)rec.y + borderWidth, borderWidth, (int)rec.height - 2*borderWidth, GuiFade(borderColor, guiAlpha));
+ DrawRectangle((int)rec.x, (int)rec.y + (int)rec.height - borderWidth, (int)rec.width, borderWidth, GuiFade(borderColor, guiAlpha));
+ }
+
+#if defined(RAYGUI_DEBUG_RECS_BOUNDS)
+ DrawRectangle((int)rec.x, (int)rec.y, (int)rec.width, (int)rec.height, Fade(RED, 0.4f));
+#endif
+}
+
+// Draw tooltip using control bounds
+static void GuiTooltip(Rectangle controlRec)
+{
+ if (!guiLocked && guiTooltip && (guiTooltipPtr != NULL) && !guiControlExclusiveMode)
+ {
+ Vector2 textSize = MeasureTextEx(guiFont, guiTooltipPtr, (float)GuiGetStyle(DEFAULT, TEXT_SIZE),
+ (float)GuiGetStyle(DEFAULT, TEXT_SPACING));
+
+ if ((controlRec.x + textSize.x + 16) > GetScreenWidth()) controlRec.x -= (textSize.x + 16 - controlRec.width);
+
+ int lineCount = 0;
+ GetTextLines(guiTooltipPtr, &lineCount); // Only using the line count
+ if ((controlRec.y + controlRec.height + textSize.y + 4 + 8*lineCount) > GetScreenHeight())
+ controlRec.y -= (controlRec.height + textSize.y + 4 + 8*lineCount);
+
+ // TODO: Probably TEXT_LINE_SPACING should be considered on panel size instead of hardcoding 8.0f
+ GuiPanel(RAYGUI_CLITERAL(Rectangle){ controlRec.x, controlRec.y + controlRec.height + 4, textSize.x + 16, textSize.y + 8.0f*lineCount }, NULL);
+
+ int textPadding = GuiGetStyle(LABEL, TEXT_PADDING);
+ int textAlignment = GuiGetStyle(LABEL, TEXT_ALIGNMENT);
+ GuiSetStyle(LABEL, TEXT_PADDING, 0);
+ GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ GuiLabel(RAYGUI_CLITERAL(Rectangle){ controlRec.x, controlRec.y + controlRec.height + 4, textSize.x + 16, textSize.y + 8.0f*lineCount }, guiTooltipPtr);
+ GuiSetStyle(LABEL, TEXT_ALIGNMENT, textAlignment);
+ GuiSetStyle(LABEL, TEXT_PADDING, textPadding);
+ }
+}
+
+// Scroll bar control (used by GuiScrollPanel())
+static int GuiScrollBar(Rectangle bounds, int value, int minValue, int maxValue)
+{
+ GuiState state = guiState;
+
+ // Is the scrollbar horizontal or vertical?
+ bool isVertical = (bounds.width > bounds.height)? false : true;
+
+ // The size (width or height depending on scrollbar type) of the spinner buttons
+ const int spinnerSize = GuiGetStyle(SCROLLBAR, ARROWS_VISIBLE)?
+ (isVertical? (int)bounds.width - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH) :
+ (int)bounds.height - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH)) : 0;
+
+ // Arrow buttons [<] [>] [∧] [∨]
+ Rectangle arrowUpLeft = { 0 };
+ Rectangle arrowDownRight = { 0 };
+
+ // Actual area of the scrollbar excluding the arrow buttons
+ Rectangle scrollbar = { 0 };
+
+ // Slider bar that moves --[///]-----
+ Rectangle slider = { 0 };
+
+ // Normalize value
+ if (value > maxValue) value = maxValue;
+ if (value < minValue) value = minValue;
+
+ int valueRange = maxValue - minValue;
+ if (valueRange <= 0) valueRange = 1;
+
+ int sliderSize = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE);
+ if (sliderSize < 1) sliderSize = 1; // TODO: Consider a minimum slider size
+
+ // Calculate rectangles for all of the components
+ arrowUpLeft = RAYGUI_CLITERAL(Rectangle){
+ (float)bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH),
+ (float)bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH),
+ (float)spinnerSize, (float)spinnerSize };
+
+ if (isVertical)
+ {
+ arrowDownRight = RAYGUI_CLITERAL(Rectangle){ (float)bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)bounds.y + bounds.height - spinnerSize - GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)spinnerSize, (float)spinnerSize };
+ scrollbar = RAYGUI_CLITERAL(Rectangle){ bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING), arrowUpLeft.y + arrowUpLeft.height, bounds.width - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING)), bounds.height - arrowUpLeft.height - arrowDownRight.height - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH) };
+
+ // Make sure the slider won't get outside of the scrollbar
+ sliderSize = (sliderSize >= scrollbar.height)? ((int)scrollbar.height - 2) : sliderSize;
+ slider = RAYGUI_CLITERAL(Rectangle){
+ bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING),
+ scrollbar.y + (int)(((float)(value - minValue)/valueRange)*(scrollbar.height - sliderSize)),
+ bounds.width - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING)),
+ (float)sliderSize };
+ }
+ else // horizontal
+ {
+ arrowDownRight = RAYGUI_CLITERAL(Rectangle){ (float)bounds.x + bounds.width - spinnerSize - GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)spinnerSize, (float)spinnerSize };
+ scrollbar = RAYGUI_CLITERAL(Rectangle){ arrowUpLeft.x + arrowUpLeft.width, bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING), bounds.width - arrowUpLeft.width - arrowDownRight.width - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH), bounds.height - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING)) };
+
+ // Make sure the slider won't get outside of the scrollbar
+ sliderSize = (sliderSize >= scrollbar.width)? ((int)scrollbar.width - 2) : sliderSize;
+ slider = RAYGUI_CLITERAL(Rectangle){
+ scrollbar.x + (int)(((float)(value - minValue)/valueRange)*(scrollbar.width - sliderSize)),
+ bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING),
+ (float)sliderSize,
+ bounds.height - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING)) };
+ }
+
+ // Update control
+ //--------------------------------------------------------------------
+ if ((state != STATE_DISABLED) && !guiLocked)
+ {
+ Vector2 mousePoint = GUI_POINTER_POSITION;
+
+ if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds
+ {
+ if (GUI_BUTTON_DOWN &&
+ !CheckCollisionPointRec(mousePoint, arrowUpLeft) &&
+ !CheckCollisionPointRec(mousePoint, arrowDownRight))
+ {
+ if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec))
+ {
+ state = STATE_PRESSED;
+
+ if (isVertical) value = (int)(((float)(mousePoint.y - scrollbar.y - slider.height/2)*valueRange)/(scrollbar.height - slider.height) + minValue);
+ else value = (int)(((float)(mousePoint.x - scrollbar.x - slider.width/2)*valueRange)/(scrollbar.width - slider.width) + minValue);
+ }
+ }
+ else
+ {
+ guiControlExclusiveMode = false;
+ guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 };
+ }
+ }
+ else if (CheckCollisionPointRec(mousePoint, bounds))
+ {
+ state = STATE_FOCUSED;
+
+ // Handle mouse wheel
+ float scrollDelta = GUI_SCROLL_DELTA;
+ if (scrollDelta != 0) value += (int)scrollDelta;
+
+ // Handle mouse button down
+ if (GUI_BUTTON_PRESSED)
+ {
+ guiControlExclusiveMode = true;
+ guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts
+
+ // Check arrows click
+ if (CheckCollisionPointRec(mousePoint, arrowUpLeft)) value -= valueRange/GuiGetStyle(SCROLLBAR, SCROLL_SPEED);
+ else if (CheckCollisionPointRec(mousePoint, arrowDownRight)) value += valueRange/GuiGetStyle(SCROLLBAR, SCROLL_SPEED);
+ else if (!CheckCollisionPointRec(mousePoint, slider))
+ {
+ // If click on scrollbar position but not on slider, place slider directly on that position
+ if (isVertical) value = (int)(((float)(mousePoint.y - scrollbar.y - slider.height/2)*valueRange)/(scrollbar.height - slider.height) + minValue);
+ else value = (int)(((float)(mousePoint.x - scrollbar.x - slider.width/2)*valueRange)/(scrollbar.width - slider.width) + minValue);
+ }
+
+ state = STATE_PRESSED;
+ }
+
+ // Keyboard control on mouse hover scrollbar
+ /*
+ if (isVertical)
+ {
+ if (GUI_KEY_DOWN(KEY_DOWN)) value += 5;
+ else if (GUI_KEY_DOWN(KEY_UP)) value -= 5;
+ }
+ else
+ {
+ if (GUI_KEY_DOWN(KEY_RIGHT)) value += 5;
+ else if (GUI_KEY_DOWN(KEY_LEFT)) value -= 5;
+ }
+ */
+ }
+
+ // Normalize value
+ if (value > maxValue) value = maxValue;
+ if (value < minValue) value = minValue;
+ }
+ //--------------------------------------------------------------------
+
+ // Draw control
+ //--------------------------------------------------------------------
+ GuiDrawRectangle(bounds, GuiGetStyle(SCROLLBAR, BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER + state*3)), GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED))); // Draw the background
+
+ GuiDrawRectangle(scrollbar, 0, BLANK, GetColor(GuiGetStyle(BUTTON, BASE_COLOR_NORMAL))); // Draw the scrollbar active area background
+ GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BORDER + state*3))); // Draw the slider bar
+
+ // Draw arrows (using icon if available)
+ if (GuiGetStyle(SCROLLBAR, ARROWS_VISIBLE))
+ {
+#if defined(RAYGUI_NO_ICONS)
+ GuiDrawText(isVertical? "^" : "<",
+ RAYGUI_CLITERAL(Rectangle){ arrowUpLeft.x, arrowUpLeft.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height },
+ TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3))));
+ GuiDrawText(isVertical? "v" : ">",
+ RAYGUI_CLITERAL(Rectangle){ arrowDownRight.x, arrowDownRight.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height },
+ TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3))));
+#else
+ GuiDrawText(isVertical? GuiIconText(ICON_ARROW_UP_FILL, NULL) : GuiIconText(ICON_ARROW_LEFT_FILL, NULL),
+ RAYGUI_CLITERAL(Rectangle){ arrowUpLeft.x, arrowUpLeft.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height },
+ TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(SCROLLBAR, TEXT + state*3))); // ICON_ARROW_UP_FILL / ICON_ARROW_LEFT_FILL
+ GuiDrawText(isVertical? GuiIconText(ICON_ARROW_DOWN_FILL, NULL) : GuiIconText(ICON_ARROW_RIGHT_FILL, NULL),
+ RAYGUI_CLITERAL(Rectangle){ arrowDownRight.x, arrowDownRight.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height },
+ TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(SCROLLBAR, TEXT + state*3))); // ICON_ARROW_DOWN_FILL / ICON_ARROW_RIGHT_FILL
+#endif
+ }
+ //--------------------------------------------------------------------
+
+ return value;
+}
+
+// Update font image atlas to append raygui icons
+static int GuiFontIconBaking(Image *imFont, Font font, Rectangle *whiteRec)
+{
+ int iconOffsetY = 0;
+
+ // Check max glyph rec y bottom position in the atlas image,
+ // to start drawing icons below that line
+ int maxGlyphRecY = 0;
+ for (int i = 0; i < font.glyphCount; i++)
+ {
+ if ((font.recs[i].y + font.recs[i].height) > maxGlyphRecY)
+ maxGlyphRecY = (int)font.recs[i].y + (int)font.recs[i].height;
+ }
+
+ int maxIconsPerLine = imFont->width/(RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING);
+ int reqIconLines = RAYGUI_ICON_MAX_FONT_BACKED/maxIconsPerLine + RAYGUI_ICON_MAX_FONT_BACKED%maxIconsPerLine + 1; // One extra line
+ int reqHeight = reqIconLines*(RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING);
+
+ // Check if image requires scaling and how much
+ if ((maxGlyphRecY + reqHeight) > imFont->height)
+ {
+ int newImHeight = 1;
+ while (newImHeight < (maxGlyphRecY + reqHeight)) newImHeight <<= 1; // Round to next POT
+
+ char *newImData = (char *)RL_CALLOC(imFont->width*newImHeight*2, sizeof(char));
+ memcpy(newImData, imFont->data, imFont->width*imFont->height*2);
+ RL_FREE(imFont->data);
+
+ // Clear imFont bottom-right corner rec
+ for (int y = 0; y < 4; y++)
+ for (int x = 0; x < 4; x++)
+ ((unsigned short *)newImData)[(imFont->height - y - 1)*imFont->width + imFont->width - x - 1] = 0x0000;
+
+ imFont->data = newImData;
+ imFont->height = newImHeight;
+
+ // Set new image white corner and new white rec
+ for (int y = 0; y < 3; y++)
+ for (int x = 0; x < 3; x++)
+ ((unsigned short *)newImData)[(imFont->height - y - 1)*imFont->width + imFont->width - x - 1] = 0xffff;
+
+ *whiteRec = (Rectangle){ (float)imFont->width - 2, (float)imFont->height - 2, 1, 1 };
+ }
+
+ // Calculate image offset positions to start drawing
+ // NOTE: For Y, look for a multiple of icon size + 2*padding, for better alignment
+ int offsetX = RAYGUI_ICON_FONT_ATLAS_PADDING;
+ int offsetY = ((maxGlyphRecY + (RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING - 1))/
+ (RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING))*(RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING) +
+ RAYGUI_ICON_FONT_ATLAS_PADDING;
+
+ iconOffsetY = offsetY - RAYGUI_ICON_FONT_ATLAS_PADDING;
+ unsigned short *pixels = (unsigned short *)imFont->data;
+
+ #define BIT_CHECK(a,b) ((a) & (1u<<(b)))
+
+ for (int iconId = 0; iconId < RAYGUI_ICON_MAX_FONT_BACKED; iconId++)
+ {
+ // Wrap to next line if next icon won't fit
+ if ((offsetX + (RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING)) > imFont->width)
+ {
+ offsetX = RAYGUI_ICON_FONT_ATLAS_PADDING;
+ offsetY += RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING;
+ }
+
+ for (int i = 0, y = 0; i < RAYGUI_ICON_DATA_ELEMENTS; i++)
+ {
+ unsigned int data = guiIconsPtr[iconId*RAYGUI_ICON_DATA_ELEMENTS + i];
+
+ for (int k = 0; k < 32; k++)
+ {
+ pixels[(offsetY + y)*imFont->width + offsetX + k%16] = (BIT_CHECK(data, k))? 0xffff : 0x00ff;
+
+ if ((k == 15) || (k == 31)) y++;
+ }
+ }
+
+ // Update current icon X position
+ offsetX += (RAYGUI_ICON_SIZE + 2*RAYGUI_ICON_FONT_ATLAS_PADDING);
+ }
+
+ return iconOffsetY;
+}
+
+// Split controls text into multiple strings
+// NOTE: Re-used by GuiToggleSlider(), GuiComboBox(), GuiDropdownBox(), GuiListView(), GuiMessageBox(), GuiInputBox()
+static char **GuiTextSplit(const char *text, char delimiter, int *count)
+{
+ // NOTE: Current implementation returns a copy of the provided string with '\0' (string end delimiter)
+ // inserted between strings defined by "delimiter" parameter. No memory is dynamically allocated,
+ // all used memory is static... it has some limitations:
+ // 1. Maximum number of possible split strings is set by RAYGUI_TEXTSPLIT_MAX_ITEMS
+ // 2. Maximum size of text to split is RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE
+ // NOTE: Those definitions could be externally provided if required
+
+ #if !defined(RAYGUI_TEXTSPLIT_MAX_ITEMS)
+ #define RAYGUI_TEXTSPLIT_MAX_ITEMS 128
+ #endif
+ #if !defined(RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE)
+ #define RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE 1024 // WARNING: Max expected size for all concat items
+ #endif
+
+ static char *itemPtrs[RAYGUI_TEXTSPLIT_MAX_ITEMS] = { 0 }; // String pointers array (points to buffer data)
+ static char buffer[RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE] = { 0 }; // Buffer data (text input copy with '\0' added)
+ memset(buffer, 0, RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE);
+
+ itemPtrs[0] = buffer;
+ int itemCounter = 1;
+
+ // Count how many substrings text contains and point to every one of them
+ for (int i = 0; i < RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE; i++)
+ {
+ buffer[i] = text[i];
+ if (buffer[i] == '\0') break;
+ else if ((buffer[i] == delimiter) || (buffer[i] == '\n'))
+ {
+ itemPtrs[itemCounter] = buffer + i + 1;
+ buffer[i] = '\0'; // Set terminator for current item
+
+ itemCounter++;
+ if (itemCounter >= RAYGUI_TEXTSPLIT_MAX_ITEMS) break;
+ }
+ }
+
+ *count = itemCounter;
+ return itemPtrs;
+}
+
+// Convert color data from RGB to HSV
+// NOTE: Color data should be passed normalized
+static Vector3 ConvertRGBtoHSV(Vector3 rgb)
+{
+ Vector3 hsv = { 0 };
+ float min = 0.0f;
+ float max = 0.0f;
+ float delta = 0.0f;
+
+ min = (rgb.x < rgb.y)? rgb.x : rgb.y;
+ min = (min < rgb.z)? min : rgb.z;
+
+ max = (rgb.x > rgb.y)? rgb.x : rgb.y;
+ max = (max > rgb.z)? max : rgb.z;
+
+ hsv.z = max; // Value
+ delta = max - min;
+
+ if (delta < 0.00001f)
+ {
+ hsv.y = 0.0f;
+ hsv.x = 0.0f; // Undefined, maybe NAN?
+ return hsv;
+ }
+
+ if (max > 0.0f)
+ {
+ // NOTE: If max is 0, this divide would cause a crash
+ hsv.y = (delta/max); // Saturation
+ }
+ else
+ {
+ // NOTE: If max is 0, then r = g = b = 0, s = 0, h is undefined
+ hsv.y = 0.0f;
+ hsv.x = 0.0f; // Undefined, maybe NAN?
+ return hsv;
+ }
+
+ // NOTE: Comparing float values could not work properly
+ if (rgb.x >= max) hsv.x = (rgb.y - rgb.z)/delta; // Between yellow & magenta
+ else
+ {
+ if (rgb.y >= max) hsv.x = 2.0f + (rgb.z - rgb.x)/delta; // Between cyan & yellow
+ else hsv.x = 4.0f + (rgb.x - rgb.y)/delta; // Between magenta & cyan
+ }
+
+ hsv.x *= 60.0f; // Convert to degrees
+
+ if (hsv.x < 0.0f) hsv.x += 360.0f;
+
+ return hsv;
+}
+
+// Convert color data from HSV to RGB
+// NOTE: Color data should be passed normalized
+static Vector3 ConvertHSVtoRGB(Vector3 hsv)
+{
+ Vector3 rgb = { 0 };
+ float hh = 0.0f, p = 0.0f, q = 0.0f, t = 0.0f, ff = 0.0f;
+ long i = 0;
+
+ // NOTE: Comparing float values could not work properly
+ if (hsv.y <= 0.0f)
+ {
+ rgb.x = hsv.z;
+ rgb.y = hsv.z;
+ rgb.z = hsv.z;
+ return rgb;
+ }
+
+ hh = hsv.x;
+ if (hh >= 360.0f) hh = 0.0f;
+ hh /= 60.0f;
+
+ i = (long)hh;
+ ff = hh - i;
+ p = hsv.z*(1.0f - hsv.y);
+ q = hsv.z*(1.0f - (hsv.y*ff));
+ t = hsv.z*(1.0f - (hsv.y*(1.0f - ff)));
+
+ switch (i)
+ {
+ case 0:
+ {
+ rgb.x = hsv.z;
+ rgb.y = t;
+ rgb.z = p;
+ } break;
+ case 1:
+ {
+ rgb.x = q;
+ rgb.y = hsv.z;
+ rgb.z = p;
+ } break;
+ case 2:
+ {
+ rgb.x = p;
+ rgb.y = hsv.z;
+ rgb.z = t;
+ } break;
+ case 3:
+ {
+ rgb.x = p;
+ rgb.y = q;
+ rgb.z = hsv.z;
+ } break;
+ case 4:
+ {
+ rgb.x = t;
+ rgb.y = p;
+ rgb.z = hsv.z;
+ } break;
+ case 5:
+ default:
+ {
+ rgb.x = hsv.z;
+ rgb.y = p;
+ rgb.z = q;
+ } break;
+ }
+
+ return rgb;
+}
+
+// Color fade-in or fade-out, alpha goes from 0.0f to 1.0f
+// WARNING: It multiplies current alpha by alpha scale factor,
+// raylib Fade() multiplies alpha by 255.0f
+static Color GuiFade(Color color, float alpha)
+{
+ if (alpha < 0.0f) alpha = 0.0f;
+ else if (alpha > 1.0f) alpha = 1.0f;
+
+ Color result = { color.r, color.g, color.b, (unsigned char)(color.a*alpha) };
+
+ return result;
+}
+
+#if defined(RAYGUI_STANDALONE)
+// Returns a Color struct from hexadecimal value
+static Color GetColor(int hexValue)
+{
+ Color color;
+
+ color.r = (unsigned char)(hexValue >> 24) & 0xff;
+ color.g = (unsigned char)(hexValue >> 16) & 0xff;
+ color.b = (unsigned char)(hexValue >> 8) & 0xff;
+ color.a = (unsigned char)hexValue & 0xff;
+
+ return color;
+}
+
+// Get color with alpha applied, alpha goes from 0.0f to 1.0f
+static Color Fade(Color color, float alpha)
+{
+ Color result = color;
+
+ if (alpha < 0.0f) alpha = 0.0f;
+ else if (alpha > 1.0f) alpha = 1.0f;
+
+ result.a = (unsigned char)(255.0f*alpha);
+
+ return result;
+}
+
+// Returns hexadecimal value for a Color
+static int ColorToInt(Color color)
+{
+ return (((int)color.r << 24) | ((int)color.g << 16) | ((int)color.b << 8) | (int)color.a);
+}
+
+// Check if point is inside rectangle
+static bool CheckCollisionPointRec(Vector2 point, Rectangle rec)
+{
+ bool collision = false;
+
+ if ((point.x >= rec.x) && (point.x <= (rec.x + rec.width)) &&
+ (point.y >= rec.y) && (point.y <= (rec.y + rec.height))) collision = true;
+
+ return collision;
+}
+
+// Formatting of text with variables to 'embed'
+static const char *TextFormat(const char *text, ...)
+{
+ #if !defined(RAYGUI_TEXTFORMAT_MAX_SIZE)
+ #define RAYGUI_TEXTFORMAT_MAX_SIZE 256
+ #endif
+
+ static char buffer[RAYGUI_TEXTFORMAT_MAX_SIZE];
+
+ va_list args;
+ va_start(args, text);
+ vsnprintf(buffer, RAYGUI_TEXTFORMAT_MAX_SIZE, text, args);
+ va_end(args);
+
+ return buffer;
+}
+
+// Draw rectangle with vertical gradient fill color
+// NOTE: This function is only used by GuiColorPicker()
+static void DrawRectangleGradientV(int posX, int posY, int width, int height, Color color1, Color color2)
+{
+ Rectangle bounds = { (float)posX, (float)posY, (float)width, (float)height };
+ DrawRectangleGradientEx(bounds, color1, color2, color2, color1);
+}
+
+// Get integer value from text
+// NOTE: This function replaces atoi() [stdlib.h]
+static int TextToInteger(const char *text)
+{
+ int value = 0;
+ int sign = 1;
+
+ if ((text[0] == '+') || (text[0] == '-'))
+ {
+ if (text[0] == '-') sign = -1;
+ text++;
+ }
+
+ for (int i = 0; ((text[i] >= '0') && (text[i] <= '9')); i++) value = value*10 + (int)(text[i] - '0');
+
+ return value*sign;
+}
+
+// Get float value from text
+// NOTE: This function replaces atof() [stdlib.h]
+// WARNING: Only '.' character is understood as decimal point
+static float TextToFloat(const char *text)
+{
+ float value = 0.0f;
+ float sign = 1.0f;
+
+ if ((text[0] == '+') || (text[0] == '-'))
+ {
+ if (text[0] == '-') sign = -1.0f;
+ text++;
+ }
+
+ int i = 0;
+ for (; ((text[i] >= '0') && (text[i] <= '9')); i++) value = value*10.0f + (float)(text[i] - '0');
+
+ if (text[i++] != '.') value *= sign;
+ else
+ {
+ float divisor = 10.0f;
+ for (; ((text[i] >= '0') && (text[i] <= '9')); i++)
+ {
+ value += ((float)(text[i] - '0'))/divisor;
+ divisor = divisor*10.0f;
+ }
+ }
+
+ return value;
+}
+
+// Encode codepoint into UTF-8 text (char array size returned as parameter)
+static const char *CodepointToUTF8(int codepoint, int *byteSize)
+{
+ static char utf8[6] = { 0 };
+ int size = 0;
+
+ if (codepoint <= 0x7f)
+ {
+ utf8[0] = (char)codepoint;
+ size = 1;
+ }
+ else if (codepoint <= 0x7ff)
+ {
+ utf8[0] = (char)(((codepoint >> 6) & 0x1f) | 0xc0);
+ utf8[1] = (char)((codepoint & 0x3f) | 0x80);
+ size = 2;
+ }
+ else if (codepoint <= 0xffff)
+ {
+ utf8[0] = (char)(((codepoint >> 12) & 0x0f) | 0xe0);
+ utf8[1] = (char)(((codepoint >> 6) & 0x3f) | 0x80);
+ utf8[2] = (char)((codepoint & 0x3f) | 0x80);
+ size = 3;
+ }
+ else if (codepoint <= 0x10ffff)
+ {
+ utf8[0] = (char)(((codepoint >> 18) & 0x07) | 0xf0);
+ utf8[1] = (char)(((codepoint >> 12) & 0x3f) | 0x80);
+ utf8[2] = (char)(((codepoint >> 6) & 0x3f) | 0x80);
+ utf8[3] = (char)((codepoint & 0x3f) | 0x80);
+ size = 4;
+ }
+
+ *byteSize = size;
+
+ return utf8;
+}
+
+// Get next codepoint in a UTF-8 encoded text, scanning until '\0' is found
+// When a invalid UTF-8 byte is encountered, exiting as soon as possible and returning a '?'(0x3f) codepoint
+// Total number of bytes processed are returned as a parameter
+// NOTE: The standard says U+FFFD should be returned in case of errors
+// but that character is not supported by the default font in raylib
+static int GetCodepointNext(const char *text, int *codepointSize)
+{
+ const char *ptr = text;
+ int codepoint = 0x3f; // Codepoint (defaults to '?')
+ *codepointSize = 1;
+
+ // Get current codepoint and bytes processed
+ if (0xf0 == (0xf8 & ptr[0]))
+ {
+ // 4 byte UTF-8 codepoint
+ if (((ptr[1] & 0xC0) ^ 0x80) || ((ptr[2] & 0xC0) ^ 0x80) || ((ptr[3] & 0xC0) ^ 0x80)) { return codepoint; } //10xxxxxx checks
+ codepoint = ((0x07 & ptr[0]) << 18) | ((0x3f & ptr[1]) << 12) | ((0x3f & ptr[2]) << 6) | (0x3f & ptr[3]);
+ *codepointSize = 4;
+ }
+ else if (0xe0 == (0xf0 & ptr[0]))
+ {
+ // 3 byte UTF-8 codepoint
+ if (((ptr[1] & 0xC0) ^ 0x80) || ((ptr[2] & 0xC0) ^ 0x80)) { return codepoint; } //10xxxxxx checks
+ codepoint = ((0x0f & ptr[0]) << 12) | ((0x3f & ptr[1]) << 6) | (0x3f & ptr[2]);
+ *codepointSize = 3;
+ }
+ else if (0xc0 == (0xe0 & ptr[0]))
+ {
+ // 2 byte UTF-8 codepoint
+ if ((ptr[1] & 0xC0) ^ 0x80) { return codepoint; } //10xxxxxx checks
+ codepoint = ((0x1f & ptr[0]) << 6) | (0x3f & ptr[1]);
+ *codepointSize = 2;
+ }
+ else if (0x00 == (0x80 & ptr[0]))
+ {
+ // 1 byte UTF-8 codepoint
+ codepoint = ptr[0];
+ *codepointSize = 1;
+ }
+
+ return codepoint;
+}
+#endif // RAYGUI_STANDALONE
+
+#endif // RAYGUI_IMPLEMENTATION
diff --git a/res.c b/res.c
new file mode 100644
index 0000000..b1ed86f
--- /dev/null
+++ b/res.c
@@ -0,0 +1,251 @@
+#include "res.h"
+#include "raygui.h"
+
+typedef struct {
+ map_t(Asset) assets;
+ map_t(FunShader) sdr;
+} Resources;
+
+static Resources res = {0};
+
+static bool
+asset_load_data(cstr key) {
+ Asset *asset = map_get(&res.assets, key);
+ cstr ext = GetFileExtension(key);
+ bool success = false;
+
+ if (!strcmp(ext, ".glb")) {
+ Model mdl = LoadModel(key);
+
+ if (!IsModelValid(mdl)) {
+ UnloadModel(mdl);
+ return false;
+ }
+
+ for (int i = 0; i < mdl.materialCount; ++i) {
+ mdl.materials[i].shader = shader_get("model")->sdr;
+ }
+
+ asset->tag = ASSET_TAG_MODEL;
+ asset->mdl = mdl;
+ success = true;
+ }
+
+ if (!strcmp(ext, ".png")) {
+ Texture tex = LoadTexture(key);
+
+ if (!IsTextureValid(tex)) {
+ UnloadTexture(tex);
+ return false;
+ }
+
+ GenTextureMipmaps(&tex);
+ // SetTextureFilter(tex, TEXTURE_FILTER_ANISOTROPIC_16X);
+ SetTextureFilter(tex, TEXTURE_FILTER_TRILINEAR);
+ // SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR);
+
+ asset->tag = ASSET_TAG_TEXTURE;
+ asset->tex = tex;
+ success = true;
+ }
+
+ if (!strcmp(ext, ".ttf") || !strcmp(ext, ".otf")) {
+ Font fnt = LoadFont(key);
+
+ if (!IsFontValid(fnt)) {
+ UnloadFont(fnt);
+ return false;
+ }
+
+ asset->tag = ASSET_TAG_FONT;
+ asset->fnt = fnt;
+ success = true;
+ }
+
+ if (!strcmp(ext, ".ogg")) {
+ Sound snd = LoadSound(key);
+
+ if (!IsSoundValid(snd)) {
+ UnloadSound(snd);
+ return false;
+ }
+
+ asset->tag = ASSET_TAG_SOUND;
+ asset->snd = snd;
+ success = true;
+ }
+
+ if (!success) {
+ TraceLog(LOG_WARNING, "'%s' no valid file ext found.", key);
+ return false;
+ }
+
+ asset->loaded = true;
+ return true;
+}
+
+static void
+asset_unload(cstr key) {
+ Asset *asset = map_get(&res.assets, key);
+ if (!asset) {
+ TraceLog(LOG_WARNING,
+ "asset_unload: invalid key '%s'\n", key);
+ return;
+ }
+
+ if (!asset->loaded) {
+ TraceLog(LOG_INFO,
+ "asset_unload: nothing to unload '%s'", key);
+ return;
+ }
+
+ free(asset->path);
+ asset->path_strlen = 0;
+
+ switch(asset->tag) {
+ case ASSET_TAG_MODEL:
+ UnloadModel(asset->mdl);
+ break;
+ case ASSET_TAG_TEXTURE:
+ UnloadTexture(asset->tex);
+ break;
+ case ASSET_TAG_SOUND:
+ UnloadSound(asset->snd);
+ break;
+ case ASSET_TAG_FONT:
+ UnloadFont(asset->fnt);
+ break;
+ case ASSET_TAG_INVALID:
+ TraceLog(LOG_WARNING,
+ "asset_unload: invalid tag '%s'\n", key);
+ break;
+ }
+}
+
+void
+res_init(void) {
+ map_init(&res.sdr);
+ map_init(&res.assets);
+
+ fun_shader_create("model",
+ "res/sdr/mdl_vtx_lit.vs",
+ "res/sdr/mdl_vtx_lit.fs");
+
+ fun_shader_create("sky", NULL, "res/sdr/sky.fs");
+
+ cstr filter = ".glb;.png;.ttf;.otf;.ogg";
+ FilePathList res_list = LoadDirectoryFilesEx("res", filter, true);
+
+ for (int i = 0; i < res_list.count; ++i) {
+ cstr path = res_list.paths[i];
+ Asset asset = {0};
+ asset.path_strlen = strlen(path);
+ asset.path = strdup(path); // freed in asset_unload
+ map_set(&res.assets, asset.path, asset);
+ }
+
+ UnloadDirectoryFiles(res_list);
+}
+
+void
+res_exit(void) {
+ cstr key;
+ map_iter_t iter;
+
+ iter = map_iter(&res.assets);
+ while ((key = map_next(&res.assets, &iter))) {
+ asset_unload(key);
+ }
+
+ iter = map_iter(&res.sdr);
+ while ((key = map_next(&res.sdr, &iter))) {
+ FunShader *sdr = map_get(&res.sdr, key);
+ if (!sdr) {
+ TraceLog(LOG_ERROR, "res_exit: '%s' invalid\n", key);
+ }
+ fun_shader_delete(sdr);
+ }
+
+ map_deinit(&res.assets);
+ map_deinit(&res.sdr);
+}
+
+void
+res_update(void) {
+}
+
+Asset*
+asset_get(cstr key) {
+ Asset *asset = map_get(&res.assets, key);
+ if (!asset) {
+ TraceLog(LOG_ERROR, "asset '%s' not found.\n", key);
+ return NULL;
+ }
+
+ if (!asset->loaded) {
+ if (!asset_load_data(key)) {
+ TraceLog(LOG_ERROR, "asset '%s' couldn't load.", key);
+ return NULL;
+ }
+ }
+
+ return asset;
+}
+
+FunShader*
+shader_get(cstr key) {
+ FunShader *sdr = map_get(&res.sdr, key);
+ if (!sdr) {
+ TraceLog(LOG_ERROR, "shader '%s' not found.", key);
+ return NULL;
+ }
+ return sdr;
+}
+
+void
+fun_shader_create(cstr key, cstr vpath, cstr fpath) {
+ FunShader shader = {0};
+ shader.sdr = LoadShader(vpath, fpath);
+ if (!IsShaderValid(shader.sdr)) {
+ printf("err: fun_shader_create('%s', '%s', '%s')\n",
+ key, vpath, fpath);
+ exit(1);
+ }
+ map_init(&shader.uniforms);
+ map_set(&res.sdr, key, shader);
+}
+
+void
+fun_shader_delete(FunShader *shader) {
+ UnloadShader(shader->sdr);
+ map_deinit(&shader->uniforms);
+}
+
+int
+fun_shader_uniform(cstr skey, cstr ukey) {
+ FunShader *sdr = shader_get(skey);
+
+ int *ret = map_get(&sdr->uniforms, ukey);
+ if (ret) {
+ return *ret;
+ }
+
+ BeginShaderMode(sdr->sdr);
+ int loc = rlGetLocationUniform(sdr->sdr.id, ukey);
+ if (loc < 0) {
+ TraceLog(LOG_ERROR,
+ "shader '%s' uniform '%s' not found.\n", skey, ukey);
+ exit(1);
+ }
+ EndShaderMode();
+
+ map_set(&sdr->uniforms, ukey, loc);
+ ret = map_get(&sdr->uniforms, ukey);
+ if (!ret) {
+ TraceLog(LOG_ERROR,
+ "shader '%s' uniform '%s' not found", skey, ukey);
+ return -1;
+ }
+
+ return *ret;
+}
diff --git a/res.h b/res.h
new file mode 100644
index 0000000..6824842
--- /dev/null
+++ b/res.h
@@ -0,0 +1,43 @@
+#ifndef RES_H
+#define RES_H
+
+#include "common.h"
+
+typedef struct {
+ Shader sdr;
+ map_t(int) uniforms;
+} FunShader;
+
+typedef enum {
+ ASSET_TAG_INVALID,
+ ASSET_TAG_MODEL,
+ ASSET_TAG_TEXTURE,
+ ASSET_TAG_SOUND,
+ ASSET_TAG_FONT,
+} AssetTag;
+
+typedef struct {
+ AssetTag tag;
+ union {
+ Model mdl;
+ Texture tex;
+ Sound snd;
+ Font fnt;
+ };
+ char *path;
+ u32 path_strlen, ref_cnt;
+ bool loaded;
+} Asset;
+
+void res_init(void);
+void res_exit(void);
+void res_update(void);
+
+Asset* asset_get(cstr key);
+FunShader* shader_get(cstr key);
+
+void fun_shader_create(cstr key, cstr vpath, cstr fpath);
+void fun_shader_delete(FunShader*);
+int fun_shader_uniform(cstr skey, cstr ukey);
+
+#endif
diff --git a/res/fnt/Aileron-Regular.otf b/res/fnt/Aileron-Regular.otf
new file mode 100644
index 0000000..4248f6f
--- /dev/null
+++ b/res/fnt/Aileron-Regular.otf
Binary files differ
diff --git a/res/fnt/Inter-Regular.ttf b/res/fnt/Inter-Regular.ttf
new file mode 100644
index 0000000..b7aaca8
--- /dev/null
+++ b/res/fnt/Inter-Regular.ttf
Binary files differ
diff --git a/res/mdl/Car.glb b/res/mdl/Car.glb
new file mode 100644
index 0000000..c66144e
--- /dev/null
+++ b/res/mdl/Car.glb
Binary files differ
diff --git a/res/mdl/Car2.glb b/res/mdl/Car2.glb
new file mode 100644
index 0000000..6de7136
--- /dev/null
+++ b/res/mdl/Car2.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard.glb b/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard.glb
new file mode 100644
index 0000000..7192c07
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard_light.glb b/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard_light.glb
new file mode 100644
index 0000000..576f180
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard_light.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/retro_bollard.001.glb b/res/mdl/Cones & Barriers/Bollards/retro_bollard.001.glb
new file mode 100644
index 0000000..f2450fd
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/retro_bollard.001.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/retro_bollard.glb b/res/mdl/Cones & Barriers/Bollards/retro_bollard.glb
new file mode 100644
index 0000000..f5eb49b
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/retro_bollard.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/retro_bollard_detailed.glb b/res/mdl/Cones & Barriers/Bollards/retro_bollard_detailed.glb
new file mode 100644
index 0000000..279cad9
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/retro_bollard_detailed.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/security_bollard.glb b/res/mdl/Cones & Barriers/Bollards/security_bollard.glb
new file mode 100644
index 0000000..f0ac143
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/security_bollard.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/simple_bollard.glb b/res/mdl/Cones & Barriers/Bollards/simple_bollard.glb
new file mode 100644
index 0000000..27ebdf3
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/simple_bollard.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/simple_bollard_short.glb b/res/mdl/Cones & Barriers/Bollards/simple_bollard_short.glb
new file mode 100644
index 0000000..a2f3d11
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/simple_bollard_short.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/square_bollard.glb b/res/mdl/Cones & Barriers/Bollards/square_bollard.glb
new file mode 100644
index 0000000..b09c125
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/square_bollard.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Bollards/square_bollard_reflectors.glb b/res/mdl/Cones & Barriers/Bollards/square_bollard_reflectors.glb
new file mode 100644
index 0000000..6e3f4a5
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Bollards/square_bollard_reflectors.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier.glb b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier.glb
new file mode 100644
index 0000000..fe57e54
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center.glb b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center.glb
new file mode 100644
index 0000000..3ce70f0
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center_dirty.glb b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center_dirty.glb
new file mode 100644
index 0000000..88a095c
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center_dirty.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge.glb b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge.glb
new file mode 100644
index 0000000..8868d72
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge_dirty.glb b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge_dirty.glb
new file mode 100644
index 0000000..63d1f28
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge_dirty.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_dirty.glb b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_dirty.glb
new file mode 100644
index 0000000..08298ff
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_dirty.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Construction_cone._thin.glb b/res/mdl/Cones & Barriers/Cones/Construction_cone._thin.glb
new file mode 100644
index 0000000..e771be7
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Construction_cone._thin.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Construction_cone._thin_rubber.glb b/res/mdl/Cones & Barriers/Cones/Construction_cone._thin_rubber.glb
new file mode 100644
index 0000000..50c277c
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Construction_cone._thin_rubber.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Construction_cone.glb b/res/mdl/Cones & Barriers/Cones/Construction_cone.glb
new file mode 100644
index 0000000..ecd5b75
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Construction_cone.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Construction_cone_rubber.glb b/res/mdl/Cones & Barriers/Cones/Construction_cone_rubber.glb
new file mode 100644
index 0000000..f3a2653
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Construction_cone_rubber.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Traffic_cone_round.glb b/res/mdl/Cones & Barriers/Cones/Traffic_cone_round.glb
new file mode 100644
index 0000000..52faf12
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Traffic_cone_round.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Traffic_cone_round_rubber.glb b/res/mdl/Cones & Barriers/Cones/Traffic_cone_round_rubber.glb
new file mode 100644
index 0000000..994abed
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Traffic_cone_round_rubber.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Traffic_cone_square.glb b/res/mdl/Cones & Barriers/Cones/Traffic_cone_square.glb
new file mode 100644
index 0000000..8b1402d
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Traffic_cone_square.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Cones/Traffic_cone_square_rubber.glb b/res/mdl/Cones & Barriers/Cones/Traffic_cone_square_rubber.glb
new file mode 100644
index 0000000..c62a918
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Cones/Traffic_cone_square_rubber.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Textures/bollards.png b/res/mdl/Cones & Barriers/Textures/bollards.png
new file mode 100644
index 0000000..7098e5a
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Textures/bollards.png
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Textures/concrete_barriers.png b/res/mdl/Cones & Barriers/Textures/concrete_barriers.png
new file mode 100644
index 0000000..b8270f4
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Textures/concrete_barriers.png
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Textures/cones.png b/res/mdl/Cones & Barriers/Textures/cones.png
new file mode 100644
index 0000000..e192e83
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Textures/cones.png
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Textures/type_barriers.png b/res/mdl/Cones & Barriers/Textures/type_barriers.png
new file mode 100644
index 0000000..820680e
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Textures/type_barriers.png
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top.glb b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top.glb
new file mode 100644
index 0000000..afe5a44
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom.glb b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom.glb
new file mode 100644
index 0000000..e7d9c21
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom_wood.glb b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom_wood.glb
new file mode 100644
index 0000000..8faf473
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom_wood.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.fbx b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.fbx
new file mode 100644
index 0000000..e62e5b4
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.fbx
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.glb b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.glb
new file mode 100644
index 0000000..481f9e5
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.fbx b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.fbx
new file mode 100644
index 0000000..5e969d6
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.fbx
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.glb b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.glb
new file mode 100644
index 0000000..f61e680
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.fbx b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.fbx
new file mode 100644
index 0000000..d5772d8
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.fbx
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.glb b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.glb
new file mode 100644
index 0000000..ec43085
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.fbx b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.fbx
new file mode 100644
index 0000000..fff5804
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.fbx
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.glb b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.glb
new file mode 100644
index 0000000..1803870
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.glb
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.fbx b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.fbx
new file mode 100644
index 0000000..2e01663
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.fbx
Binary files differ
diff --git a/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.glb b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.glb
new file mode 100644
index 0000000..5943901
--- /dev/null
+++ b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.glb
Binary files differ
diff --git a/res/mdl/PSX-Waste/glb/barrel.glb b/res/mdl/PSX-Waste/glb/barrel.glb
new file mode 100644
index 0000000..ae41339
--- /dev/null
+++ b/res/mdl/PSX-Waste/glb/barrel.glb
Binary files differ
diff --git a/res/mdl/PSX-Waste/glb/cardboard-box.glb b/res/mdl/PSX-Waste/glb/cardboard-box.glb
new file mode 100644
index 0000000..611c479
--- /dev/null
+++ b/res/mdl/PSX-Waste/glb/cardboard-box.glb
Binary files differ
diff --git a/res/mdl/PSX-Waste/glb/crumpled-can.glb b/res/mdl/PSX-Waste/glb/crumpled-can.glb
new file mode 100644
index 0000000..7936bd5
--- /dev/null
+++ b/res/mdl/PSX-Waste/glb/crumpled-can.glb
Binary files differ
diff --git a/res/mdl/PSX-Waste/glb/dumpster.glb b/res/mdl/PSX-Waste/glb/dumpster.glb
new file mode 100644
index 0000000..627e5d5
--- /dev/null
+++ b/res/mdl/PSX-Waste/glb/dumpster.glb
Binary files differ
diff --git a/res/mdl/PSX-Waste/glb/glass-bottle.glb b/res/mdl/PSX-Waste/glb/glass-bottle.glb
new file mode 100644
index 0000000..a795a43
--- /dev/null
+++ b/res/mdl/PSX-Waste/glb/glass-bottle.glb
Binary files differ
diff --git a/res/mdl/PSX-Waste/glb/trash-bag.glb b/res/mdl/PSX-Waste/glb/trash-bag.glb
new file mode 100644
index 0000000..3d08c88
--- /dev/null
+++ b/res/mdl/PSX-Waste/glb/trash-bag.glb
Binary files differ
diff --git a/res/mdl/PSX-Waste/textures/barrel256x256.png b/res/mdl/PSX-Waste/textures/barrel256x256.png
new file mode 100644
index 0000000..23f6292
--- /dev/null
+++ b/res/mdl/PSX-Waste/textures/barrel256x256.png
Binary files differ
diff --git a/res/mdl/PSX-Waste/textures/can128x128.png b/res/mdl/PSX-Waste/textures/can128x128.png
new file mode 100644
index 0000000..1b96b99
--- /dev/null
+++ b/res/mdl/PSX-Waste/textures/can128x128.png
Binary files differ
diff --git a/res/mdl/PSX-Waste/textures/cardboard-box256x256.png b/res/mdl/PSX-Waste/textures/cardboard-box256x256.png
new file mode 100644
index 0000000..b6b5c34
--- /dev/null
+++ b/res/mdl/PSX-Waste/textures/cardboard-box256x256.png
Binary files differ
diff --git a/res/mdl/PSX-Waste/textures/dumpster256x256.png b/res/mdl/PSX-Waste/textures/dumpster256x256.png
new file mode 100644
index 0000000..73fcfe8
--- /dev/null
+++ b/res/mdl/PSX-Waste/textures/dumpster256x256.png
Binary files differ
diff --git a/res/mdl/PSX-Waste/textures/glass-bottle128x128.png b/res/mdl/PSX-Waste/textures/glass-bottle128x128.png
new file mode 100644
index 0000000..f2b5893
--- /dev/null
+++ b/res/mdl/PSX-Waste/textures/glass-bottle128x128.png
Binary files differ
diff --git a/res/mdl/PSX-Waste/textures/trash-bag128x128.png b/res/mdl/PSX-Waste/textures/trash-bag128x128.png
new file mode 100644
index 0000000..630226d
--- /dev/null
+++ b/res/mdl/PSX-Waste/textures/trash-bag128x128.png
Binary files differ
diff --git a/res/mdl/female_gunner_003.glb b/res/mdl/female_gunner_003.glb
new file mode 100644
index 0000000..b8991c8
--- /dev/null
+++ b/res/mdl/female_gunner_003.glb
Binary files differ
diff --git a/res/mdl/female_gunner_003_posed.glb b/res/mdl/female_gunner_003_posed.glb
new file mode 100644
index 0000000..b437cc7
--- /dev/null
+++ b/res/mdl/female_gunner_003_posed.glb
Binary files differ
diff --git a/res/mdl/female_gunner_004.glb b/res/mdl/female_gunner_004.glb
new file mode 100644
index 0000000..5bd973e
--- /dev/null
+++ b/res/mdl/female_gunner_004.glb
Binary files differ
diff --git a/res/mdl/lighthouse.glb b/res/mdl/lighthouse.glb
new file mode 100644
index 0000000..379d17e
--- /dev/null
+++ b/res/mdl/lighthouse.glb
Binary files differ
diff --git a/res/mdl/male_casual.glb b/res/mdl/male_casual.glb
new file mode 100644
index 0000000..ad0824a
--- /dev/null
+++ b/res/mdl/male_casual.glb
Binary files differ
diff --git a/res/mdl/map_prototype.glb b/res/mdl/map_prototype.glb
new file mode 100644
index 0000000..40f8781
--- /dev/null
+++ b/res/mdl/map_prototype.glb
Binary files differ
diff --git a/res/mdl/map_prototype_orig.glb b/res/mdl/map_prototype_orig.glb
new file mode 100644
index 0000000..a3f0132
--- /dev/null
+++ b/res/mdl/map_prototype_orig.glb
Binary files differ
diff --git a/res/save/scene.fgs b/res/save/scene.fgs
new file mode 100644
index 0000000..191cfe7
--- /dev/null
+++ b/res/save/scene.fgs
Binary files differ
diff --git a/res/sdr/mdl_lit.fs b/res/sdr/mdl_lit.fs
new file mode 100644
index 0000000..f3a51c6
--- /dev/null
+++ b/res/sdr/mdl_lit.fs
@@ -0,0 +1,69 @@
+#version 330
+
+uniform sampler2D texture0;
+uniform vec4 colDiffuse;
+
+uniform vec3 sun_direction;
+uniform vec3 sun_colour;
+uniform float sun_intensity;
+
+uniform vec3 moon_direction;
+uniform vec3 moon_colour;
+uniform float moon_intensity;
+
+uniform vec3 ambient_colour;
+uniform float ambient_intensity;
+
+in vec2 frag_texcoord;
+in vec4 frag_colour;
+in vec3 frag_world_normal;
+
+out vec4 finalColor;
+
+void
+main()
+{
+ vec3 normal = normalize(frag_world_normal);
+
+ // float sun_diffuse = max(
+ // dot(normal, sun_direction),
+ // 0.0
+ // );
+
+ // half lambert
+ float sun_diffuse = dot(normal, sun_direction) * 0.5 + 0.5;
+ sun_diffuse *= sun_diffuse;
+
+ float moon_diffuse = max(
+ dot(normal, moon_direction),
+ 0.0
+ );
+
+ vec3 ambient_light =
+ ambient_colour * ambient_intensity;
+
+ vec3 sun_light =
+ sun_colour *
+ sun_intensity *
+ sun_diffuse;
+
+ vec3 moon_light =
+ moon_colour *
+ moon_intensity *
+ moon_diffuse;
+
+ vec3 lighting =
+ ambient_light +
+ sun_light +
+ moon_light;
+
+ vec4 texel =
+ texture(texture0, frag_texcoord) *
+ colDiffuse *
+ frag_colour;
+
+ finalColor = vec4(
+ texel.rgb * max(lighting, vec3(0.0)),
+ texel.a
+ );
+}
diff --git a/res/sdr/mdl_lit.vs b/res/sdr/mdl_lit.vs
new file mode 100644
index 0000000..eb13a28
--- /dev/null
+++ b/res/sdr/mdl_lit.vs
@@ -0,0 +1,28 @@
+#version 330
+
+in vec3 vertexPosition;
+in vec2 vertexTexCoord;
+in vec3 vertexNormal;
+in vec4 vertexColor;
+
+uniform mat4 mvp;
+uniform mat4 matNormal;
+
+out vec2 frag_texcoord;
+out vec4 frag_colour;
+out vec3 frag_world_normal;
+
+void
+main() {
+ frag_texcoord = vertexTexCoord;
+ frag_colour = vertexColor;
+
+ /*
+ * Do not necessarily normalize here. Interpolation changes the
+ * length, so the fragment shader must normalize it regardless.
+ */
+ frag_world_normal =
+ (matNormal * vec4(vertexNormal, 0.0)).xyz;
+
+ gl_Position = mvp * vec4(vertexPosition, 1.0);
+}
diff --git a/res/sdr/mdl_vtx_lit.fs b/res/sdr/mdl_vtx_lit.fs
new file mode 100644
index 0000000..d2320c9
--- /dev/null
+++ b/res/sdr/mdl_vtx_lit.fs
@@ -0,0 +1,29 @@
+#version 330
+
+uniform sampler2D texture0;
+uniform vec4 colDiffuse;
+
+in vec2 frag_texcoord;
+in vec4 frag_colour;
+in vec3 frag_lighting;
+
+out vec4 finalColor;
+
+void
+main() {
+ vec4 texel =
+ texture(texture0, frag_texcoord) *
+ // colDiffuse *
+ frag_colour +
+ colDiffuse;
+
+ vec3 lighting = max(
+ frag_lighting,
+ vec3(0.0)
+ );
+
+ finalColor = vec4(
+ texel.rgb * lighting,
+ texel.a
+ );
+}
diff --git a/res/sdr/mdl_vtx_lit.vs b/res/sdr/mdl_vtx_lit.vs
new file mode 100644
index 0000000..c69cb3d
--- /dev/null
+++ b/res/sdr/mdl_vtx_lit.vs
@@ -0,0 +1,64 @@
+#version 330
+
+in vec3 vertexPosition;
+in vec2 vertexTexCoord;
+in vec3 vertexNormal;
+in vec4 vertexColor;
+
+uniform mat4 mvp;
+uniform mat4 matNormal;
+
+uniform vec3 sun_direction;
+uniform vec3 sun_colour;
+uniform float sun_intensity;
+
+uniform vec3 moon_direction;
+uniform vec3 moon_colour;
+uniform float moon_intensity;
+
+uniform vec3 ambient_colour;
+uniform float ambient_intensity;
+
+out vec2 frag_texcoord;
+out vec4 frag_colour;
+out vec3 frag_lighting;
+
+void
+main() {
+ vec3 world_normal = normalize(
+ (matNormal * vec4(vertexNormal, 0.0)).xyz
+ );
+
+ float sun_diffuse = max(
+ dot(world_normal, sun_direction),
+ 0.0
+ );
+
+ float moon_diffuse = max(
+ dot(world_normal, moon_direction),
+ 0.0
+ );
+
+ vec3 ambient_light =
+ ambient_colour * ambient_intensity;
+
+ vec3 sun_light =
+ sun_colour *
+ sun_intensity *
+ sun_diffuse;
+
+ vec3 moon_light =
+ moon_colour *
+ moon_intensity *
+ moon_diffuse;
+
+ frag_lighting =
+ ambient_light +
+ sun_light +
+ moon_light;
+
+ frag_texcoord = vertexTexCoord;
+ frag_colour = vertexColor;
+
+ gl_Position = mvp * vec4(vertexPosition, 1.0);
+}
diff --git a/res/sdr/ocean.fs b/res/sdr/ocean.fs
new file mode 100644
index 0000000..53b81f5
--- /dev/null
+++ b/res/sdr/ocean.fs
@@ -0,0 +1,264 @@
+#version 330
+
+in vec3 world_position;
+
+uniform float time;
+uniform vec3 camera_position;
+
+uniform vec3 sun_direction;
+uniform vec3 moon_direction;
+uniform vec3 sun_colour;
+uniform vec3 moon_colour;
+uniform vec3 ambient_colour;
+uniform vec3 horizon_colour;
+uniform vec3 zenith_colour;
+
+uniform float sun_intensity;
+uniform float moon_intensity;
+uniform float ambient_intensity;
+
+out vec4 finalColor;
+
+float
+hash12(vec2 position) {
+ vec3 hash_value = fract(position.xyx * 0.1031);
+ hash_value += dot(hash_value, hash_value.yzx + vec3(33.33));
+
+ return fract((hash_value.x + hash_value.y) * hash_value.z);
+}
+
+float
+noise2(vec2 position) {
+ vec2 cell = floor(position);
+ vec2 fraction = fract(position);
+ vec2 curve = fraction * fraction *
+ (vec2(3.0) - 2.0 * fraction);
+
+ float a = hash12(cell);
+ float b = hash12(cell + vec2(1.0, 0.0));
+ float c = hash12(cell + vec2(0.0, 1.0));
+ float d = hash12(cell + vec2(1.0, 1.0));
+
+ return mix(
+ mix(a, b, curve.x),
+ mix(c, d, curve.x),
+ curve.y
+ );
+}
+
+float
+fbm(vec2 position) {
+ vec2 sample_position = position;
+ float value = 0.0;
+
+ value += noise2(sample_position) * 0.500;
+ sample_position *= 2.02;
+
+ value += noise2(sample_position) * 0.250;
+ sample_position *= 2.03;
+
+ value += noise2(sample_position) * 0.125;
+ sample_position *= 2.01;
+
+ value += noise2(sample_position) * 0.0625;
+
+ return value;
+}
+
+float
+wave_height(vec2 position) {
+ vec2 domain = position * 0.008;
+
+ vec2 warp = vec2(
+ fbm(domain * 0.8 + vec2(time * 0.030, 0.0)),
+ fbm(domain * 0.8 + vec2(7.3, time * 0.024))
+ );
+
+ domain += (warp - vec2(0.5)) * 1.8;
+
+ float wave_a = sin(dot(domain, normalize(vec2(1.0, 0.28))) *
+ 7.0 + time * 0.45);
+
+ float wave_b = sin(dot(domain, normalize(vec2(-0.42, 1.0))) *
+ 11.0 + time * 0.72);
+
+ float wave_c = sin(dot(domain, normalize(vec2(0.75, 0.63))) *
+ 15.0 - time * 0.95);
+
+ float wave_d = sin(dot(domain, normalize(vec2(-0.88, 0.18))) *
+ 5.0 + time * 0.28);
+
+ return wave_a * 0.50 + wave_b * 0.25 +
+ wave_c * 0.15 + wave_d * 0.10;
+}
+
+vec3
+wave_normal(vec2 position) {
+ const float offset = 2.0;
+
+ float height = wave_height(position);
+ float height_x = wave_height(position + vec2(offset, 0.0));
+ float height_z = wave_height(position + vec2(0.0, offset));
+
+ return normalize(vec3(
+ height - height_x,
+ 0.55,
+ height - height_z
+ ));
+}
+
+float
+light_glint(vec3 normal, vec3 view_direction,
+ vec3 light_direction, float power) {
+ vec3 half_direction = normalize(
+ light_direction + view_direction
+ );
+
+ return pow(max(dot(normal, half_direction), 0.0), power);
+}
+
+void
+main() {
+ vec2 position = world_position.xz;
+
+ vec3 normal = wave_normal(position);
+ vec3 view_direction = normalize(
+ camera_position - world_position
+ );
+
+ /* Large-scale irregular turquoise/blue variation. */
+ float patch_a = fbm(
+ position * 0.0025 + vec2(time * 0.010, 3.1)
+ );
+
+ float patch_b = fbm(
+ position * 0.0013 + vec2(-2.7, time * 0.006)
+ );
+
+ float water_patch = clamp(
+ patch_a * 0.65 + patch_b * 0.35,
+ 0.0, 1.0
+ );
+
+ vec3 deep_blue = vec3(0.02, 0.42, 0.70);
+ vec3 turquoise = vec3(0.03, 0.73, 0.74);
+ vec3 aqua = vec3(0.16, 0.78, 0.88);
+
+ vec3 base_colour = mix(
+ deep_blue, turquoise, water_patch
+ );
+
+ base_colour = mix(
+ base_colour,
+ aqua,
+ smoothstep(0.72, 1.0, water_patch)
+ );
+
+ /*
+ * Actually light the base water using the current environment.
+ * During the day this stays bright; at night it becomes properly
+ * dark and takes on the ambient sky colour.
+ */
+ vec3 ambient_light =
+ vec3(0.10) +
+ ambient_colour * ambient_intensity * 1.8;
+
+ vec3 colour = base_colour * ambient_light;
+
+ /*
+ * Fake reflected sky. The reflected wave normal determines
+ * whether we see more horizon or zenith colour.
+ */
+ float facing = max(dot(normal, view_direction), 0.0);
+ float fresnel = 1.0 - facing;
+ fresnel *= fresnel;
+ fresnel *= fresnel;
+
+ vec3 reflected_direction = reflect(
+ -view_direction,
+ normal
+ );
+
+ float sky_height = smoothstep(
+ -0.12, 0.70, reflected_direction.y
+ );
+
+ vec3 reflected_sky = mix(
+ horizon_colour,
+ zenith_colour,
+ sky_height
+ );
+
+ colour = mix(
+ colour,
+ reflected_sky,
+ 0.18 + fresnel * 0.45
+ );
+
+ /* Sun and moon glints. */
+ float sun_glint = light_glint(
+ normal,
+ view_direction,
+ normalize(sun_direction),
+ 24.0
+ );
+
+ float moon_glint = light_glint(
+ normal,
+ view_direction,
+ normalize(moon_direction),
+ 20.0
+ );
+
+ colour += sun_colour * sun_glint *
+ sun_intensity * 0.30;
+
+ colour += moon_colour * moon_glint *
+ moon_intensity * 0.18;
+
+ /* Irregular whitecaps. */
+ float cap_a = fbm(
+ position * 0.020 +
+ vec2(time * 0.080, -time * 0.050)
+ );
+
+ float cap_b = fbm(
+ position * 0.045 +
+ vec2(-4.0, time * 0.110)
+ );
+
+ float cap_field = cap_a * 0.55 + cap_b * 0.45;
+
+ float crest = wave_height(
+ position * 1.35 + vec2(17.0, -9.0)
+ ) * 0.5 + 0.5;
+
+ float steepness = clamp(
+ 1.0 - normal.y,
+ 0.0, 1.0
+ );
+
+ float whitecap =
+ smoothstep(0.72, 0.86, cap_field) *
+ smoothstep(0.58, 0.80, crest) *
+ smoothstep(0.02, 0.10, steepness);
+
+ /*
+ * Foam also receives environment lighting rather than remaining
+ * glowing white at night.
+ */
+ vec3 foam_colour = vec3(0.92, 0.98, 0.98);
+ foam_colour *= vec3(0.18) +
+ ambient_colour * ambient_intensity * 1.5;
+
+ foam_colour += sun_colour *
+ sun_intensity * 0.25;
+
+ colour = mix(
+ colour,
+ foam_colour,
+ whitecap * 0.90
+ );
+
+ finalColor = vec4(0.03, 0.73, 0.74, 1.0);
+}
diff --git a/res/sdr/ocean.vs b/res/sdr/ocean.vs
new file mode 100644
index 0000000..517d0aa
--- /dev/null
+++ b/res/sdr/ocean.vs
@@ -0,0 +1,16 @@
+#version 330
+
+in vec3 vertexPosition;
+
+uniform mat4 mvp;
+uniform mat4 matModel;
+
+out vec3 world_position;
+
+void
+main() {
+ vec4 world = matModel * vec4(vertexPosition, 1.0);
+ world_position = world.xyz;
+
+ gl_Position = mvp * vec4(vertexPosition, 1.0);
+}
diff --git a/res/sdr/sky.fs b/res/sdr/sky.fs
new file mode 100644
index 0000000..8fde0f2
--- /dev/null
+++ b/res/sdr/sky.fs
@@ -0,0 +1,72 @@
+#version 330
+
+uniform vec2 resolution;
+
+uniform vec3 camera_forward;
+uniform vec3 camera_right;
+uniform vec3 camera_up;
+uniform vec3 sun_direction;
+
+uniform vec3 horizon_colour;
+uniform vec3 zenith_colour;
+
+uniform float day_amount;
+uniform float twilight_amount;
+uniform float tan_half_fovy;
+
+out vec4 finalColor;
+
+void
+main() {
+ vec2 ndc = gl_FragCoord.xy / resolution * 2.0 - 1.0;
+ float aspect_ratio = resolution.x / resolution.y;
+
+ vec3 view_direction = normalize(
+ camera_forward +
+ camera_right * ndc.x * aspect_ratio * tan_half_fovy +
+ camera_up * ndc.y * tan_half_fovy
+ );
+
+ float height = smoothstep(-0.20, 0.80, view_direction.y);
+ float horizon = 1.0 - smoothstep(0.0, 0.35, abs(view_direction.y));
+
+ vec3 sky = mix(horizon_colour, zenith_colour, height);
+
+ float sun_alignment = dot(view_direction, sun_direction);
+ float sun_facing = max(sun_alignment, 0.0);
+ float sun_facing_2 = sun_facing * sun_facing;
+ float sun_facing_4 = sun_facing_2 * sun_facing_2;
+
+ sky += vec3(0.65, 0.16, 0.035) *
+ horizon * twilight_amount * sun_facing_4;
+
+ float sun_wide = smoothstep(0.9510565, 0.9986295, sun_alignment);
+ float sun_core = smoothstep(0.9925462, 0.9998477, sun_alignment);
+
+ float moon_alignment = -sun_alignment;
+ float moon_wide = smoothstep(0.9702957, 0.9993908, moon_alignment);
+ float moon_core = smoothstep(0.9961947, 0.9999619, moon_alignment);
+
+ float sun_visibility = smoothstep(-0.12, 0.04, sun_direction.y);
+ float moon_visibility = smoothstep(-0.10, 0.04, -sun_direction.y);
+
+ vec3 sun_glow_colour = mix(
+ vec3(1.00, 0.82, 0.38),
+ vec3(1.00, 0.25, 0.035),
+ twilight_amount
+ );
+
+ vec3 moon_glow_colour = vec3(0.32, 0.46, 0.82);
+
+ float sun_glow =
+ (sun_wide * 0.13 + sun_core * 0.32) * sun_visibility;
+
+ float moon_glow =
+ (moon_wide * 0.08 + moon_core * 0.17) *
+ moon_visibility * (1.0 - day_amount);
+
+ sky += (1.0 - sky) * sun_glow_colour * sun_glow;
+ sky += (1.0 - sky) * moon_glow_colour * moon_glow;
+
+ finalColor = vec4(clamp(sky, 0.0, 1.0), 1.0);
+}
diff --git a/res/sdr/sky_halo_old.fs b/res/sdr/sky_halo_old.fs
new file mode 100644
index 0000000..50f2002
--- /dev/null
+++ b/res/sdr/sky_halo_old.fs
@@ -0,0 +1,279 @@
+#version 330
+
+uniform vec2 resolution;
+
+uniform vec3 camera_forward;
+uniform vec3 camera_right;
+uniform vec3 camera_up;
+
+uniform vec3 sun_direction;
+
+uniform float aspect_ratio;
+uniform float tan_half_fovy;
+
+out vec4 finalColor;
+
+void
+main()
+{
+ /*
+ * Reconstruct a world-space direction for this screen pixel.
+ */
+ vec2 screen = gl_FragCoord.xy / resolution;
+ vec2 ndc = screen * 2.0 - 1.0;
+
+ vec3 view_direction = normalize(
+ camera_forward +
+ camera_right * ndc.x * aspect_ratio * tan_half_fovy +
+ camera_up * ndc.y * tan_half_fovy
+ );
+
+ vec3 moon_direction = -sun_direction;
+
+ /*
+ * Vertical sky gradient.
+ */
+ float height = smoothstep(
+ -0.20,
+ 0.80,
+ view_direction.y
+ );
+
+ /*
+ * Day/night transition based on the sun's elevation.
+ */
+ float day_amount = smoothstep(
+ -0.10,
+ 0.12,
+ sun_direction.y
+ );
+
+ float night_amount = 1.0 - day_amount;
+
+ vec3 night_horizon = vec3(
+ 0.055,
+ 0.045,
+ 0.110
+ );
+
+ vec3 night_zenith = vec3(
+ 0.005,
+ 0.008,
+ 0.025
+ );
+
+ vec3 day_horizon = vec3(
+ 0.620,
+ 0.780,
+ 0.920
+ );
+
+ vec3 day_zenith = vec3(
+ 0.080,
+ 0.280,
+ 0.620
+ );
+
+ vec3 night_sky = mix(
+ night_horizon,
+ night_zenith,
+ height
+ );
+
+ vec3 day_sky = mix(
+ day_horizon,
+ day_zenith,
+ height
+ );
+
+ vec3 sky = mix(
+ night_sky,
+ day_sky,
+ day_amount
+ );
+
+ /*
+ * Sunrise and sunset colouring.
+ */
+ float horizon =
+ 1.0 -
+ smoothstep(
+ 0.0,
+ 0.35,
+ abs(view_direction.y)
+ );
+
+ float twilight =
+ 1.0 -
+ smoothstep(
+ 0.02,
+ 0.28,
+ abs(sun_direction.y)
+ );
+
+ float sun_alignment = dot(
+ view_direction,
+ sun_direction
+ );
+
+ float sun_facing = max(
+ sun_alignment,
+ 0.0
+ );
+
+ /*
+ * Avoid pow(): x^4 is sufficient for the broad sunset effect.
+ */
+ float sun_facing_2 = sun_facing * sun_facing;
+ float sun_facing_4 = sun_facing_2 * sun_facing_2;
+
+ vec3 sunset_colour = vec3(
+ 0.65,
+ 0.16,
+ 0.035
+ );
+
+ sky +=
+ sunset_colour *
+ horizon *
+ twilight *
+ sun_facing_4;
+
+ /*
+ * Sun halo.
+ *
+ * The values are precalculated cosine values. Since both
+ * view_direction and sun_direction are normalized, their dot
+ * product is the cosine of the angle between them.
+ *
+ * Wide halo:
+ * outer edge: 18 degrees
+ * inner edge: 3 degrees
+ *
+ * Core halo:
+ * outer edge: 7 degrees
+ * inner edge: 1 degree
+ */
+ float sun_wide = smoothstep(
+ 0.9510565,
+ 0.9986295,
+ sun_alignment
+ );
+
+ float sun_core = smoothstep(
+ 0.9925462,
+ 0.9998477,
+ sun_alignment
+ );
+
+ /*
+ * Moon halo.
+ *
+ * Because the moon is directly opposite the sun in this model,
+ * its alignment is the negative sun alignment.
+ *
+ * Wide halo:
+ * outer edge: 14 degrees
+ * inner edge: 2 degrees
+ *
+ * Core halo:
+ * outer edge: 5 degrees
+ * inner edge: 0.5 degrees
+ */
+ float moon_alignment = -sun_alignment;
+
+ float moon_wide = smoothstep(
+ 0.9702957,
+ 0.9993908,
+ moon_alignment
+ );
+
+ float moon_core = smoothstep(
+ 0.9961947,
+ 0.9999619,
+ moon_alignment
+ );
+
+ /*
+ * Fade each halo near and below the horizon.
+ *
+ * These ranges are deliberately broader than the symbol fade so
+ * the surrounding light disappears gradually.
+ */
+ float sun_above_horizon = smoothstep(
+ -0.12,
+ 0.04,
+ sun_direction.y
+ );
+
+ float moon_above_horizon = smoothstep(
+ -0.10,
+ 0.04,
+ moon_direction.y
+ );
+
+ /*
+ * Warm the sun halo around sunrise and sunset.
+ */
+ vec3 daylight_glow = vec3(
+ 1.00,
+ 0.82,
+ 0.38
+ );
+
+ vec3 sunset_glow = vec3(
+ 1.00,
+ 0.25,
+ 0.035
+ );
+
+ vec3 sun_glow_colour = mix(
+ daylight_glow,
+ sunset_glow,
+ twilight
+ );
+
+ vec3 moon_glow_colour = vec3(
+ 0.32,
+ 0.46,
+ 0.82
+ );
+
+ /*
+ * Broad component blends the sprite into the sky.
+ * Core component makes the centre appear luminous.
+ */
+ float sun_glow =
+ (
+ sun_wide * 0.13 +
+ sun_core * 0.32
+ ) *
+ sun_above_horizon;
+
+ float moon_glow =
+ (
+ moon_wide * 0.08 +
+ moon_core * 0.17
+ ) *
+ moon_above_horizon *
+ night_amount;
+
+ /*
+ * Soft screen-like blending. This brightens the sky without
+ * immediately clipping bright daytime colours to white.
+ */
+ sky +=
+ (1.0 - sky) *
+ sun_glow_colour *
+ sun_glow;
+
+ sky +=
+ (1.0 - sky) *
+ moon_glow_colour *
+ moon_glow;
+
+ finalColor = vec4(
+ clamp(sky, 0.0, 1.0),
+ 1.0
+ );
+}
diff --git a/res/sdr/sky_old.fs b/res/sdr/sky_old.fs
new file mode 100644
index 0000000..039691c
--- /dev/null
+++ b/res/sdr/sky_old.fs
@@ -0,0 +1,92 @@
+#version 330
+
+uniform vec2 resolution;
+uniform vec3 camera_forward;
+uniform vec3 camera_right;
+uniform vec3 camera_up;
+
+uniform vec3 sun_direction;
+
+uniform float aspect_ratio;
+uniform float tan_half_fovy;
+
+out vec4 finalColor;
+
+float
+angular_halo(float alignment, float outer_cos, float inner_cos) {
+ /*
+ * alignment is 1.0 while looking directly at the celestial body.
+ *
+ * outer_cos = cos(outer halo angle)
+ * inner_cos = cos(inner halo angle)
+ */
+ return smoothstep(
+ outer_cos,
+ inner_cos,
+ alignment
+ );
+}
+
+void
+main() {
+ vec2 screen = gl_FragCoord.xy / resolution;
+ vec2 ndc = screen * 2.0 - 1.0;
+
+ vec3 view_direction = normalize(
+ camera_forward +
+ camera_right * ndc.x * aspect_ratio * tan_half_fovy +
+ camera_up * ndc.y * tan_half_fovy
+ );
+
+ /*
+ * Controls the vertical gradient.
+ *
+ * -0.2 includes some colour below the horizon so the transition
+ * remains smooth while looking downward.
+ */
+ float height = smoothstep(-0.20, 0.80, view_direction.y);
+
+ /*
+ * The world becomes mostly daylight shortly after the sun crosses
+ * the horizon, and mostly night shortly after it sets.
+ */
+ float day_amount = smoothstep(-0.10, 0.12, sun_direction.y);
+
+ vec3 night_horizon = vec3(0.055, 0.045, 0.110);
+ vec3 night_zenith = vec3(0.005, 0.008, 0.025);
+
+ vec3 day_horizon = vec3(0.620, 0.780, 0.920);
+ vec3 day_zenith = vec3(0.080, 0.280, 0.620);
+
+ vec3 night_sky = mix(night_horizon, night_zenith, height);
+ vec3 day_sky = mix(day_horizon, day_zenith, height);
+
+ vec3 sky = mix(night_sky, day_sky, day_amount);
+
+ /*
+ * Stylized sunrise/sunset colour.
+ *
+ * This is strongest:
+ * - near the horizon;
+ * - while the sun is near the horizon;
+ * - while looking toward the sun.
+ */
+ float horizon =
+ 1.0 - smoothstep(0.0, 0.35, abs(view_direction.y));
+
+ float twilight =
+ 1.0 - smoothstep(0.02, 0.28, abs(sun_direction.y));
+
+ float sun_facing = max(dot(view_direction, sun_direction), 0.0);
+ float sun_facing_2 = sun_facing * sun_facing;
+ float sun_facing_4 = sun_facing_2 * sun_facing_2;
+
+ vec3 sunset_colour = vec3(0.65, 0.16, 0.035);
+
+ sky += sunset_colour *
+ horizon *
+ twilight *
+ sun_facing_4;
+
+ finalColor = vec4(min(sky, vec3(1.0)), 1.0);
+}
diff --git a/res/tex/moon.png b/res/tex/moon.png
new file mode 100644
index 0000000..d4c9d00
--- /dev/null
+++ b/res/tex/moon.png
Binary files differ
diff --git a/res/tex/sand.png b/res/tex/sand.png
new file mode 100644
index 0000000..c0b79e5
--- /dev/null
+++ b/res/tex/sand.png
Binary files differ
diff --git a/res/tex/sand2.png b/res/tex/sand2.png
new file mode 100644
index 0000000..ced7352
--- /dev/null
+++ b/res/tex/sand2.png
Binary files differ
diff --git a/res/tex/sun.png b/res/tex/sun.png
new file mode 100644
index 0000000..2031df2
--- /dev/null
+++ b/res/tex/sun.png
Binary files differ
diff --git a/res/tex/water.png b/res/tex/water.png
new file mode 100644
index 0000000..0c4fbb8
--- /dev/null
+++ b/res/tex/water.png
Binary files differ
diff --git a/save.c b/save.c
new file mode 100644
index 0000000..9d46fe3
--- /dev/null
+++ b/save.c
@@ -0,0 +1,1056 @@
+#include "save.h"
+#include "ent.h"
+
+typedef struct {
+ i64 size_pos;
+ i64 data_pos;
+} SaveChunk;
+
+typedef struct {
+ u64 next_pid;
+} SaveLoadContext;
+
+static bool
+read_u8_le(FILE *fp, u8 *val) {
+ return fread(val, 1, 1, fp) == 1;
+}
+
+static bool
+write_u8_le(FILE *fp, u8 val) {
+ return fwrite(&val, 1, 1, fp) == 1;
+}
+
+static bool
+read_u16_le(FILE *fp, u16 *val) {
+ u8 bytes[2];
+
+ if (fread(bytes, 1, 2, fp) != 2) {
+ return false;
+ }
+
+ *val = (u16)bytes[0]
+ | (u16)bytes[1] << 8;
+
+ return true;
+}
+
+static bool
+write_u16_le(FILE *fp, u16 val) {
+ u8 bytes[2] = {
+ (u8)(val >> 0),
+ (u8)(val >> 8),
+ };
+
+ return fwrite(bytes, 1, 2, fp) == 2;
+}
+
+static bool
+read_u32_le(FILE *fp, u32 *val) {
+ u8 bytes[4];
+
+ if (fread(bytes, 1, 4, fp) != 4) {
+ return false;
+ }
+
+ *val = (u32)bytes[0] << 0
+ | (u32)bytes[1] << 8
+ | (u32)bytes[2] << 16
+ | (u32)bytes[3] << 24;
+
+ return true;
+}
+
+static bool
+write_u32_le(FILE *fp, u32 val) {
+ u8 bytes[4] = {
+ (u8)(val >> 0),
+ (u8)(val >> 8),
+ (u8)(val >> 16),
+ (u8)(val >> 24),
+ };
+
+ return fwrite(bytes, 1, 4, fp) == 4;
+}
+
+static bool
+read_u64_le(FILE *fp, u64 *val) {
+ u8 bytes[8];
+
+ if (fread(bytes, 1, 8, fp) != 8) {
+ return false;
+ }
+
+ *val = (u64)bytes[0] << 0
+ | (u64)bytes[1] << 8
+ | (u64)bytes[2] << 16
+ | (u64)bytes[3] << 24
+ | (u64)bytes[4] << 32
+ | (u64)bytes[5] << 40
+ | (u64)bytes[6] << 48
+ | (u64)bytes[7] << 56;
+
+ return true;
+}
+
+static bool
+write_u64_le(FILE *fp, u64 val) {
+ u8 bytes[8] = {
+ (u8)(val >> 0),
+ (u8)(val >> 8),
+ (u8)(val >> 16),
+ (u8)(val >> 24),
+ (u8)(val >> 32),
+ (u8)(val >> 40),
+ (u8)(val >> 48),
+ (u8)(val >> 56),
+ };
+
+ return fwrite(bytes, 1, 8, fp) == 8;
+}
+
+static bool
+read_f32_le(FILE *fp, f32 *val) {
+ u32 bits;
+ if (!read_u32_le(fp, &bits)) {
+ return false;
+ }
+
+ memcpy(val, &bits, sizeof(*val));
+ return true;
+}
+
+static bool
+write_f32_le(FILE *fp, f32 value) {
+ u32 bits;
+ memcpy(&bits, &value, 4);
+ return write_u32_le(fp, bits);
+}
+
+static bool
+read_vec3(FILE *fp, Vec3 *val) {
+ return read_f32_le(fp, &val->x) &&
+ read_f32_le(fp, &val->y) &&
+ read_f32_le(fp, &val->z);
+}
+
+static bool
+write_vec3(FILE *fp, Vec3 val) {
+ return write_f32_le(fp, val.x) &&
+ write_f32_le(fp, val.y) &&
+ write_f32_le(fp, val.z);
+}
+
+static bool
+read_quat(FILE *fp, Quaternion *val) {
+ return read_f32_le(fp, &val->x) &&
+ read_f32_le(fp, &val->y) &&
+ read_f32_le(fp, &val->z) &&
+ read_f32_le(fp, &val->w);
+}
+
+static bool
+write_quat(FILE *fp, Quaternion val) {
+ return write_f32_le(fp, val.x) &&
+ write_f32_le(fp, val.y) &&
+ write_f32_le(fp, val.z) &&
+ write_f32_le(fp, val.w);
+}
+
+static bool
+write_byte_arr(FILE *fp, const void *data, u32 len) {
+ return fwrite(data, 1, len, fp) == len;
+}
+
+/* some entities are 'transient', so we need a persistent entity count */
+static u32
+persistent_entity_count(void) {
+ u32 cnt = 0;
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ if (entity_data()[i].pid != 0) {
+ cnt++;
+ }
+ }
+
+ return cnt;
+}
+
+static bool
+chunk_begin(FILE *fp, const char label[4], SaveChunk *chunk) {
+ if (!write_byte_arr(fp, label, 4)) {
+ return false;
+ }
+
+ chunk->size_pos = ftell(fp);
+ if (chunk->size_pos < 0 || !write_u32_le(fp, 0)) {
+ return false;
+ }
+
+ chunk->data_pos = ftell(fp);
+ return chunk->data_pos >= 0;
+}
+
+static bool
+chunk_end(FILE *fp, SaveChunk *chunk) {
+ u32 end_pos = ftell(fp);
+ if (end_pos < chunk->data_pos) {
+ return false;
+ }
+
+ u64 size = (u64)(end_pos - chunk->data_pos);
+ if (size > UINT32_MAX) {
+ return false;
+ }
+
+ if (fseek(fp, chunk->size_pos, SEEK_SET) != 0 ||
+ !write_u32_le(fp, (u32)size) ||
+ fseek(fp, end_pos, SEEK_SET) != 0) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+read_header(FILE *fp) {
+ char magic[4];
+ u16 major, minor;
+
+ if (fread(magic, 1, 4, fp) != 4 ||
+ !read_u16_le(fp, &major) ||
+ !read_u16_le(fp, &minor)) {
+ return false;
+ }
+
+ if (memcmp(magic, "FGSF", 4) != 0) {
+ TraceLog(LOG_ERROR, "invalid save file magic.");
+ return false;
+ }
+
+ if (major != SAVE_VERSION_MAJOR ||
+ minor != SAVE_VERSION_MINOR) {
+ TraceLog(LOG_ERROR, "unsupported save file version %u.%u.",
+ major, minor);
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+write_header(FILE *fp) {
+ return fwrite("FGSF", 1, 4, fp) == 4 &&
+ write_u16_le(fp, SAVE_VERSION_MAJOR) &&
+ write_u16_le(fp, SAVE_VERSION_MINOR);
+}
+
+static bool
+read_entity_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u64 next_pid;
+ u32 entity_cnt;
+
+ if (!read_u64_le(fp, &next_pid) ||
+ !read_u32_le(fp, &entity_cnt)) {
+ return false;
+ }
+
+ if (next_pid == 0) {
+ return false;
+ }
+
+ ctx->next_pid = next_pid;
+
+ for (u32 i = 0; i < entity_cnt; ++i) {
+ u64 pid = 0;
+ read_u64_le(fp, &pid);
+
+ if (pid == 0 || pid >= next_pid) {
+ TraceLog(LOG_ERROR, "entity PID invalid.");
+ return false;
+ }
+
+ u32 name_len = 0;
+ read_u32_le(fp, &name_len);
+
+ u64 id = entity_restore(pid);
+ if (!id) {
+ TraceLog(LOG_ERROR, "entity pid already exists.");
+ return false;
+ }
+ Entity *ent = entity_get(id);
+ ent->name = malloc(name_len + 1);
+
+ if (name_len > 0 &&
+ fread(ent->name, 1, name_len, fp) != name_len) {
+ TraceLog(LOG_ERROR, "failed to read entity name.");
+ return false;
+ }
+
+ ent->name[name_len] = '\0';
+ }
+
+ entity_system_set_next_pid(next_pid);
+ return true;
+}
+
+static bool
+write_entity_chunk(FILE *fp) {
+ SaveChunk chunk = {0};
+
+ if (!chunk_begin(fp, "ENTT", &chunk) ||
+ !write_u64_le(fp, entity_next_pid()) ||
+ !write_u32_le(fp, persistent_entity_count())) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (ent->pid == 0) {
+ continue;
+ }
+
+ if (!write_u64_le(fp, ent->pid) ||
+ !write_u32_le(fp, strlen(ent->name)) ||
+ !write_byte_arr(fp, ent->name, strlen(ent->name))) {
+ return false;
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+static bool
+read_transform_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u32 tr_cnt;
+ read_u32_le(fp, &tr_cnt);
+
+ u64 *parent_pids = calloc(ctx->next_pid, sizeof(u64));
+ if (!parent_pids) {
+ TraceLog(LOG_FATAL, "calloc failed.");
+ }
+
+ for (u32 i = 0; i < tr_cnt; ++i) {
+ u64 pid, parent_pid;
+
+ if (!read_u64_le(fp, &pid) ||
+ !read_u64_le(fp, &parent_pid) ||
+ pid == 0 || pid >= ctx->next_pid ||
+ parent_pid >= ctx->next_pid) {
+ goto fail;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ Entity *ent = entity_get(id);
+ if (!ent) {
+ TraceLog(LOG_ERROR, "failed to properly load entity");
+ goto fail;
+ }
+
+ if (!read_vec3(fp, &ent->tr.pos) ||
+ !read_quat(fp, &ent->tr.rot) ||
+ !read_vec3(fp, &ent->tr.scale)) {
+ goto fail;
+ }
+
+ parent_pids[pid] = parent_pid;
+ }
+
+ /* Build hierarchy only after all transforms exist. */
+ for (u64 pid = 1; pid < ctx->next_pid; ++pid) {
+ u64 parent_pid = parent_pids[pid];
+
+ if (!parent_pid) {
+ continue;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ u64 parent_id = entity_id_from_pid(parent_pid);
+
+ if (!id || !parent_id || !entity_set_parent(id, parent_id)) {
+ TraceLog(LOG_ERROR, "entity hierarchy build failed.");
+ goto fail;
+ }
+ }
+
+ free(parent_pids);
+ return true;
+
+fail:
+ free(parent_pids);
+ return false;
+}
+
+static bool
+write_transform_chunk(FILE *fp) {
+ SaveChunk chunk;
+
+ u32 tr_cnt = 0;
+ for (u32 i = 0; i < entity_count(); ++i) {
+ if (entity_data()[i].pid) {
+ tr_cnt++;
+ }
+ }
+
+ if (!chunk_begin(fp, "TRFM", &chunk) ||
+ !write_u32_le(fp, tr_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (!ent->pid) {
+ continue;
+ }
+
+ u64 parent_pid = 0;
+ if (ent->tr.parent) {
+ Entity *parent = entity_get(ent->tr.parent);
+ if (!parent) {
+ return false;
+ }
+ parent_pid = parent->pid;
+ }
+
+ if (!write_u64_le(fp, ent->pid) ||
+ !write_u64_le(fp, parent_pid) ||
+ !write_vec3(fp, ent->tr.pos) ||
+ !write_quat(fp, ent->tr.rot) ||
+ !write_vec3(fp, ent->tr.scale)) {
+ return false;
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+static bool
+read_model_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u32 mdl_cnt;
+ if (!read_u32_le(fp, &mdl_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < mdl_cnt; ++i) {
+ u64 pid;
+ u32 key_len;
+ u8 draw;
+
+ if (!read_u64_le(fp, &pid) || !read_u32_le(fp, &key_len)) {
+ return false;
+ }
+
+ if (pid == 0 || pid >= ctx->next_pid) {
+ return false;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ Entity *ent = entity_get(id);
+
+ if (!ent || ent->mdl.has) {
+ return false;
+ }
+
+ char *key = malloc(key_len + 1);
+ if (!key) {
+ return false;
+ }
+
+ if (key_len && fread(key, 1, key_len, fp) != key_len) {
+ free(key);
+ return false;
+ }
+
+ key[key_len] = '\0';
+
+ if (!read_u8_le(fp, &draw)) {
+ free(key);
+ return false;
+ }
+
+ ent->mdl.key = key;
+ ent->mdl.draw = draw;
+ ent->mdl.has = true;
+ }
+
+ return true;
+}
+
+static bool
+write_model_chunk(FILE *fp) {
+ SaveChunk chunk = {0};
+ u32 mdl_cnt = 0;
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (ent->pid && ent->mdl.has) {
+ mdl_cnt++;
+ }
+ }
+
+ if (!chunk_begin(fp, "MODL", &chunk) ||
+ !write_u32_le(fp, mdl_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (!ent->pid || !ent->mdl.has) {
+ continue;
+ }
+
+ u32 key_len = (u32)strlen(ent->mdl.key);
+
+ if (!write_u64_le(fp, ent->pid) ||
+ !write_u32_le(fp, key_len) ||
+ !write_byte_arr(fp, ent->mdl.key, key_len) ||
+ !write_u8_le(fp, ent->mdl.draw ? 1 : 0)) {
+ return false;
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+static bool
+read_physics_shape(FILE *fp, PhysicsShape *shape) {
+ *shape = (PhysicsShape){0};
+
+ u8 tag = 0;
+ if (!read_u8_le(fp, &tag)) {
+ TraceLog(LOG_ERROR, "failed to read physics shape tag");
+ return false;
+ }
+ shape->tag = tag;
+
+ if (!read_vec3(fp, &shape->pos) ||
+ !read_quat(fp, &shape->rot) ||
+ !read_f32_le(fp, &shape->density) ||
+ !read_f32_le(fp, &shape->friction) ||
+ !read_f32_le(fp, &shape->restitution) ||
+ !read_u32_le(fp, &shape->category_bits) ||
+ !read_u32_le(fp, &shape->mask_bits)) {
+ TraceLog(LOG_ERROR, "failed to read physics shape.");
+ return false;
+ }
+
+ switch (shape->tag) {
+ case PHYS_SHAPE_BOX:
+ return read_vec3(fp, &shape->Box.sz);
+ break;
+
+ case PHYS_SHAPE_SPHERE:
+ return read_f32_le(fp, &shape->Sphere.radius);
+ break;
+
+ case PHYS_SHAPE_CAPSULE:
+ return read_f32_le(fp, &shape->Capsule.height) &&
+ read_f32_le(fp, &shape->Capsule.radius);
+ break;
+
+ case PHYS_SHAPE_CYLINDER:
+ return read_f32_le(fp, &shape->Cylinder.height) &&
+ read_f32_le(fp, &shape->Cylinder.radius);
+ break;
+
+ case PHYS_SHAPE_CONVEX_HULL:
+ if (!read_u32_le(fp, &shape->ConvexHull.point_cnt)) {
+ return false;
+ }
+
+ if (shape->ConvexHull.point_cnt) {
+ shape->ConvexHull.points =
+ calloc(shape->ConvexHull.point_cnt,
+ sizeof(Vec3));
+
+ if (!shape->ConvexHull.points) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->ConvexHull.point_cnt; ++i) {
+ if (!read_vec3(fp, &shape->ConvexHull.points[i])) {
+ return false;
+ }
+ }
+ break;
+
+ case PHYS_SHAPE_STATIC_TRIMESH:
+ if (!read_u32_le(fp, &shape->TriMesh.point_cnt) ||
+ !read_u32_le(fp, &shape->TriMesh.idx_cnt)) {
+ return false;
+ }
+
+ if (shape->TriMesh.point_cnt) {
+ shape->TriMesh.points =
+ calloc(shape->TriMesh.point_cnt,
+ sizeof(Vec3));
+
+ if (!shape->TriMesh.points) {
+ return false;
+ }
+ }
+
+ if (shape->TriMesh.idx_cnt) {
+ shape->TriMesh.indices =
+ calloc(shape->TriMesh.idx_cnt, sizeof(u32));
+
+ if (!shape->TriMesh.indices) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.point_cnt; ++i) {
+ if (!read_vec3(fp, &shape->TriMesh.points[i])) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.idx_cnt; ++i) {
+ if (!read_u32_le(fp, &shape->TriMesh.indices[i])) {
+ return false;
+ }
+ }
+ break;
+ };
+
+ return true;
+}
+
+static bool
+write_physics_shape(FILE *fp, PhysicsShape *shape) {
+ if (!write_u8_le(fp, (u8)shape->tag) ||
+ !write_vec3(fp, shape->pos) ||
+ !write_quat(fp, shape->rot) ||
+ !write_f32_le(fp, shape->density) ||
+ !write_f32_le(fp, shape->friction) ||
+ !write_f32_le(fp, shape->restitution) ||
+ !write_u32_le(fp, shape->category_bits) ||
+ !write_u32_le(fp, shape->mask_bits)) {
+ return false;
+ }
+
+ switch (shape->tag) {
+ case PHYS_SHAPE_BOX:
+ return write_vec3(fp, shape->Box.sz);
+ break;
+
+ case PHYS_SHAPE_SPHERE:
+ return write_f32_le(fp, shape->Sphere.radius);
+ break;
+
+ case PHYS_SHAPE_CAPSULE:
+ return write_f32_le(fp, shape->Capsule.height) &&
+ write_f32_le(fp, shape->Capsule.radius);
+ break;
+
+ case PHYS_SHAPE_CYLINDER:
+ return write_f32_le(fp, shape->Cylinder.height) &&
+ write_f32_le(fp, shape->Cylinder.radius);
+ break;
+
+ case PHYS_SHAPE_CONVEX_HULL:
+ if (shape->ConvexHull.point_cnt &&
+ !shape->ConvexHull.points) {
+ return false;
+ }
+
+ if (!write_u32_le(fp, shape->ConvexHull.point_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < shape->ConvexHull.point_cnt; ++i) {
+ if (!write_vec3(fp, shape->ConvexHull.points[i])) {
+ return false;
+ }
+ }
+ break;
+
+ case PHYS_SHAPE_STATIC_TRIMESH:
+ if ((shape->TriMesh.point_cnt && !shape->TriMesh.points) ||
+ (shape->TriMesh.idx_cnt && !shape->TriMesh.indices)) {
+ return false;
+ }
+
+ if (!write_u32_le(fp, shape->TriMesh.point_cnt) ||
+ !write_u32_le(fp, shape->TriMesh.idx_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.point_cnt; ++i) {
+ if (!write_vec3(fp, shape->TriMesh.points[i])) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < shape->TriMesh.idx_cnt; ++i) {
+ if (!write_u32_le(fp, shape->TriMesh.indices[i])) {
+ return false;
+ }
+ }
+ break;
+ }
+
+ return true;
+}
+
+static bool
+read_physics_body(FILE *fp, PhysicsBody *body) {
+ u8 mode, enable_sleep;
+ *body = (PhysicsBody){0};
+
+ if (!read_vec3(fp, &body->pos) ||
+ !read_quat(fp, &body->rot) ||
+ !read_f32_le(fp, &body->linear_damping) ||
+ !read_f32_le(fp, &body->angular_damping) ||
+ !read_f32_le(fp, &body->gravity_scale) ||
+ !read_u8_le(fp, &mode) ||
+ !read_u8_le(fp, &enable_sleep) ||
+ !read_u32_le(fp, &body->shape_cnt)) {
+ return false;
+ }
+
+ body->mode = (PhysicsMode)mode;
+ body->enable_sleep = enable_sleep;
+
+ if (body->shape_cnt) {
+ body->shapes = calloc(body->shape_cnt, sizeof(PhysicsShape));
+ if (!body->shapes) {
+ return false;
+ }
+ }
+
+ for (u32 i = 0; i < body->shape_cnt; ++i) {
+ if (!read_physics_shape(fp, &body->shapes[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool
+write_physics_body(FILE *fp, PhysicsBody *body) {
+ if (!write_vec3(fp, body->pos) ||
+ !write_quat(fp, body->rot) ||
+ !write_f32_le(fp, body->linear_damping) ||
+ !write_f32_le(fp, body->angular_damping) ||
+ !write_f32_le(fp, body->gravity_scale) ||
+ !write_u8_le(fp, (u8)body->mode) ||
+ !write_u8_le(fp, body->enable_sleep ? 1 : 0) ||
+ !write_u32_le(fp, body->shape_cnt)) {
+ return false;
+ }
+
+ if (body->shape_cnt && !body->shapes) {
+ return false;
+ }
+
+ for (u32 i = 0; i < body->shape_cnt; ++i) {
+ if (!write_physics_shape(fp, &body->shapes[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool
+read_physics_joint(FILE *fp, PhysicsJoint *joint, u32 body_cnt) {
+ u8 tag;
+ *joint = (PhysicsJoint){0};
+
+ if (!read_u8_le(fp, &tag) ||
+ tag > PHYS_JOINT_DISTANCE ||
+ !read_u32_le(fp, &joint->body_a) ||
+ !read_u32_le(fp, &joint->body_b) ||
+ joint->body_a >= body_cnt ||
+ joint->body_b >= body_cnt ||
+ !read_u32_le(fp, &joint->flags) ||
+ !read_vec3(fp, &joint->anchor_a) ||
+ !read_vec3(fp, &joint->anchor_b) ||
+ !read_vec3(fp, &joint->axis_a) ||
+ !read_vec3(fp, &joint->axis_b) ||
+ !read_f32_le(fp, &joint->lower) ||
+ !read_f32_le(fp, &joint->upper) ||
+ !read_f32_le(fp, &joint->spring_hz) ||
+ !read_f32_le(fp, &joint->spring_damping) ||
+ !read_f32_le(fp, &joint->motor_speed) ||
+ !read_f32_le(fp, &joint->motor_force)) {
+ return false;
+ }
+
+ joint->tag = (PhysicsJointTag)tag;
+ return true;
+}
+
+static bool
+write_physics_joint(FILE *fp, PhysicsJoint *joint, u32 body_cnt) {
+ if (joint->body_a >= body_cnt ||
+ joint->body_b >= body_cnt) {
+ return false;
+ }
+
+ return write_u8_le(fp, (u8)joint->tag) &&
+ write_u32_le(fp, joint->body_a) &&
+ write_u32_le(fp, joint->body_b) &&
+ write_u32_le(fp, joint->flags) &&
+ write_vec3(fp, joint->anchor_a) &&
+ write_vec3(fp, joint->anchor_b) &&
+ write_vec3(fp, joint->axis_a) &&
+ write_vec3(fp, joint->axis_b) &&
+ write_f32_le(fp, joint->lower) &&
+ write_f32_le(fp, joint->upper) &&
+ write_f32_le(fp, joint->spring_hz) &&
+ write_f32_le(fp, joint->spring_damping) &&
+ write_f32_le(fp, joint->motor_speed) &&
+ write_f32_le(fp, joint->motor_force);
+}
+
+static bool
+read_physics_chunk(FILE *fp, SaveLoadContext *ctx) {
+ u32 phys_cnt = 0;
+ if (!read_u32_le(fp, &phys_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < phys_cnt; ++i) {
+ u64 pid;
+ u32 body_cnt, joint_cnt;
+
+ if (!read_u64_le(fp, &pid) || !pid || pid >= ctx->next_pid) {
+ return false;
+ }
+
+ u64 id = entity_id_from_pid(pid);
+ Entity *ent = entity_get(id);
+
+ if (!read_u32_le(fp, &body_cnt) || !ent || ent->phys.has) {
+ return false;
+ }
+
+ ent->phys.body_cnt = body_cnt;
+
+ if (body_cnt) {
+ ent->phys.bodies =
+ calloc(body_cnt, sizeof(PhysicsBody));
+ if (!ent->phys.bodies) {
+ return false;
+ }
+ }
+
+ for (u32 j = 0; j < body_cnt; ++j) {
+ if (!read_physics_body(fp, &ent->phys.bodies[j])) {
+ return false;
+ }
+ }
+
+ if (!read_u32_le(fp, &joint_cnt)) {
+ return false;
+ }
+
+ ent->phys.joint_cnt = joint_cnt;
+
+ if (joint_cnt) {
+ ent->phys.joints =
+ calloc(joint_cnt, sizeof(PhysicsJoint));
+ if (!ent->phys.joints) {
+ return false;
+ }
+ }
+
+ for (u32 j = 0; j < joint_cnt; ++j) {
+ if (!read_physics_joint(fp,
+ &ent->phys.joints[j], body_cnt)) {
+ return false;
+ }
+ }
+
+ ent->phys.has = true;
+ }
+
+ return true;
+}
+
+static bool
+write_physics_chunk(FILE *fp) {
+ SaveChunk chunk = {0};
+ u32 phys_cnt = 0;
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (ent->pid && ent->phys.has) {
+ phys_cnt++;
+ }
+ }
+
+ if (!chunk_begin(fp, "PHYS", &chunk) ||
+ !write_u32_le(fp, phys_cnt)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+ PhysicsCmp *phys = &ent->phys;
+
+ if (!ent->pid || !phys->has) {
+ continue;
+ }
+
+ if ((phys->body_cnt && !phys->bodies) ||
+ (phys->joint_cnt && !phys->joints) ||
+ !write_u64_le(fp, ent->pid) ||
+ !write_u32_le(fp, phys->body_cnt)) {
+ return false;
+ }
+
+ for (u32 j = 0; j < phys->body_cnt; ++j) {
+ if (!write_physics_body(fp, &phys->bodies[j])) {
+ return false;
+ }
+ }
+
+ if (!write_u32_le(fp, phys->joint_cnt)) {
+ return false;
+ }
+
+ for (u32 j = 0; j < phys->joint_cnt; ++j) {
+ if (!write_physics_joint(fp,
+ &phys->joints[j], phys->body_cnt)) {
+ return false;
+ }
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+bool
+save_file_read(cstr path) {
+ FILE *fp = fopen(path, "rb");
+ if (!fp) {
+ TraceLog(LOG_ERROR, "save file '%s' failed to load.", path);
+ return false;
+ }
+
+ SaveLoadContext ctx = {0};
+ bool success = read_header(fp);
+ bool read_entt = false;
+
+ while (success) {
+ char label[4];
+ size_t read_cnt = fread(label, 1, 4, fp);
+ if (read_cnt == 0 && feof(fp)) {
+ break;
+ }
+
+ if (read_cnt != 4) {
+ TraceLog(LOG_ERROR, "failed to read chunk label.");
+ success = false;
+ break;
+ }
+
+ u32 chunk_size;
+ if (!read_u32_le(fp, &chunk_size)) {
+ TraceLog(LOG_ERROR, "failed to read chunk size.");
+ success = false;
+ break;
+ }
+
+ u32 data_pos = ftell(fp);
+ if (data_pos < 0) {
+ success = false;
+ break;
+ }
+
+ u32 end_pos = data_pos + chunk_size;
+
+ if (memcmp(label, "ENTT", 4) == 0) {
+ if (read_entt) {
+ TraceLog(LOG_ERROR, "duplicate ENTT chunk.");
+ success = false;
+ break;
+ }
+
+ success = read_entity_chunk(fp, &ctx);
+ read_entt = success;
+ } else if (memcmp(label, "TRFM", 4) == 0) {
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "TRFM found before ENTT.");
+ success = false;
+ break;
+ }
+ success = read_transform_chunk(fp, &ctx);
+ } else if (memcmp(label, "MODL", 4) == 0) {
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "TRFM found before ENTT.");
+ success = false;
+ break;
+ }
+ success = read_model_chunk(fp, &ctx);
+ } else if (memcmp(label, "PHYS", 4) == 0) {
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "PHYS found before ENTT.");
+ success = false;
+ break;
+ }
+ success = read_physics_chunk(fp, &ctx);
+ } else {
+ TraceLog(LOG_INFO, "skipped unknown chunk.");
+ }
+
+ if (!success) {
+ break;
+ }
+
+ u32 pos = ftell(fp);
+ if (pos < 0 || pos > end_pos) {
+ TraceLog(LOG_ERROR,
+ "chunk reader exceeded chunk size.");
+ success = false;
+ break;
+ }
+
+ if (fseek(fp, end_pos, SEEK_SET) != 0) {
+ success = false;
+ break;
+ }
+ }
+
+ if (!read_entt) {
+ TraceLog(LOG_ERROR, "save contains no ENTT chunk.");
+ success = false;
+ }
+
+ if (fclose(fp) != 0) {
+ success = false;
+ }
+
+ return success;
+}
+
+bool
+save_file_write(cstr path) {
+ FILE *fp = fopen(path, "wb");
+ if (!fp) {
+ return false;
+ }
+
+ bool success =
+ write_header(fp) &&
+ write_entity_chunk(fp) &&
+ write_transform_chunk(fp) &&
+ write_model_chunk(fp) &&
+ write_physics_chunk(fp);
+
+ if (fclose(fp) != 0) {
+ success = false;
+ }
+
+ return success;
+}
+
diff --git a/save.h b/save.h
new file mode 100644
index 0000000..d7e2be4
--- /dev/null
+++ b/save.h
@@ -0,0 +1,13 @@
+#ifndef SAVE_H
+#define SAVE_H
+
+#include "common.h"
+
+/* fun game save format defined in fgsf_spec.txt */
+#define SAVE_VERSION_MAJOR 0
+#define SAVE_VERSION_MINOR 1
+
+bool save_file_read(cstr path);
+bool save_file_write(cstr path);
+
+#endif