From 08cb8d6a585dd552b69a488d9db1be123f01467a Mon Sep 17 00:00:00 2001 From: tech kaczynski Date: Tue, 22 Sep 2026 22:38:57 +1000 Subject: initial commit. --- LICENSE | 675 ++ README | 6 + app.c | 132 + app.h | 33 + b3internalassert.c | 9 + cam.c | 88 + cam.h | 24 + common.h | 111 + editor.c | 1744 ++++ editor.h | 99 + ent.c | 814 ++ ent.h | 96 + env.c | 384 + env.h | 46 + ext/fuzzy_match.c | 158 + ext/fuzzy_match.h | 8 + ext/hashmap.c | 193 + ext/hashmap.h | 77 + ext/tinyfiledialogs/README.txt | 233 + ext/tinyfiledialogs/hello.c | 307 + ext/tinyfiledialogs/hello_wchar_t.c | 241 + .../more_dialogs/tinyfd_moredialogs.c | 259 + .../more_dialogs/tinyfd_moredialogs.h | 48 + ext/tinyfiledialogs/tinyfiledialogs.c | 8401 ++++++++++++++++++++ ext/tinyfiledialogs/tinyfiledialogs.h | 314 + game.c | 46 + game.h | 31 + physics.c | 40 + physics.h | 156 + raygui.h | 6319 +++++++++++++++ res.c | 251 + res.h | 43 + res/fnt/Aileron-Regular.otf | Bin 0 -> 27644 bytes res/fnt/Inter-Regular.ttf | Bin 0 -> 411640 bytes res/mdl/Car.glb | Bin 0 -> 42936 bytes res/mdl/Car2.glb | Bin 0 -> 40648 bytes .../Bollards/concrete_retro_bollard.glb | Bin 0 -> 75648 bytes .../Bollards/concrete_retro_bollard_light.glb | Bin 0 -> 75652 bytes .../Bollards/retro_bollard.001.glb | Bin 0 -> 80392 bytes .../Cones & Barriers/Bollards/retro_bollard.glb | Bin 0 -> 76184 bytes .../Bollards/retro_bollard_detailed.glb | Bin 0 -> 80396 bytes .../Cones & Barriers/Bollards/security_bollard.glb | Bin 0 -> 69456 bytes .../Cones & Barriers/Bollards/simple_bollard.glb | Bin 0 -> 69452 bytes .../Bollards/simple_bollard_short.glb | Bin 0 -> 69460 bytes .../Cones & Barriers/Bollards/square_bollard.glb | Bin 0 -> 73476 bytes .../Bollards/square_bollard_reflectors.glb | Bin 0 -> 74000 bytes .../Concrete Barriers/Jersey_barrier.glb | Bin 0 -> 42652 bytes .../Jersey_barrier_broken_center.glb | Bin 0 -> 47420 bytes .../Jersey_barrier_broken_center_dirty.glb | Bin 0 -> 47780 bytes .../Jersey_barrier_broken_edge.glb | Bin 0 -> 48220 bytes .../Jersey_barrier_broken_edge_dirty.glb | Bin 0 -> 48448 bytes .../Concrete Barriers/Jersey_barrier_dirty.glb | Bin 0 -> 42848 bytes .../Cones/Construction_cone._thin.glb | Bin 0 -> 38044 bytes .../Cones/Construction_cone._thin_rubber.glb | Bin 0 -> 38048 bytes .../Cones & Barriers/Cones/Construction_cone.glb | Bin 0 -> 37908 bytes .../Cones/Construction_cone_rubber.glb | Bin 0 -> 37916 bytes .../Cones & Barriers/Cones/Traffic_cone_round.glb | Bin 0 -> 38908 bytes .../Cones/Traffic_cone_round_rubber.glb | Bin 0 -> 38592 bytes .../Cones & Barriers/Cones/Traffic_cone_square.glb | Bin 0 -> 37100 bytes .../Cones/Traffic_cone_square_rubber.glb | Bin 0 -> 37104 bytes res/mdl/Cones & Barriers/Textures/bollards.png | Bin 0 -> 64222 bytes .../Textures/concrete_barriers.png | Bin 0 -> 39044 bytes res/mdl/Cones & Barriers/Textures/cones.png | Bin 0 -> 32058 bytes .../Cones & Barriers/Textures/type_barriers.png | Bin 0 -> 38061 bytes .../Type 2 Barriers/Type_2_barrier_top.glb | Bin 0 -> 44468 bytes .../Type 2 Barriers/Type_2_barrier_top_bottom.glb | Bin 0 -> 45896 bytes .../Type_2_barrier_top_bottom_wood.glb | Bin 0 -> 46160 bytes .../Type 3 Barriers/Type_3_barrier_2_planks.fbx | Bin 0 -> 58876 bytes .../Type 3 Barriers/Type_3_barrier_2_planks.glb | Bin 0 -> 45576 bytes .../Type 3 Barriers/Type_3_barrier_3_planks.fbx | Bin 0 -> 59036 bytes .../Type 3 Barriers/Type_3_barrier_3_planks.glb | Bin 0 -> 46376 bytes .../Type_3_barrier_broken_planks.fbx | Bin 0 -> 59068 bytes .../Type_3_barrier_broken_planks.glb | Bin 0 -> 45540 bytes .../Type 3 Barriers/Type_3_barrier_plank.fbx | Bin 0 -> 57308 bytes .../Type 3 Barriers/Type_3_barrier_plank.glb | Bin 0 -> 40260 bytes .../Type 3 Barriers/Type_3_barrier_stilts.fbx | Bin 0 -> 58364 bytes .../Type 3 Barriers/Type_3_barrier_stilts.glb | Bin 0 -> 43852 bytes res/mdl/PSX-Waste/glb/barrel.glb | Bin 0 -> 48680 bytes res/mdl/PSX-Waste/glb/cardboard-box.glb | Bin 0 -> 11928 bytes res/mdl/PSX-Waste/glb/crumpled-can.glb | Bin 0 -> 6888 bytes res/mdl/PSX-Waste/glb/dumpster.glb | Bin 0 -> 24976 bytes res/mdl/PSX-Waste/glb/glass-bottle.glb | Bin 0 -> 10168 bytes res/mdl/PSX-Waste/glb/trash-bag.glb | Bin 0 -> 9484 bytes res/mdl/PSX-Waste/textures/barrel256x256.png | Bin 0 -> 44820 bytes res/mdl/PSX-Waste/textures/can128x128.png | Bin 0 -> 3658 bytes .../PSX-Waste/textures/cardboard-box256x256.png | Bin 0 -> 9770 bytes res/mdl/PSX-Waste/textures/dumpster256x256.png | Bin 0 -> 20624 bytes res/mdl/PSX-Waste/textures/glass-bottle128x128.png | Bin 0 -> 5166 bytes res/mdl/PSX-Waste/textures/trash-bag128x128.png | Bin 0 -> 5133 bytes res/mdl/female_gunner_003.glb | Bin 0 -> 783652 bytes res/mdl/female_gunner_003_posed.glb | Bin 0 -> 176456 bytes res/mdl/female_gunner_004.glb | Bin 0 -> 783856 bytes res/mdl/lighthouse.glb | Bin 0 -> 90388 bytes res/mdl/male_casual.glb | Bin 0 -> 189760 bytes res/mdl/map_prototype.glb | Bin 0 -> 2185768 bytes res/mdl/map_prototype_orig.glb | Bin 0 -> 2682220 bytes res/save/scene.fgs | Bin 0 -> 1552 bytes res/sdr/mdl_lit.fs | 69 + res/sdr/mdl_lit.vs | 28 + res/sdr/mdl_vtx_lit.fs | 29 + res/sdr/mdl_vtx_lit.vs | 64 + res/sdr/ocean.fs | 264 + res/sdr/ocean.vs | 16 + res/sdr/sky.fs | 72 + res/sdr/sky_halo_old.fs | 279 + res/sdr/sky_old.fs | 92 + res/tex/moon.png | Bin 0 -> 36095 bytes res/tex/sand.png | Bin 0 -> 1930220 bytes res/tex/sand2.png | Bin 0 -> 43585 bytes res/tex/sun.png | Bin 0 -> 56174 bytes res/tex/water.png | Bin 0 -> 45017 bytes save.c | 1056 +++ save.h | 13 + 113 files changed, 23368 insertions(+) create mode 100644 LICENSE create mode 100644 README create mode 100644 app.c create mode 100644 app.h create mode 100644 b3internalassert.c create mode 100644 cam.c create mode 100644 cam.h create mode 100644 common.h create mode 100644 editor.c create mode 100644 editor.h create mode 100644 ent.c create mode 100644 ent.h create mode 100644 env.c create mode 100644 env.h create mode 100644 ext/fuzzy_match.c create mode 100644 ext/fuzzy_match.h create mode 100644 ext/hashmap.c create mode 100644 ext/hashmap.h create mode 100644 ext/tinyfiledialogs/README.txt create mode 100644 ext/tinyfiledialogs/hello.c create mode 100644 ext/tinyfiledialogs/hello_wchar_t.c create mode 100644 ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.c create mode 100644 ext/tinyfiledialogs/more_dialogs/tinyfd_moredialogs.h create mode 100644 ext/tinyfiledialogs/tinyfiledialogs.c create mode 100644 ext/tinyfiledialogs/tinyfiledialogs.h create mode 100644 game.c create mode 100644 game.h create mode 100644 physics.c create mode 100644 physics.h create mode 100644 raygui.h create mode 100644 res.c create mode 100644 res.h create mode 100644 res/fnt/Aileron-Regular.otf create mode 100644 res/fnt/Inter-Regular.ttf create mode 100644 res/mdl/Car.glb create mode 100644 res/mdl/Car2.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard_light.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/retro_bollard.001.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/retro_bollard.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/retro_bollard_detailed.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/security_bollard.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/simple_bollard.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/simple_bollard_short.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/square_bollard.glb create mode 100644 res/mdl/Cones & Barriers/Bollards/square_bollard_reflectors.glb create mode 100644 res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier.glb create mode 100644 res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center.glb create mode 100644 res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center_dirty.glb create mode 100644 res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge.glb create mode 100644 res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge_dirty.glb create mode 100644 res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_dirty.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Construction_cone._thin.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Construction_cone._thin_rubber.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Construction_cone.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Construction_cone_rubber.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Traffic_cone_round.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Traffic_cone_round_rubber.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Traffic_cone_square.glb create mode 100644 res/mdl/Cones & Barriers/Cones/Traffic_cone_square_rubber.glb create mode 100644 res/mdl/Cones & Barriers/Textures/bollards.png create mode 100644 res/mdl/Cones & Barriers/Textures/concrete_barriers.png create mode 100644 res/mdl/Cones & Barriers/Textures/cones.png create mode 100644 res/mdl/Cones & Barriers/Textures/type_barriers.png create mode 100644 res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top.glb create mode 100644 res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom.glb create mode 100644 res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom_wood.glb create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.fbx create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.glb create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.fbx create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.glb create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.fbx create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.glb create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.fbx create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.glb create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.fbx create mode 100644 res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.glb create mode 100644 res/mdl/PSX-Waste/glb/barrel.glb create mode 100644 res/mdl/PSX-Waste/glb/cardboard-box.glb create mode 100644 res/mdl/PSX-Waste/glb/crumpled-can.glb create mode 100644 res/mdl/PSX-Waste/glb/dumpster.glb create mode 100644 res/mdl/PSX-Waste/glb/glass-bottle.glb create mode 100644 res/mdl/PSX-Waste/glb/trash-bag.glb create mode 100644 res/mdl/PSX-Waste/textures/barrel256x256.png create mode 100644 res/mdl/PSX-Waste/textures/can128x128.png create mode 100644 res/mdl/PSX-Waste/textures/cardboard-box256x256.png create mode 100644 res/mdl/PSX-Waste/textures/dumpster256x256.png create mode 100644 res/mdl/PSX-Waste/textures/glass-bottle128x128.png create mode 100644 res/mdl/PSX-Waste/textures/trash-bag128x128.png create mode 100644 res/mdl/female_gunner_003.glb create mode 100644 res/mdl/female_gunner_003_posed.glb create mode 100644 res/mdl/female_gunner_004.glb create mode 100644 res/mdl/lighthouse.glb create mode 100644 res/mdl/male_casual.glb create mode 100644 res/mdl/map_prototype.glb create mode 100644 res/mdl/map_prototype_orig.glb create mode 100644 res/save/scene.fgs create mode 100644 res/sdr/mdl_lit.fs create mode 100644 res/sdr/mdl_lit.vs create mode 100644 res/sdr/mdl_vtx_lit.fs create mode 100644 res/sdr/mdl_vtx_lit.vs create mode 100644 res/sdr/ocean.fs create mode 100644 res/sdr/ocean.vs create mode 100644 res/sdr/sky.fs create mode 100644 res/sdr/sky_halo_old.fs create mode 100644 res/sdr/sky_old.fs create mode 100644 res/tex/moon.png create mode 100644 res/tex/sand.png create mode 100644 res/tex/sand2.png create mode 100644 res/tex/sun.png create mode 100644 res/tex/water.png create mode 100644 save.c create mode 100644 save.h 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. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. + 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 +#include + +/* 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 + +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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#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 + +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 = ""; +// } 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 = ""; + } 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 +#include +#include +#include +#include + +#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 + +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 +#include +#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 + +#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 +#include +#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 +#include +#include + +#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 +#include +#include + +#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 +#include +#include +#include +#include + +#ifdef _WIN32 + #ifdef __BORLANDC__ + #define _getch getch + #endif + #ifndef _WIN32_WINNT + #define _WIN32_WINNT 0x0500 + #endif + #include + #include + #include + #include + #include + #define TINYFD_NOCCSUNICODE + #define TINYFD_SLASH "\\" +#else + #include + #include + #include /* on old systems try instead */ + #include + #include + #include /* on old systems try 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\ +\n\ +\n\ +"); + if ( aTitle && wcslen(aTitle) ) wcscat(lDialogString, aTitle); + wcscat(lDialogString, L"\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +
\n"); + + wcscat(lDialogString, aMessage ? aMessage : L""); + + wcscat(lDialogString, L"\n\ +\n\ +\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\ +\n\ +\n\ +"); + if (aTitle) wcscat(lDialogString, aTitle); + wcscat(lDialogString, L"\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +
\n"); + + wcscat(lDialogString, aMessage ? aMessage : L""); + + wcscat(lDialogString, L"\n\ +\n\ +\n\ +\n\ +
\n\ +

\n\ +\n\ +
\n\ +
\n"); + + wcscat(lDialogString, L"\n\ +\n\ +\n\ +\n\ +
\n\ +
\n\ +
\n\ +\n\ +\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 // Required for: FILE, fopen(), fclose(), fprintf(), feof(), fscanf(), snprintf(), vsprintf() [GuiLoadStyle(), GuiLoadIcons()] +#include // Required for: strlen() [GuiTextBox(), GuiValueBox()], memset(), memcpy() +#include // Required for: va_list, va_start(), vfprintf(), va_end() [TextFormat()] +#include // Required for: roundf() [GuiColorPicker()] +#include // 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 // 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 + + sscanf(buffer, "p %d %d 0x%x", &controlId, &propertyId, &propertyValue); + GuiSetStyle(controlId, propertyId, (int)propertyValue); + + } break; + case 'f': + { + // Style font: f + + 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 Binary files /dev/null and b/res/fnt/Aileron-Regular.otf differ diff --git a/res/fnt/Inter-Regular.ttf b/res/fnt/Inter-Regular.ttf new file mode 100644 index 0000000..b7aaca8 Binary files /dev/null and b/res/fnt/Inter-Regular.ttf differ diff --git a/res/mdl/Car.glb b/res/mdl/Car.glb new file mode 100644 index 0000000..c66144e Binary files /dev/null and b/res/mdl/Car.glb differ diff --git a/res/mdl/Car2.glb b/res/mdl/Car2.glb new file mode 100644 index 0000000..6de7136 Binary files /dev/null and b/res/mdl/Car2.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/concrete_retro_bollard_light.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/retro_bollard.001.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/retro_bollard.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/retro_bollard_detailed.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/security_bollard.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/simple_bollard.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/simple_bollard_short.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/square_bollard.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Bollards/square_bollard_reflectors.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_center_dirty.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_broken_edge_dirty.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Concrete Barriers/Jersey_barrier_dirty.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Construction_cone._thin.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Construction_cone._thin_rubber.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Construction_cone.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Construction_cone_rubber.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Traffic_cone_round.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Traffic_cone_round_rubber.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Traffic_cone_square.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Cones/Traffic_cone_square_rubber.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Textures/bollards.png 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Textures/concrete_barriers.png 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Textures/cones.png 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Textures/type_barriers.png 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 2 Barriers/Type_2_barrier_top_bottom_wood.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.fbx 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_2_planks.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.fbx 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_3_planks.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.fbx 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_broken_planks.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.fbx 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_plank.glb 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.fbx 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 Binary files /dev/null and b/res/mdl/Cones & Barriers/Type 3 Barriers/Type_3_barrier_stilts.glb 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 Binary files /dev/null and b/res/mdl/PSX-Waste/glb/barrel.glb 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 Binary files /dev/null and b/res/mdl/PSX-Waste/glb/cardboard-box.glb 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 Binary files /dev/null and b/res/mdl/PSX-Waste/glb/crumpled-can.glb 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 Binary files /dev/null and b/res/mdl/PSX-Waste/glb/dumpster.glb 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 Binary files /dev/null and b/res/mdl/PSX-Waste/glb/glass-bottle.glb 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 Binary files /dev/null and b/res/mdl/PSX-Waste/glb/trash-bag.glb 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 Binary files /dev/null and b/res/mdl/PSX-Waste/textures/barrel256x256.png 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 Binary files /dev/null and b/res/mdl/PSX-Waste/textures/can128x128.png 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 Binary files /dev/null and b/res/mdl/PSX-Waste/textures/cardboard-box256x256.png 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 Binary files /dev/null and b/res/mdl/PSX-Waste/textures/dumpster256x256.png 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 Binary files /dev/null and b/res/mdl/PSX-Waste/textures/glass-bottle128x128.png 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 Binary files /dev/null and b/res/mdl/PSX-Waste/textures/trash-bag128x128.png differ diff --git a/res/mdl/female_gunner_003.glb b/res/mdl/female_gunner_003.glb new file mode 100644 index 0000000..b8991c8 Binary files /dev/null and b/res/mdl/female_gunner_003.glb 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 Binary files /dev/null and b/res/mdl/female_gunner_003_posed.glb differ diff --git a/res/mdl/female_gunner_004.glb b/res/mdl/female_gunner_004.glb new file mode 100644 index 0000000..5bd973e Binary files /dev/null and b/res/mdl/female_gunner_004.glb differ diff --git a/res/mdl/lighthouse.glb b/res/mdl/lighthouse.glb new file mode 100644 index 0000000..379d17e Binary files /dev/null and b/res/mdl/lighthouse.glb differ diff --git a/res/mdl/male_casual.glb b/res/mdl/male_casual.glb new file mode 100644 index 0000000..ad0824a Binary files /dev/null and b/res/mdl/male_casual.glb differ diff --git a/res/mdl/map_prototype.glb b/res/mdl/map_prototype.glb new file mode 100644 index 0000000..40f8781 Binary files /dev/null and b/res/mdl/map_prototype.glb differ diff --git a/res/mdl/map_prototype_orig.glb b/res/mdl/map_prototype_orig.glb new file mode 100644 index 0000000..a3f0132 Binary files /dev/null and b/res/mdl/map_prototype_orig.glb differ diff --git a/res/save/scene.fgs b/res/save/scene.fgs new file mode 100644 index 0000000..191cfe7 Binary files /dev/null and b/res/save/scene.fgs 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 Binary files /dev/null and b/res/tex/moon.png differ diff --git a/res/tex/sand.png b/res/tex/sand.png new file mode 100644 index 0000000..c0b79e5 Binary files /dev/null and b/res/tex/sand.png differ diff --git a/res/tex/sand2.png b/res/tex/sand2.png new file mode 100644 index 0000000..ced7352 Binary files /dev/null and b/res/tex/sand2.png differ diff --git a/res/tex/sun.png b/res/tex/sun.png new file mode 100644 index 0000000..2031df2 Binary files /dev/null and b/res/tex/sun.png differ diff --git a/res/tex/water.png b/res/tex/water.png new file mode 100644 index 0000000..0c4fbb8 Binary files /dev/null and b/res/tex/water.png 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 -- cgit v1.2.3