init commit

This commit is contained in:
lily 2026-07-10 01:43:19 -04:00
commit e6ca5fce3b
119 changed files with 10326 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.gradle/
build/
run/
out/
.idea/
*.iml
.direnv/

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

41
NOTICE.md Normal file
View file

@ -0,0 +1,41 @@
# Third-party notices
Lattice is licensed under GPL-3.0-or-later (see LICENSE).
Portions of the NanoVG rendering layer (`xyz.meowing.lattice.render`) are
derived from OdinFabric, Copyright (c) 2023-2025 odtheking, licensed under the
BSD 3-Clause License, via Vexel (GPL-3.0-or-later).
BSD 3-Clause License
Copyright (c) 2023-2025, odtheking
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lattice supersedes and incorporates code from:
- Knit (https://github.com/StellariumMC/knit), GPL-3.0-or-later, a fork of OmniCore.
- Vexel (https://github.com/meowing-xyz/vexel-1.21), GPL-3.0-or-later.

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# Lattice
Unified UI framework for Minecraft clients on Fabric (Minecraft 26.1.2).
Lattice supersedes **Knit** (client base library) and **Vexel** (NanoVG UI
library), merging both into one dependency with a single event bus, one input
abstraction, and one screen stack.
## Features
- NanoVG vector renderer (rects, gradients, text, images, SVG, shadows)
- Constraint-based layout with scrollable containers and a full widget set
- Semantic theming (`Theme`) — retheme every widget in one place
- Time-based animations with easing and presets
- Single event bus with client lifecycle, tick, and render events
- `UIScreen` bridging vanilla screens, plus direct-draw HUD hooks
- Commodore command DSL over Brigadier, fluent `Component` text builder
- Tick and wall-clock schedulers, client/input utilities
## Documentation
Full docs live in [`docs/`](docs/README.md) — start with
[getting started](docs/getting-started.md), or the
[Knit/Vexel migration guide](docs/migration.md). Design rationale is in
[docs/architecture.md](docs/architecture.md).
A runnable demo mod lives in [`example/`](example/README.md): `/lattice`
opens a screen showing every widget; `/lattice hud`, `/lattice chat`, and
`/lattice greet` demo the render, text, and command APIs.
## Building
With Nix: `nix-shell --run './gradlew build'` (requires JDK 25 otherwise).
Publish to your local Maven repository with `./gradlew publish`
(artifact `xyz.meowing:lattice-26.1.2-fabric`).
Runtime NanoVG natives ship for linux, linux-arm64, windows, macos, and
macos-arm64.
## License
GPL-3.0-or-later. Portions of the rendering layer derive from OdinFabric
(BSD-3-Clause); see NOTICE.md.
# lattice

65
build.gradle Normal file
View file

@ -0,0 +1,65 @@
plugins {
id 'net.fabricmc.fabric-loom' version "${loom_version}"
id 'maven-publish'
id 'org.jetbrains.kotlin.jvm' version "${kotlin_version}"
}
version = "${mod_version}"
group = project.maven_group
base {
archivesName = "lattice-${project.minecraft_version}-fabric"
}
repositories {
mavenCentral()
}
// 26.1.2 is non-obfuscated: fabric-loom without remapping, no mappings dependency.
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}"
implementation "com.mojang:brigadier:1.2.9"
implementation "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}"
['linux', 'linux-arm64', 'windows', 'macos', 'macos-arm64'].each { platform ->
runtimeOnly "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}:natives-${platform}"
}
}
processResources {
inputs.property "version", project.version
filesMatching("fabric.mod.json") {
expand "version": inputs.properties.version
}
}
tasks.withType(JavaCompile).configureEach {
it.options.encoding = "UTF-8"
it.options.release = 25
}
java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
kotlin {
jvmToolchain(25)
}
publishing {
publications {
create("mavenJava", MavenPublication) {
artifactId = "lattice-${project.minecraft_version}-fabric"
from components.java
}
}
repositories {
mavenLocal()
}
}

16
docs/README.md Normal file
View file

@ -0,0 +1,16 @@
# Lattice documentation
- [Getting started](getting-started.md) — dependency setup, first screen.
- [Layout](layout.md) — the constraint system: `Pos`, `Size`, `Alignment`, `Offset`.
- [Components](components.md) — `Element`, `Box`, `Container`, `Rectangle`, `Text`, `SvgImage`, `Tooltip`.
- [Widgets](widgets.md) — the interactive widget set.
- [Theming](theming.md) — semantic color tokens.
- [Animation](animation.md) — animations, easings, presets.
- [Events & scheduling](events.md) — event bus, lifecycle events, schedulers.
- [Rendering](rendering.md) — the `Renderer` API and NanoVG backend.
- [Commands](commands.md) — the Commodore Brigadier DSL.
- [Text](text.md) — the `Component` builder DSL.
- [Migration](migration.md) — moving off Knit and Vexel.
- [Architecture](architecture.md) — design decisions and rationale.
A complete runnable demo lives in [`example/`](../example/).

39
docs/animation.md Normal file
View file

@ -0,0 +1,39 @@
# Animation
Animations are wall-clock based, updated once per frame by
`AnimationManager.update()` (the `Window` calls it). Registering a new
animation with the same element id and type replaces the running one.
## Element extensions (`xyz.meowing.lattice.animation`)
Low-level, animate anything:
```kotlin
element.animateFloat({ element.rotation }, { element.rotation = it }, 90f, 300, EasingType.EASE_OUT)
element.animateColor({ rect.backgroundColor }, { rect.backgroundColor = it }, target, 200)
element.animatePosition(endX, endY, 500, EasingType.EASE_IN_OUT) // animates constraints
element.animateSize(w, h, 300)
```
All return the running animation and accept an optional `onComplete`.
## Presets
```kotlin
element.fadeIn(300) // alpha via colors; recurses into children
element.fadeOut(300) { done() } // hides when finished
element.moveTo(x, y) // constraint move
element.scaleTo(w, h)
element.colorTo(argb) // background/text/svg tint by component type
element.slideIn(fromX = -width) // slides to its original position
element.bounceScale(1.2f) // press feedback
```
`slideIn`/`bounceScale` remember the element's original geometry on first
use and restore it.
## Easing
`EasingType.LINEAR`, `EASE_IN`, `EASE_OUT`, `EASE_IN_OUT` (quadratic).
Use `Theme.animFast/animNormal/animSlow` for consistent durations.

106
docs/architecture.md Normal file
View file

@ -0,0 +1,106 @@
# Architecture
Lattice replaces Knit and Vexel with one library. This document records the
major decisions and what changed relative to the two parents.
## Module layout
Single Gradle module; boundaries are packages with a strict dependency
direction (lower layers never import higher ones):
```
event/ EventBus, EventCall, lifecycle/tick/render events
client/ Minecraft wrappers: Client, Player, Chat, Clipboard, Desktop, Loader
input/ GLFW-backed Keys/Mouse/Keyboard, InputCode abstraction
render/ Renderer interface, NVGRenderer, Font/Image/Color/Gradient, Resolution, GLState
scheduler/ TickScheduler (game ticks), TimeScheduler (wall clock)
animation/ Animation core, manager, element extensions, presets
ui/ Element, Box, Window, UIScreen, layout enums, element events
ui/component Container, Rectangle, Text, SvgImage, Tooltip
ui/widget Button, CheckBox, ColorPicker, Dropdown, Keybind, NumberInput, Slider, Switch, TextInput
ui/theme Theme tokens
text/ Texts + TextBuilder (Component DSL)
command/ Commodore command DSL over Brigadier
util/ Number/String helpers
mixins/ (Java) Minecraft, GameRenderer, GlStateManager
```
A multi-module split was considered and rejected: the whole library is ~10k
lines and every layer targets the same Minecraft version; module plumbing
would outweigh the benefit.
## Key decisions
**One event bus.** Knit and Vexel each created their own `EventBus` instance,
so subscribers had to know which library fired an event. `Lattice.eventBus`
is the only bus. The unused class-hierarchy dispatch path (`checkHierarchy`)
was removed — subscribers register on concrete event classes.
**One screen class.** Knit's `KnitScreen` adapted vanilla `Screen` input and
Vexel's `VexelScreen` layered a `Window` plus render-event wiring on top.
`UIScreen` does both jobs in one class half the size.
**`Box` deduplicates scrolling.** Vexel's `Container` and `Rectangle` each
carried a full copy of the scroll/scrollbar/hover/padding logic (~300 lines
duplicated). `Box` owns padding, scrolling, scrollbar drawing and the
scroll-adjusted input handling; `Container` is `Box` with no drawing and
`Rectangle` is `Box` plus painting. The layout engine reads padding uniformly
through `Box` instead of type-switching on two unrelated classes.
**Int ARGB everywhere.** The renderer API mixed Int ARGB, `java.awt.Color`,
and Vexel's HSB `Color` class. The `Renderer` interface now takes Int ARGB
only; the HSB `Color` remains in `render/` as a utility (used by ColorPicker
internals and available to clients).
**Theme tokens.** Widgets previously hardcoded their palette. Widget
constructor defaults now read `Theme` (accent, surfaces, borders, text,
track/scrollbar, animation durations), initialized to the exact Vexel palette
so the rendered result is unchanged. Aether's theme system inspired the shape.
**Fabric-only, current-version-only.** Knit carried multi-loader preprocessor
blocks (Forge/NeoForge) and multi-version conditionals that this port could
never exercise. All of it is gone; `client.Loader` is a thin Fabric wrapper.
## Deleted dead code
- World rendering layer: `RenderContext` was all no-ops and
`MixinWorldRenderer` threw unconditionally in the 26.1.2 port. Gone, along
with `WorldRenderEvent` (14 event types that could never fire).
- `MixinClientCommonNetworkHandler`: empty mixin.
- `TickEvent.Server` and the server scheduler: nothing ever posted or ticked
them.
- Duplicate mixin sources: both parents shipped identical `.java` files in
`src/main/java` *and* `src/main/kotlin`, which broke `sourcesJar` on
Gradle 9.
- `Renderer.createImage(id)` parameter: ignored by the implementation.
- `scrollbarIgnorePadding`/`scrollbarCustomPadding`: duplicated
`scrollbarPadding`.
## Fixes
- **Linux natives**: Vexel shipped only `natives-macos-arm64` for NanoVG.
Lattice ships linux, linux-arm64, windows, macos, macos-arm64 (as Aether
does).
- **Framebuffer reflection cached**: `NVGRenderer.beginFrame` walked class
hierarchies reflectively every frame to find `directStateAccess`/`getFbo`;
the lookup now resolves once.
- **Render hook is lazy**: the GameRenderer mixin skips the whole NanoVG
frame when nothing subscribes to `RenderEvent.Gui`.
- **`Mouse.Scaled.y`** used the width-based scale factor (copy-paste bug in
Knit).
- **`TickScheduler` actually runs**: Knit's was never wired to a tick source;
Lattice registers it on `TickEvent.Start` on first use.
- **`TimeScheduler.repeat` honored `initialDelayMillis`** only in one
overload; the rewrite has one code path.
- **Animation preset leak**: `slideIn`/`bounceScale` stored original geometry
in global maps keyed by `hashCode()`, never cleaned; the bookkeeping now
lives on the element and dies with it.
- **`SvgImage` releases its NVG image** on destroy and no longer generates a
random UUID cache key per recolor.
## Threading model
The event bus uses `CopyOnWriteArrayList`/`ConcurrentHashMap`; posting is
safe from any thread, but UI elements must only be touched on the render
thread. `TimeScheduler` runs on a background pool (use it + `display()` /
`TickScheduler.post` to hop back).

42
docs/commands.md Normal file
View file

@ -0,0 +1,42 @@
# Commands (Commodore)
Commodore is a Kotlin DSL over Brigadier. Function parameters become command
arguments via reflection.
```kotlin
val command = Commodore("mymod", "mm") { // name + aliases
runs { Chat.fakeMessage("base command") } // /mymod
literal("teleport", "tp") { // /mymod teleport <x> <y> <z>
runs { x: Float, y: Float, z: Float ->
// typed params are parsed & suggested automatically
}
}
literal("say") {
runs { message: GreedyString -> // consumes the rest of the line
Chat.sendMessage(message.toString())
}
}
literal("mode") {
executable {
param("name") { suggests { listOf("fast", "slow") } }
runs { name: String -> setMode(name) }
}
}
}
// Fabric client commands:
ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ ->
command.register(dispatcher)
}
```
Built-in parsers: `String`, `GreedyString`, `Int`, `Long`, `Float`,
`Double`, `Boolean`. Optional parameters: give the lambda parameter a
nullable type (`Int?`) — trailing optionals may be omitted by the user.
Custom types: annotate a class with `@CommandParsable` (its primary
constructor parameters must themselves be parsable), or supply a
per-parameter parser with `param("p") { parser { input: String -> ... } }`.

62
docs/components.md Normal file
View file

@ -0,0 +1,62 @@
# Components
## Element
`Element<T>` is the self-typed base class: children, layout state, visibility,
focus, hover/press state, listeners, and fluent configuration (every setter
returns `T`). Subclasses implement `onRender(mouseX, mouseY)`.
Input listeners (fluent):
```kotlin
element
.onClick { event -> true } // return true to consume
.onRelease { event -> true }
.onScroll { event -> true }
.onMouseEnter { }
.onMouseExit { }
.onMouseMove { }
.onCharType { event -> true } // needs focus, or ignoreFocus()
.onValueChange { value -> } // widgets push their new value here
```
Tree management: `childOf(parent)`, `addChild(child)`, `destroy()` (recursive,
detaches from parent, clears listeners). Attach roots to a `Window` (screens
expose one as `window`).
Focus: clicking focuses an element and unfocuses the rest of the tree;
`setRequiresFocus()` makes clicks elsewhere drop focus; `ignoreFocus()` lets
an element receive key events without focus.
Debug: `enableDebugRendering()` draws hitboxes (cyan = idle, yellow =
hovered, orange = focused), recursively.
## Box
`Box<T>` adds padding and optional vertical scrolling (see
[layout](layout.md)). It is the base for anything that contains other
elements.
## Built-in components (`ui.component`)
- **`Container`** — invisible `Box`; pure layout/scroll region.
- **`Rectangle`** — `Box` that paints: background (solid or two-color
gradient), border (solid or gradient), per-corner radii
(`borderRadiusVarying`), hover/pressed background swap, `dropShadow()`,
`rotation`/`rotateTo()`.
- **`Text`** — single-line label; `Auto` size measures the string. Optional
shadow. Ignores the mouse.
- **`SvgImage`** — rasterized SVG with tint (`setSvgColor(argb)`) and
rotation; releases its GPU image on `destroy()`.
- **`Tooltip`** — attach with `element.addTooltip("text")`; fades in/out on
hover. Reposition with `setPosition(TooltipPosition.Top/Bottom/Left/Right)`.
## Window and UIScreen
`Window` is the root container: it drives rendering (`draw()`), routes input,
and cleans up (`cleanup()` destroys the tree and stops animations).
`UIScreen` bridges a vanilla `Screen` to a `Window`: it renders through the
`RenderEvent.Gui` NanoVG frame, translates vanilla input events, closes on
ESC when nothing consumes the key, and calls `afterInitialization()` once.
`display()` opens the screen on the next tick from any thread.

50
docs/events.md Normal file
View file

@ -0,0 +1,50 @@
# Events & scheduling
## Event bus
One global bus: `Lattice.eventBus`. Subscribers register on concrete event
classes; higher `priority` runs first; exceptions in handlers are caught and
logged.
```kotlin
val call = Lattice.eventBus.register<TickEvent.Start> { doSomething() }
call.unregister() // and call.register() to resubscribe
```
`post { EventObject() }` (supplier form) skips construction when nothing
listens. Cancellable events stop dispatch when `cancel()` is called and make
`post` return `true`.
## Built-in events
| Event | Fired |
| --- | --- |
| `ClientEvent.Start` / `Stop` | client run/stop |
| `TickEvent.Start` / `End` | each client tick |
| `RenderEvent.Gui` | every frame after vanilla GUI rendering, inside a prepared NanoVG frame (cancellable) |
Custom events: extend `Event` or `CancellableEvent` and `post` them on the
same bus.
## Schedulers
**`TickScheduler`** — game-tick based, runs on the client thread:
```kotlin
TickScheduler.post { } // next tick
TickScheduler.schedule(20) { } // in 20 ticks
val h = TickScheduler.repeat(100) { } // every 100 ticks
h.cancel()
TickScheduler.repeatDynamic({ nextInterval() }) { }
```
**`TimeScheduler`** — wall-clock, runs on a background thread pool (shut
down automatically on client stop):
```kotlin
TimeScheduler.schedule(500) { } // ms
TimeScheduler.repeat(1000, stopCondition = { done }) { }
```
TimeScheduler callbacks are **not** on the render thread — hop back with
`TickScheduler.post` before touching UI or game state.

71
docs/getting-started.md Normal file
View file

@ -0,0 +1,71 @@
# Getting started
## Dependency
Publish Lattice to your local Maven repository:
```sh
nix-shell --run './gradlew publish'
```
Then in your mod:
```groovy
repositories { mavenLocal() }
dependencies {
implementation "xyz.meowing:lattice-26.1.2-fabric:1.0.0"
}
```
Declare `"lattice": "*"` in your `fabric.mod.json` `depends` block so the
loader orders things correctly.
## A first screen
```kotlin
import xyz.meowing.lattice.ui.*
import xyz.meowing.lattice.ui.component.*
import xyz.meowing.lattice.ui.theme.Theme
import xyz.meowing.lattice.ui.widget.Button
class HelloScreen : UIScreen("Hello") {
override fun afterInitialization() {
val panel = Rectangle(backgroundColor = Theme.surface, borderRadius = 8f)
.setSizing(300f, Size.Pixels, 160f, Size.Pixels)
.setPositioning(Pos.ScreenCenter, Pos.ScreenCenter)
.childOf(window)
Text("Hello, Lattice", fontSize = 16f)
.setPositioning(0f, Pos.ParentCenter, 20f, Pos.ParentPixels)
.childOf(panel)
Button(text = "Close")
.setPositioning(Pos.ParentCenter, Pos.ParentCenter)
.onClick { onClose(); true }
.childOf(panel)
}
}
HelloScreen().display() // opens on the next tick, thread-safe
```
`afterInitialization` runs once; build your element tree there and attach
roots with `childOf(window)`. Input, resize, and cleanup are handled by
`UIScreen`. Override `onRenderGui()` for per-frame drawing after the window
renders, `onCloseGui()` for teardown.
## Drawing without a screen (HUDs)
```kotlin
import xyz.meowing.lattice.Lattice.eventBus
import xyz.meowing.lattice.Lattice.renderer
import xyz.meowing.lattice.event.RenderEvent
eventBus.register<RenderEvent.Gui> {
renderer.text("hud text", 10f, 10f, 12f, 0xFFFFFFFF.toInt())
}
```
The event fires inside a prepared NanoVG frame every GUI frame; draw with
`Lattice.renderer` directly. Coordinates are window pixels
(`render.Resolution.windowWidth/Height`).

54
docs/layout.md Normal file
View file

@ -0,0 +1,54 @@
# Layout
Every element carries an x/y position constraint, a width/height sizing mode,
an optional alignment, and an optional offset. Layout resolves top-down each
frame, with per-element caches invalidated on change or resize.
## Position — `Pos`
Set with `setPositioning(xVal, xPos, yVal, yPos)` or `setPositioning(xPos, yPos)`.
| Mode | Meaning |
| --- | --- |
| `ParentPixels` | `parent origin + padding + constraint` (the default) |
| `ParentPercent` | percentage across the parent's padded content area |
| `ParentCenter` | centered in the parent, constraint as extra offset |
| `ScreenPixels` / `ScreenPercent` / `ScreenCenter` | same, relative to the window |
| `AfterSibling` | after the previous visible sibling (per axis), constraint as gap |
| `MatchSibling` | same coordinate as the previous sibling |
## Size — `Size`
Set with `setSizing(w, wType, h, hType)`.
| Mode | Meaning |
| --- | --- |
| `Pixels` | fixed size |
| `Percent` | percentage of the parent's padded content area (value via the same setter) |
| `Auto` | fits children (components override this: `Text` measures its string, `Box` adds padding) |
| `Fill` | stretch to the parent's far edge minus visible later siblings |
Clamp `Auto` with `setMaxAutoSize(maxWidth, maxHeight)`.
## Alignment and offset
`alignLeft()/alignRight()/alignTop()/alignBottom()` (or
`setAlignment(x, y)`) snap to the parent's padded edges after positioning;
the position constraint then acts as an inset. `setOffset(x, y)` adds a
final pixel or percent offset.
## Padding and scrolling
Padding lives on `Box` (so on `Container`, `Rectangle`, and every widget
built from them) as `floatArrayOf(top, right, bottom, left)`. Children
position inside the padded area.
Set `scrollable = true` on a `Box` to get vertical scrolling with an
auto-hiding, draggable scrollbar (`scrollbarWidth/Color/Radius/Padding`).
Content is scissored to the padded area.
## Coordinate spaces
Element `x/y/width/height` are raw window pixels. `element.raw` and
`element.scaled` expose edges/center in raw and GUI-scaled coordinates
(`scaled` divides by the vanilla GUI scale factor).

49
docs/migration.md Normal file
View file

@ -0,0 +1,49 @@
# Migrating from Knit / Vexel
Lattice replaces both libraries with one dependency. Remove `knit` and
`vexel` from your build and `fabric.mod.json`, add `lattice`, then apply the
mappings below. Behavior is preserved unless noted in
[architecture.md](architecture.md#deleted-dead-code).
## API mapping
| Before | After |
| --- | --- |
| `Knit.EventBus` / `Vexel.eventBus` | `Lattice.eventBus` |
| `VexelScreen` + `KnitScreen` | `ui.UIScreen` |
| `VexelElement` | `ui.Element` |
| `VexelWindow` | `ui.Window` |
| `components.core.*` (Container, Rectangle, Text, SvgImage, Tooltip) | `ui.component.*` |
| `elements.*` (Button, Slider, …) | `ui.widget.*` |
| `components.base.enums.*` (Pos, Size, Alignment, Offset) | `ui.*` |
| `animations.*` (extensions, presets, EasingType) | `animation.*` |
| `api.RenderAPI` / `api.nvg.NVGRenderer` | `render.Renderer` / `render.NVGRenderer` |
| `api.style.{Color, Font, Gradient, Image}` | `render.*` |
| `GuiEvent.Render` | `event.RenderEvent.Gui` |
| `KnitClient` | `client.Client` (`client` property is now `minecraft`) |
| `KnitPlayer` / `KnitChat` / `KnitClipboard` / `KnitDesktop` / `KnitLoader` | `client.Player` / `Chat` / `Clipboard` / `Desktop` / `Loader` |
| `KnitKeys` / `KnitMouse` / `KnitKeyboard` / `KnitInputs` | `input.Keys` / `Mouse` / `Keyboard` / `Inputs` |
| `KnitKey` / `KnitMouseButton` / `KnitInputCode` | `input.Key` / `MouseButton` / `InputCode` |
| `KnitResolution` | `render.Resolution` |
| `KnitText` / `text.asKnit()` | `text.Texts` / `asBuilder()` |
| `api.command.*` (Commodore) | `command.*` (unchanged API) |
| `api.utils.{NumberUtils, StringUtils}` | `util.*` |
| `TickScheduler.Client.x` | `TickScheduler.x` (now actually ticks) |
## Behavioral changes
- **One event bus.** Anything registered on Vexel's bus moves to
`Lattice.eventBus`. `EventBus.post` no longer takes `checkHierarchy`
subscribe to concrete event classes.
- **Renderer takes Int ARGB only**: `dropShadow` and `createImage` no longer
accept `java.awt.Color`; `createImage` lost its unused `id` parameter.
- **Element listener aliases removed**: `mouseClickListeners`
`listeners.mouseClick` (the fluent `onClick`-style methods are unchanged).
- **Removed as dead code**: `WorldRenderEvent`/`RenderContext` (were no-op
stubs), `TickEvent.Server`, the server tick scheduler, timer API on
`TickScheduler`, `scrollbarIgnorePadding`/`scrollbarCustomPadding`
(use `scrollbarPadding`).
- **Assets**: `assets/vexel/*` (default font, checkmark, dropdown icons) are
now `assets/lattice/*`.
- **Widget palette** now flows through `ui.theme.Theme`; values are
unchanged, but you can retheme globally.

58
docs/rendering.md Normal file
View file

@ -0,0 +1,58 @@
# Rendering
`Lattice.renderer` implements `render.Renderer`, backed by NanoVG
(`NVGRenderer`). All colors are Int ARGB (`0xAARRGGBB`). Coordinates are raw
window pixels.
## Frame lifecycle
Inside `RenderEvent.Gui` or `UIScreen` a frame is already open — just draw.
Outside those hooks, wrap calls yourself:
```kotlin
renderer.beginFrame(Resolution.windowWidth.toFloat(), Resolution.windowHeight.toFloat())
// ... draw ...
renderer.endFrame()
```
`beginFrame` binds Minecraft's main framebuffer and saves GL state;
`endFrame` restores what vanilla expects.
## Drawing
```kotlin
renderer.rect(x, y, w, h, color, radius)
renderer.rect(x, y, w, h, color, tr, tl, br, bl) // per-corner radii
renderer.gradientRect(x, y, w, h, c1, c2, Gradient.TopToBottom, radius)
renderer.hollowRect(x, y, w, h, thickness, color, radius) // border only
renderer.circle(cx, cy, radius, color)
renderer.line(x1, y1, x2, y2, thickness, color)
renderer.dropShadow(x, y, w, h, blur, spread, shadowColor, radius)
renderer.text("hi", x, y, size, color) // Lattice.defaultFont
renderer.shadowedText("hi", x, y, size, color)
renderer.wrappedText("long...", x, y, maxWidth, size, color)
renderer.textWidth("hi", size) // measure
renderer.textBounds("long...", maxWidth, size) // [minX, minY, maxX, maxY]
renderer.svg("/assets/mymod/icon.svg", x, y, w, h) // after createImage
renderer.image(image, x, y, w, h, radius)
```
Transforms and clipping nest with `push()`/`pop()` and
`pushScissor(x, y, w, h)`/`popScissor()`; `translate`, `scale`, `rotate`
(radians), and `globalAlpha` apply to subsequent calls.
## Fonts and images
`Font(name, resourcePath | inputStream)` — TTFs registered lazily with
NanoVG. The bundled default is `Lattice.defaultFont`.
`createImage(path, width, height, tint)` loads a PNG/JPG or rasterizes an
SVG (tint replaces `currentColor`); images are reference-counted — pair with
`deleteImage`. Paths resolve as classpath resources or filesystem paths.
## Custom backends
`Renderer` is an interface; swap the implementation with
`Lattice.renderer = MyRenderer`.

36
docs/text.md Normal file
View file

@ -0,0 +1,36 @@
# Text
`xyz.meowing.lattice.text` wraps vanilla `Component` in a fluent builder.
```kotlin
import xyz.meowing.lattice.text.*
val message = buildText {
text("Lattice ") { color(0x4c87f9); bold() }
text("docs: ")
text("click") { green(); underlined(); openUrl("https://example.com") }
space()
text("hover") { yellow(); onHover("shown on hover") }
}
Chat.fakeMessage(message) // client-side chat line
```
Entry points:
- `Texts.literal("...")` / `"...".asText()` — single styled component.
- `Texts.fromFormatted("§ahello")` / `"...".asFormattedText()` — parse legacy
`§` codes.
- `component.asBuilder()` — wrap an existing vanilla `Component`.
- `buildText { }` — chain of parts (as above).
Builder styling: the 16 named colors (`red()`, `gold()`, …) or
`color(rgb)` / `color("#RRGGBB")`, plus `bold()`, `italic()`, `underlined()`,
`strikethrough()`, `obfuscated()`, `insertion()`.
Interactivity: `runCommand("/cmd")`, `suggestCommand`, `copyToClipboard`,
`openUrl`, `changePage`, `onHover(text | builder | HoverEvent)`.
Convert with `toVanilla()` (alias `build()`); `string()` flattens to plain
text. `ColorCodes` and `FormattingCodes` expose the legacy palette and
`§`-code helpers (`strip`, `translateAlternate`).

28
docs/theming.md Normal file
View file

@ -0,0 +1,28 @@
# Theming
`xyz.meowing.lattice.ui.theme.Theme` holds the semantic tokens every widget
default reads:
| Token | Role |
| --- | --- |
| `accent` | primary accent (slider fill, switch on-state, checkmark, dropdown icon) |
| `accentSelection` | text selection highlight |
| `background` / `backgroundHover` / `backgroundPressed` | interactive surfaces |
| `backgroundDark` / `backgroundDisabled` | filled/disabled states |
| `surface` / `surfaceBorder` | panels and tooltips |
| `border` | default widget border |
| `text` / `textMuted` | foreground text |
| `track` / `scrollbar` | slider/switch tracks, scrollbars |
| `animFast` / `animNormal` / `animSlow` | shared animation durations (ms) |
All colors are Int ARGB. Defaults match the original Vexel palette.
Tokens are read **at construction time** — widget constructor defaults
evaluate `Theme.x` when the widget is created. So:
```kotlin
Theme.accent = 0xFF10B981.toInt() // before building UI: everything green
```
Retheming a live screen means rebuilding it (see the "Cycle accent" row in
the example mod). Explicit constructor arguments always win over tokens.

40
docs/widgets.md Normal file
View file

@ -0,0 +1,40 @@
# Widgets
All widgets live in `xyz.meowing.lattice.ui.widget`, extend `Element`, take
their colors from [`Theme`](theming.md) by default, and report changes
through `onValueChange { value -> }`.
| Widget | Value pushed | Notes |
| --- | --- | --- |
| `Button` | — | `onClick`; text/hover/pressed colors, fluent styling |
| `Switch` | `Boolean` | animated thumb; `setEnabled(value, animated, silent)` |
| `CheckBox` | `Boolean` | SVG checkmark; `setChecked(value, animated, silent)` |
| `Slider` | `Float` | `minValue`/`maxValue`/`step`, draggable + click-to-set |
| `Dropdown` | `Int` (index) | floating popup list; `options: List<String>` |
| `TextInput` | `String` | caret, selection, clipboard (ctrl-C/X/V), placeholder |
| `NumberInput` | number as `String` | `allowDecimals`, `allowNegative` |
| `Keybind` | `Int` (key code) | click, then press a key; ESC clears |
| `ColorPicker` | `java.awt.Color` | floating HSB panel with alpha + hex field |
Typical usage:
```kotlin
Slider(value = 50f, minValue = 0f, maxValue = 100f, step = 1f)
.onValueChange { v -> config.volume = v as Float }
.childOf(row)
Dropdown(options = listOf("A", "B", "C"))
.setSizing(150f, Size.Pixels, 26f, Size.Pixels)
.onValueChange { i -> config.mode = i as Int }
.childOf(row)
```
`onValueChange` payloads are typed `Any`; cast to the widget's value type
from the table above.
Widgets set sensible default sizes in their constructors (e.g. `Slider`
200×24, `CheckBox` 20×20); override with `setSizing`. `TextInput` and
`NumberInput` need explicit sizing.
Composite widgets expose their parts (`Button.background`,
`Button.innerText`, `Slider.thumb`, …) for fine-grained styling.

22
example/README.md Normal file
View file

@ -0,0 +1,22 @@
# Lattice Example
A small Fabric client mod that exercises the framework end to end. Built as
part of the root Gradle build (`./gradlew build`).
In game:
- `/lattice` (alias `/latticedemo`) — opens the demo screen: every built-in
widget in a scrollable panel, tooltips, `Size.Fill` layout, bounce/fade
animations, and a button that retheme-and-rebuilds via `Theme`.
- `/lattice hud` — toggles a HUD overlay drawn directly with the renderer
from `RenderEvent.Gui` (no screen involved).
- `/lattice chat` — chat message built with the `buildText { }` DSL
(colors, hover, click-to-run).
- `/lattice greet <name> <times>` — Commodore parsing typed lambda
parameters as command arguments, spaced out with `TickScheduler`.
Source map:
- `LatticeExample.kt` — entrypoint, command tree, text DSL.
- `DemoScreen.kt``UIScreen`, layout constraints, all widgets, theming.
- `DemoHud.kt` — event-bus render hook with direct renderer calls.

47
example/build.gradle Normal file
View file

@ -0,0 +1,47 @@
plugins {
id 'net.fabricmc.fabric-loom'
id 'org.jetbrains.kotlin.jvm'
}
version = rootProject.version
group = rootProject.group
base {
archivesName = "lattice-example-${rootProject.minecraft_version}-fabric"
}
repositories {
mavenCentral()
}
dependencies {
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "net.fabricmc:fabric-language-kotlin:${rootProject.fabric_kotlin_version}"
implementation "org.lwjgl:lwjgl-nanovg:${rootProject.nanovg_version}"
implementation project(':')
}
processResources {
inputs.property "version", project.version
filesMatching("fabric.mod.json") {
expand "version": inputs.properties.version
}
}
tasks.withType(JavaCompile).configureEach {
it.options.encoding = "UTF-8"
it.options.release = 25
}
java {
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
kotlin {
jvmToolchain(25)
}

View file

@ -0,0 +1,22 @@
package xyz.meowing.lattice.example
import xyz.meowing.lattice.Lattice.eventBus
import xyz.meowing.lattice.Lattice.renderer
import xyz.meowing.lattice.client.Client.minecraft
import xyz.meowing.lattice.event.RenderEvent
import xyz.meowing.lattice.ui.theme.Theme
/** Draws directly with the renderer every frame — no screen needed. */
object DemoHud {
var enabled = false
fun init() {
eventBus.register<RenderEvent.Gui> {
if (!enabled || minecraft.screen is DemoScreen) return@register
renderer.rect(10f, 10f, 4f, 34f, Theme.accent, 2f)
renderer.shadowedText("Lattice", 22f, 12f, 16f, 0xFFFFFFFF.toInt())
renderer.text("/lattice opens the demo", 22f, 30f, 11f, Theme.textMuted)
}
}
}

View file

@ -0,0 +1,172 @@
package xyz.meowing.lattice.example
import xyz.meowing.lattice.animation.bounceScale
import xyz.meowing.lattice.client.Chat
import xyz.meowing.lattice.ui.Element
import xyz.meowing.lattice.ui.Pos
import xyz.meowing.lattice.ui.Size
import xyz.meowing.lattice.ui.UIScreen
import xyz.meowing.lattice.ui.component.Rectangle
import xyz.meowing.lattice.ui.component.Text
import xyz.meowing.lattice.ui.theme.Theme
import xyz.meowing.lattice.ui.widget.Button
import xyz.meowing.lattice.ui.widget.CheckBox
import xyz.meowing.lattice.ui.widget.ColorPicker
import xyz.meowing.lattice.ui.widget.Dropdown
import xyz.meowing.lattice.ui.widget.Keybind
import xyz.meowing.lattice.ui.widget.NumberInput
import xyz.meowing.lattice.ui.widget.Slider
import xyz.meowing.lattice.ui.widget.Switch
import xyz.meowing.lattice.ui.widget.TextInput
/** Showcases every built-in widget, the layout system, theming, and animations. */
class DemoScreen : UIScreen("Lattice Demo") {
override fun afterInitialization() {
val panel = Rectangle(
backgroundColor = 0xF0141414.toInt(),
borderColor = Theme.border,
borderRadius = 10f,
borderThickness = 1f,
padding = floatArrayOf(14f, 14f, 14f, 14f),
widthType = Size.Pixels,
heightType = Size.Pixels
)
.setSizing(440f, Size.Pixels, 440f, Size.Pixels)
.setPositioning(Pos.ScreenCenter, Pos.ScreenCenter)
.dropShadow(shadowBlur = 40f)
.childOf(window)
Text("Lattice Demo", fontSize = 18f, shadowEnabled = true)
.setPositioning(0f, Pos.ParentCenter, 0f, Pos.ParentPixels)
.childOf(panel)
// Size.Fill stretches to the remaining panel height below the title.
val content = Rectangle(
backgroundColor = 0x00000000,
padding = floatArrayOf(4f, 10f, 4f, 4f),
widthType = Size.Percent,
heightType = Size.Fill,
scrollable = true
)
.setSizing(100f, Size.Percent, 0f, Size.Fill)
.setPositioning(0f, Pos.ParentPixels, 10f, Pos.AfterSibling)
.childOf(panel)
row(content, "Button") {
val button = Button(text = "Click me")
.addTooltip("Tooltips fade in on hover")
.alignRight()
.childOf(it)
button.onClick {
button.bounceScale()
Chat.fakeMessage("Button clicked!")
true
}
}
row(content, "Switch (toggles the HUD)") {
Switch()
.onValueChange { enabled -> DemoHud.enabled = enabled as Boolean }
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(it)
.setEnabled(DemoHud.enabled, animated = false, silent = true)
}
row(content, "CheckBox") {
CheckBox()
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(it)
}
row(content, "Slider") { r ->
val label = Text("50", textColor = Theme.textMuted, fontSize = 12f)
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.childOf(r)
Slider(value = 50f, minValue = 0f, maxValue = 100f, step = 1f)
.onValueChange { value -> label.text = "${(value as Float).toInt()}" }
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(r)
label.setPositioning(-215f, Pos.ParentPixels, 0f, Pos.ParentCenter).alignRight()
}
row(content, "Dropdown") {
Dropdown(options = listOf("Linear", "Ease in", "Ease out", "Ease in/out"))
.setSizing(150f, Size.Pixels, 26f, Size.Pixels)
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(it)
}
row(content, "TextInput") {
TextInput(placeholder = "Type here…")
.setSizing(150f, Size.Pixels, 26f, Size.Pixels)
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(it)
}
row(content, "NumberInput") {
NumberInput(initialValue = 42, allowNegative = true)
.setSizing(80f, Size.Pixels, 26f, Size.Pixels)
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(it)
}
row(content, "Keybind") {
Keybind()
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(it)
}
row(content, "ColorPicker") {
ColorPicker(initialColor = java.awt.Color(Theme.accent, true))
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.alignRight()
.childOf(it)
}
row(content, "Theme") {
Button(text = "Cycle accent")
.onClick {
Theme.accent = when (Theme.accent) {
0xFF4c87f9.toInt() -> 0xFF10B981.toInt()
0xFF10B981.toInt() -> 0xFFD32F2F.toInt()
else -> 0xFF4c87f9.toInt()
}
// Theme tokens apply at construction, so rebuild the screen.
onClose()
DemoScreen().display()
true
}
.alignRight()
.childOf(it)
}
}
private fun row(parent: Element<*>, label: String, build: (Rectangle) -> Unit) {
val row = Rectangle(
backgroundColor = Theme.surface,
borderRadius = 6f,
padding = floatArrayOf(8f, 10f, 8f, 10f),
widthType = Size.Percent,
heightType = Size.Pixels
)
.setSizing(96f, Size.Percent, 42f, Size.Pixels)
.setPositioning(0f, Pos.ParentPixels, 6f, Pos.AfterSibling)
.childOf(parent)
Text(label, fontSize = 13f)
.setPositioning(0f, Pos.ParentPixels, 0f, Pos.ParentCenter)
.childOf(row)
build(row)
}
}

View file

@ -0,0 +1,55 @@
package xyz.meowing.lattice.example
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
import xyz.meowing.lattice.client.Chat
import xyz.meowing.lattice.command.Commodore
import xyz.meowing.lattice.scheduler.TickScheduler
import xyz.meowing.lattice.text.buildText
object LatticeExample : ClientModInitializer {
private val command = Commodore("lattice", "latticedemo") {
runs { DemoScreen().display() }
literal("hud") {
runs {
DemoHud.enabled = !DemoHud.enabled
Chat.fakeMessage("Lattice HUD ${if (DemoHud.enabled) "enabled" else "disabled"}")
}
}
literal("chat") {
runs {
Chat.fakeMessage(buildText {
text("Lattice ") { color(0x4c87f9); bold() }
text("text DSL: ")
text("hover me") {
yellow()
underlined()
onHover("Built with buildText { }")
}
text(" or ")
text("click me") { green(); runCommand("/lattice") }
})
}
}
// Typed lambda parameters become command arguments, parsed by Commodore.
literal("greet") {
runs { name: String, times: Int ->
repeat(times.coerceIn(1, 5)) { i ->
TickScheduler.schedule((i * 10).toLong()) {
Chat.fakeMessage("Hello, $name!")
}
}
}
}
}
override fun onInitializeClient() {
DemoHud.init()
ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ ->
command.register(dispatcher)
}
}
}

View file

@ -0,0 +1,25 @@
{
"schemaVersion": 1,
"id": "lattice-example",
"version": "${version}",
"name": "Lattice Example",
"description": "Example mod showing off the Lattice UI framework: screens, widgets, theming, animations, HUD rendering, and the command DSL.",
"authors": ["Aurielyn"],
"license": "GPL-3.0-or-later",
"environment": "client",
"entrypoints": {
"client": [
{
"adapter": "kotlin",
"value": "xyz.meowing.lattice.example.LatticeExample"
}
]
},
"depends": {
"fabricloader": ">=0.15.11",
"minecraft": "~26.1.2",
"fabric-api": "*",
"fabric-language-kotlin": "*",
"lattice": "*"
}
}

21
gradle.properties Normal file
View file

@ -0,0 +1,21 @@
org.gradle.jvmargs=-Xmx4G
org.gradle.parallel=true
org.gradle.configuration-cache=false
loom.ignoreDependencyLoomVersionValidation=true
# Fabric / Minecraft (non-obfuscated)
minecraft_version=26.1.2
loader_version=0.19.3
loom_version=1.16-SNAPSHOT
# Mod Properties
mod_id=lattice
mod_name=Lattice
mod_version=1.0.0
maven_group=xyz.meowing
# Dependencies
fabric_api_version=0.150.0+26.1.2
fabric_kotlin_version=1.13.12+kotlin.2.4.0
kotlin_version=2.4.0
nanovg_version=3.4.1

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
gradlew vendored Executable file
View file

@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

13
settings.gradle Normal file
View file

@ -0,0 +1,13 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = 'lattice'
include 'example'

15
shell.nix Normal file
View file

@ -0,0 +1,15 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = [ pkgs.jdk25 ];
JAVA_HOME = pkgs.jdk25.home;
# NanoVG/GLFW natives need GL and X11/Wayland libraries at runtime.
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
pkgs.libGL
pkgs.glfw
pkgs.libx11
pkgs.wayland
];
}

View file

@ -0,0 +1,25 @@
package xyz.meowing.lattice.mixins;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.renderer.GameRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import xyz.meowing.lattice.Lattice;
import xyz.meowing.lattice.event.RenderEvent;
import xyz.meowing.lattice.render.Resolution;
@Mixin(GameRenderer.class)
public class MixinGameRenderer {
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/render/GuiRenderer;endFrame()V", shift = At.Shift.AFTER), cancellable = true)
public void lattice$onRenderGui(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) {
if (!Lattice.getEventBus().hasSubscribers(RenderEvent.Gui.class)) return;
Lattice.getRenderer().beginFrame(Resolution.getWindowWidth(), Resolution.getWindowHeight());
boolean cancelled = Lattice.getEventBus().post(new RenderEvent.Gui());
Lattice.getRenderer().endFrame();
if (cancelled) ci.cancel();
}
}

View file

@ -0,0 +1,16 @@
package xyz.meowing.lattice.mixins;
import com.mojang.blaze3d.opengl.GlStateManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import xyz.meowing.lattice.render.GLState;
@Mixin(GlStateManager.class)
public class MixinGlStateManager {
@Inject(method = "_bindTexture", at = @At("HEAD"), remap = false)
private static void lattice$onBindTexture(int texture, CallbackInfo ci) {
GLState.setPreviousBoundTexture(texture);
}
}

View file

@ -0,0 +1,33 @@
package xyz.meowing.lattice.mixins;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import xyz.meowing.lattice.Lattice;
import xyz.meowing.lattice.event.ClientEvent;
import xyz.meowing.lattice.event.TickEvent;
@Mixin(Minecraft.class)
public class MixinMinecraft {
@Inject(method = "tick", at = @At("HEAD"))
private void lattice$onStartTick(CallbackInfo info) {
Lattice.getEventBus().post(new TickEvent.Start());
}
@Inject(method = "tick", at = @At("RETURN"))
private void lattice$onEndTick(CallbackInfo info) {
Lattice.getEventBus().post(new TickEvent.End());
}
@Inject(method = "run", at = @At("HEAD"))
private void lattice$onClientStart(CallbackInfo ci) {
Lattice.getEventBus().post(new ClientEvent.Start());
}
@Inject(method = "stop", at = @At("RETURN"))
private void lattice$onClientStop(CallbackInfo ci) {
Lattice.getEventBus().post(new ClientEvent.Stop());
}
}

View file

@ -0,0 +1,30 @@
package xyz.meowing.lattice
import net.minecraft.resources.Identifier
import xyz.meowing.lattice.client.Client.minecraft
import xyz.meowing.lattice.event.EventBus
import xyz.meowing.lattice.render.Font
import xyz.meowing.lattice.render.NVGRenderer
import xyz.meowing.lattice.render.Renderer
object Lattice {
const val MOD_ID = "lattice"
@JvmStatic
val eventBus = EventBus()
@JvmStatic
val defaultFont: Font by lazy {
val id = Identifier.fromNamespaceAndPath(MOD_ID, "font.ttf")
Font("Default", minecraft.resourceManager.getResourceStack(id).first().open())
}
private var _renderer: Renderer? = null
@JvmStatic
var renderer: Renderer
get() = _renderer ?: NVGRenderer
set(value) {
_renderer = value
}
}

View file

@ -0,0 +1,118 @@
package xyz.meowing.lattice.animation
enum class EasingType {
LINEAR,
EASE_IN,
EASE_OUT,
EASE_IN_OUT,
}
enum class AnimationType {
POSITION,
SIZE,
COLOR,
ALPHA,
CUSTOM,
}
data class AnimationTarget<T>(
val startValue: T,
val endValue: T,
val setter: (T) -> Unit
)
abstract class Animation<T>(
val target: AnimationTarget<T>,
val duration: Long,
val type: EasingType,
val animationType: AnimationType,
val elementId: String,
val onComplete: (() -> Unit)? = null
) {
private var startTime: Long = 0
private var isStarted = false
var isCompleted = false
private set
fun start() {
if (!isStarted) {
startTime = System.currentTimeMillis()
isStarted = true
AnimationManager.register(this)
}
}
fun update(): Boolean {
if (!isStarted || isCompleted) return false
val elapsed = System.currentTimeMillis() - startTime
val progress = (elapsed.toFloat() / duration).coerceIn(0f, 1f)
val easedProgress = applyEasing(progress)
target.setter(interpolate(target.startValue, target.endValue, easedProgress))
if (progress >= 1f) {
isCompleted = true
onComplete?.invoke()
return true
}
return false
}
private fun applyEasing(t: Float): Float = when (type) {
EasingType.LINEAR -> t
EasingType.EASE_IN -> t * t
EasingType.EASE_OUT -> 1f - (1f - t) * (1f - t)
EasingType.EASE_IN_OUT -> if (t < 0.5f) 2f * t * t else 1f - 2f * (1f - t) * (1f - t)
}
abstract fun interpolate(start: T, end: T, progress: Float): T
}
class FloatAnimation(
target: AnimationTarget<Float>,
duration: Long,
type: EasingType,
animationType: AnimationType,
elementId: String,
onComplete: (() -> Unit)? = null
) : Animation<Float>(target, duration, type, animationType, elementId, onComplete) {
override fun interpolate(start: Float, end: Float, progress: Float): Float = start + (end - start) * progress
}
class ColorAnimation(
target: AnimationTarget<Int>,
duration: Long,
type: EasingType,
elementId: String,
onComplete: (() -> Unit)? = null
) : Animation<Int>(target, duration, type, AnimationType.COLOR, elementId, onComplete) {
override fun interpolate(start: Int, end: Int, progress: Float): Int {
val a = channel(start shr 24, end shr 24, progress)
val r = channel(start shr 16, end shr 16, progress)
val g = channel(start shr 8, end shr 8, progress)
val b = channel(start, end, progress)
return (a shl 24) or (r shl 16) or (g shl 8) or b
}
private fun channel(start: Int, end: Int, progress: Float): Int {
val s = start and 0xFF
val e = end and 0xFF
return (s + (e - s) * progress).toInt()
}
}
class VectorAnimation(
target: AnimationTarget<Pair<Float, Float>>,
duration: Long,
type: EasingType,
animationType: AnimationType,
elementId: String,
onComplete: (() -> Unit)? = null
) : Animation<Pair<Float, Float>>(target, duration, type, animationType, elementId, onComplete) {
override fun interpolate(start: Pair<Float, Float>, end: Pair<Float, Float>, progress: Float): Pair<Float, Float> {
return (start.first + (end.first - start.first) * progress) to
(start.second + (end.second - start.second) * progress)
}
}

View file

@ -0,0 +1,29 @@
package xyz.meowing.lattice.animation
object AnimationManager {
private val activeAnimations = mutableListOf<Animation<*>>()
val activeCount: Int
get() = activeAnimations.size
fun register(animation: Animation<*>) {
stopAnimations(animation.elementId, animation.animationType)
activeAnimations.add(animation)
}
fun update() {
val activeList = activeAnimations.toList()
val completed = activeList.filter { it.update() }
activeAnimations.removeAll(completed)
}
fun clear() {
activeAnimations.clear()
}
fun stopAnimations(elementId: String, animationType: AnimationType? = null) {
activeAnimations.removeAll {
it.elementId == elementId && (animationType == null || it.animationType == animationType)
}
}
}

View file

@ -0,0 +1,70 @@
package xyz.meowing.lattice.animation
import xyz.meowing.lattice.ui.Element
fun <T : Element<T>> T.animateFloat(
getter: () -> Float,
setter: (Float) -> Unit,
endValue: Float,
duration: Long,
type: EasingType = EasingType.LINEAR,
animationType: AnimationType = AnimationType.CUSTOM,
onComplete: (() -> Unit)? = null
): FloatAnimation {
val target = AnimationTarget(getter(), endValue, setter)
val animation = FloatAnimation(target, duration, type, animationType, "${hashCode()}+$endValue", onComplete)
animation.start()
return animation
}
fun <T : Element<T>> T.animateColor(
getter: () -> Int,
setter: (Int) -> Unit,
endValue: Int,
duration: Long,
type: EasingType = EasingType.LINEAR,
onComplete: (() -> Unit)? = null
): ColorAnimation {
val target = AnimationTarget(getter(), endValue, setter)
val animation = ColorAnimation(target, duration, type, "${hashCode()}+$endValue", onComplete)
animation.start()
return animation
}
fun <T : Element<T>> T.animatePosition(
endX: Float,
endY: Float,
duration: Long,
type: EasingType = EasingType.LINEAR,
onComplete: (() -> Unit)? = null
): VectorAnimation {
val target = AnimationTarget(
xConstraint to yConstraint,
endX to endY
) { (x, y) ->
xConstraint = x
yConstraint = y
}
val animation = VectorAnimation(target, duration, type, AnimationType.POSITION, "${hashCode()}:pos", onComplete)
animation.start()
return animation
}
fun <T : Element<T>> T.animateSize(
endWidth: Float,
endHeight: Float,
duration: Long,
type: EasingType = EasingType.LINEAR,
onComplete: (() -> Unit)? = null
): VectorAnimation {
val target = AnimationTarget(
width to height,
endWidth to endHeight
) { (w, h) ->
width = w
height = h
}
val animation = VectorAnimation(target, duration, type, AnimationType.SIZE, "${hashCode()}:size", onComplete)
animation.start()
return animation
}

View file

@ -0,0 +1,164 @@
package xyz.meowing.lattice.animation
import xyz.meowing.lattice.ui.Element
import xyz.meowing.lattice.ui.component.Rectangle
import xyz.meowing.lattice.ui.component.SvgImage
import xyz.meowing.lattice.ui.component.Text
fun <T : Element<T>> T.fadeIn(
duration: Long = 300,
type: EasingType = EasingType.EASE_OUT,
includeChildren: Boolean = true,
onComplete: (() -> Unit)? = null
): T {
visible = true
if (includeChildren) {
children.forEach { child ->
when (child) {
is Rectangle -> child.fadeIn(duration, type, includeChildren)
is Text -> child.fadeIn(duration, type, includeChildren)
is SvgImage -> child.fadeIn(duration, type, includeChildren)
}
}
}
when (this) {
is Rectangle -> {
val targetBg = (backgroundColor and 0x00FFFFFF) or (255 shl 24)
val targetBorder = (borderColor and 0x00FFFFFF) or (255 shl 24)
backgroundColor = backgroundColor and 0x00FFFFFF
borderColor = borderColor and 0x00FFFFFF
animateColor({ backgroundColor }, { backgroundColor = it }, targetBg, duration, type, onComplete)
animateColor({ borderColor }, { borderColor = it }, targetBorder, duration, type)
}
is Text -> {
val target = (textColor and 0x00FFFFFF) or (255 shl 24)
textColor = textColor and 0x00FFFFFF
animateColor({ textColor }, { textColor = it }, target, duration, type, onComplete)
}
is SvgImage -> {
val target = (color and 0x00FFFFFF) or (255 shl 24)
animateColor({ color }, { setSvgColor(it) }, target, duration, type, onComplete)
}
else -> animateFloat({ 0f }, {}, 1f, duration, type, AnimationType.ALPHA, onComplete)
}
return this
}
fun <T : Element<T>> T.fadeOut(
duration: Long = 300,
type: EasingType = EasingType.EASE_IN,
includeChildren: Boolean = true,
onComplete: (() -> Unit)? = null
): T {
if (includeChildren) {
children.forEach { child ->
when (child) {
is Rectangle -> child.fadeOut(duration, type, includeChildren)
is Text -> child.fadeOut(duration, type, includeChildren)
is SvgImage -> child.fadeOut(duration, type, includeChildren)
}
}
}
when (this) {
is Rectangle -> {
animateColor({ backgroundColor }, { backgroundColor = it }, backgroundColor and 0x00FFFFFF, duration, type) {
visible = false
onComplete?.invoke()
}
animateColor({ borderColor }, { borderColor = it }, borderColor and 0x00FFFFFF, duration, type)
}
is Text -> {
animateColor({ textColor }, { textColor = it }, textColor and 0x00FFFFFF, duration, type) {
visible = false
onComplete?.invoke()
}
}
is SvgImage -> {
animateColor({ color }, { setSvgColor(it) }, color and 0x00FFFFFF, duration, type) {
visible = false
onComplete?.invoke()
}
}
else -> animateFloat({ 1f }, {}, 0f, duration, type, AnimationType.ALPHA) {
visible = false
onComplete?.invoke()
}
}
return this
}
fun <T : Element<T>> T.moveTo(
x: Float,
y: Float,
duration: Long = 500,
type: EasingType = EasingType.EASE_OUT,
onComplete: (() -> Unit)? = null
): T {
animatePosition(x, y, duration, type, onComplete)
return this
}
fun <T : Element<T>> T.scaleTo(
width: Float,
height: Float,
duration: Long = 300,
type: EasingType = EasingType.EASE_OUT,
onComplete: (() -> Unit)? = null
): T {
animateSize(width, height, duration, type, onComplete)
return this
}
fun <T : Element<T>> T.colorTo(
color: Int,
duration: Long = 300,
type: EasingType = EasingType.LINEAR,
onComplete: (() -> Unit)? = null
): T {
when (this) {
is Rectangle -> animateColor({ backgroundColor }, { backgroundColor = it }, color, duration, type, onComplete)
is Text -> animateColor({ textColor }, { textColor = it }, color, duration, type, onComplete)
is SvgImage -> animateColor({ this.color }, { setSvgColor(it) }, color, duration, type, onComplete)
}
return this
}
fun <T : Element<T>> T.slideIn(
fromX: Float = -width,
fromY: Float = 0f,
duration: Long = 500,
type: EasingType = EasingType.EASE_OUT,
onComplete: (() -> Unit)? = null
): T {
if (presetOriginalPosition == null) presetOriginalPosition = xConstraint to yConstraint
val (targetX, targetY) = presetOriginalPosition!!
xConstraint = fromX
yConstraint = fromY
visible = true
animatePosition(targetX, targetY, duration, type, onComplete)
return this
}
fun <T : Element<T>> T.bounceScale(
scale: Float = 1.2f,
duration: Long = 200,
onComplete: (() -> Unit)? = null
): T {
AnimationManager.stopAnimations("${hashCode()}:size", AnimationType.SIZE)
if (presetOriginalSize == null) presetOriginalSize = width to height
val (originalWidth, originalHeight) = presetOriginalSize!!
animateSize(originalWidth * scale, originalHeight * scale, duration / 2, EasingType.EASE_OUT) {
animateSize(originalWidth, originalHeight, duration / 2, EasingType.EASE_IN, onComplete)
}
return this
}

View file

@ -0,0 +1,51 @@
package xyz.meowing.lattice.client
import net.minecraft.network.chat.Component
import xyz.meowing.lattice.client.Client.minecraft
import xyz.meowing.lattice.client.Client.player
import xyz.meowing.lattice.text.TextBuilder
import kotlin.math.roundToInt
object Chat {
private const val CHAT_WIDTH = 320
@JvmStatic
fun sendMessage(message: String) {
player?.connection?.sendChat(message)
}
@JvmStatic
fun sendCommand(command: String) {
player?.connection?.sendCommand(command.removePrefix("/"))
}
@JvmStatic
fun fakeMessage(message: Component) {
minecraft.gui.getChat().addClientSystemMessage(message)
}
@JvmStatic
fun fakeMessage(message: String) {
fakeMessage(Component.literal(message))
}
@JvmStatic
fun fakeMessage(message: TextBuilder) {
fakeMessage(message.toVanilla())
}
@JvmStatic
fun getChatBreak(): String {
val dashWidth = minecraft.font.width("-")
return "-".repeat(CHAT_WIDTH / dashWidth)
}
@JvmStatic
fun getCenteredText(text: String): String {
val textWidth = minecraft.font.width(text)
if (textWidth >= CHAT_WIDTH) return text
val spaceWidth = minecraft.font.width(" ")
val padding = ((CHAT_WIDTH - textWidth) / 2f / spaceWidth).roundToInt()
return " ".repeat(padding) + text
}
}

View file

@ -0,0 +1,73 @@
package xyz.meowing.lattice.client
import net.minecraft.SharedConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.client.multiplayer.PlayerInfo
import net.minecraft.client.player.LocalPlayer
import net.minecraft.network.chat.Component
import net.minecraft.world.level.GameType
import net.minecraft.world.scores.DisplaySlot
import java.nio.file.Path
object Client {
private val tabListComparator: Comparator<PlayerInfo> = compareBy(
{ it.getGameMode() == GameType.SPECTATOR },
{ it.getTeam()?.name ?: "" },
{ it.getProfile().name.lowercase() }
)
@JvmStatic
val minecraft: Minecraft = Minecraft.getInstance()
@JvmStatic
val world: ClientLevel? get() = minecraft.level
@JvmStatic
val player: LocalPlayer? get() = minecraft.player
@JvmStatic
val minecraftVersion: String by lazy {
SharedConstants.getCurrentVersion().name()
}
@JvmStatic
val gameDirectory: Path get() = minecraft.gameDirectory.toPath()
@JvmStatic
val configDirectory: Path get() = gameDirectory.resolve("config")
@JvmStatic
val tablist: List<PlayerInfo>
get() = minecraft.getConnection()
?.getListedOnlinePlayers()
?.sortedWith(tabListComparator) ?: emptyList()
@JvmStatic
val players: List<PlayerInfo>
get() = tablist.filter { it.getProfile().id.version() == 4 }
@JvmStatic
val scoreboard: Collection<Component>
get() {
val scoreboard = world?.scoreboard ?: return emptyList()
val objective = scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR) ?: return emptyList()
return scoreboard.listPlayerScores(objective)
.sortedBy { -it.value() }
.map {
val ownerName = Component.literal(it.owner())
val team = scoreboard.getPlayerTeam(it.owner())
if (team == null) {
ownerName.copy()
} else {
Component.empty().also { main ->
main.append(team.getPlayerPrefix())
if (ownerName.string.isNotEmpty()) main.append(ownerName)
main.append(team.getPlayerSuffix())
}
}
}
}
val scoreboardTitle get() = world?.scoreboard?.getDisplayObjective(DisplaySlot.SIDEBAR)?.displayName
}

View file

@ -0,0 +1,11 @@
package xyz.meowing.lattice.client
import org.lwjgl.glfw.GLFW
import xyz.meowing.lattice.client.Client.minecraft
object Clipboard {
@JvmStatic
var string: String
get() = GLFW.glfwGetClipboardString(minecraft.window.handle()) ?: ""
set(value) { GLFW.glfwSetClipboardString(minecraft.window.handle(), value) }
}

View file

@ -0,0 +1,23 @@
package xyz.meowing.lattice.client
object Desktop {
@JvmStatic
val isWindows: Boolean
@JvmStatic
val isMac: Boolean
@JvmStatic
val isLinux: Boolean
@JvmStatic
val isWayland: Boolean
init {
val osName = System.getProperty("os.name")?.lowercase() ?: ""
isWindows = osName.startsWith("windows")
isMac = osName.startsWith("mac")
isLinux = osName.startsWith("linux")
isWayland = isLinux && !System.getenv("WAYLAND_DISPLAY").isNullOrEmpty()
}
}

View file

@ -0,0 +1,21 @@
package xyz.meowing.lattice.client
import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.ModContainer
object Loader {
@JvmStatic
val isDevelopment: Boolean
get() = FabricLoader.getInstance().isDevelopmentEnvironment
@JvmStatic
val mods: List<ModContainer>
get() = FabricLoader.getInstance().allMods.toList()
@JvmStatic
fun isLoaded(id: String): Boolean = FabricLoader.getInstance().isModLoaded(id)
@JvmStatic
fun findMod(id: String): ModContainer? =
FabricLoader.getInstance().getModContainer(id).orElse(null)
}

View file

@ -0,0 +1,23 @@
package xyz.meowing.lattice.client
import net.minecraft.client.player.LocalPlayer
import net.minecraft.world.item.ItemStack
import xyz.meowing.lattice.client.Client.minecraft
object Player {
@JvmStatic
val player: LocalPlayer? get() = minecraft.player
@JvmStatic
val name: String? get() = player?.name?.string
@JvmStatic
val armor: Array<ItemStack?>
get() {
val inv = player?.inventory ?: return arrayOf(null, null, null, null)
return arrayOf(inv.getItem(36), inv.getItem(37), inv.getItem(38), inv.getItem(39))
}
@JvmStatic
val heldItem: ItemStack? get() = player?.mainHandItem
}

View file

@ -0,0 +1,82 @@
@file:Suppress("UNUSED", "UNCHECKED_CAST")
package xyz.meowing.lattice.command
import xyz.meowing.lattice.command.nodes.Executable
import xyz.meowing.lattice.command.nodes.LiteralNode
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
/**
* # Commodore
*
* A multi-version command library that simplifies the creation of command trees.
* It leverages powerful Kotlin DSL features and reflection to easily build
* complex command structures, streamlining the use of [Mojang's Brigadier](https://github.com/Mojang/brigadier).
*
* The command tree is constructed from a root [LiteralNode], with subsequent branches
* formed by additional nodes.
* The tree must always end in an [Executable][xyz.meowing.lattice.command.nodes.Executable],
* which serves as an exit point for a command.
*/
open class Commodore(private val root: LiteralNode) {
constructor(
vararg name: String,
block: LiteralNode.() -> Unit
) : this(LiteralNode(name[0], name.drop(1))) {
root.block()
}
constructor(
vararg name: String
) : this(LiteralNode(name[0], name.drop(1)))
/**
* DSL access to the root node for object-style definitions.
*/
fun runs(function: Function<Unit>) = root.runs(function)
fun runs(block: () -> Unit) = root.runs(block)
fun literal(string: String, block: LiteralNode.() -> Unit = {}): LiteralNode {
return root.literal(string, block)
}
fun literal(vararg names: String, block: LiteralNode.() -> Unit = {}): LiteralNode {
return root.literal(*names, block = block)
}
fun executable(block: Executable.() -> Unit): Executable {
return root.executable(block)
}
/**
* Registers this command to a dispatcher.
*
* For versions that support Brigadier (>=1.13), register the command to a dispatcher using:
* ```kotlin
* command.register(dispatcher)
* ```
*
* For legacy versions, simply call the function and specify an error callback:
* ```kotlin
* command.register { problem: String, cause: LiteralNode ->
* println("$problem")
* }
* ```
*/
fun register(dispatcher: CommandDispatcher<*>) {
for (node in root.children) {
node.build(root)
}
(dispatcher as CommandDispatcher<Any?>).register(root.builder)
val rootCommand = root.builder.build()
for (alias in root.aliases) {
val aliasBuilder = literal<Any?>(alias)
if (rootCommand.command != null) aliasBuilder.executes(rootCommand.command)
aliasBuilder.redirect(rootCommand)
dispatcher.register(aliasBuilder)
}
}
}

View file

@ -0,0 +1,90 @@
package xyz.meowing.lattice.command.functions
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
/**
* # FunctionInvoker
*
* Interface to simplify invoking functions, using Java reflection.
*
* @see LambdaInvoker
* @author Stivais
*/
sealed interface FunctionInvoker<T> {
/**
* List of parameters from the function.
*/
val parameters: List<Parameter<*>>
/**
* Invokes the function, with a list of arguments.
*
* This can error, due to incorrect input.
*/
fun invoke(arguments: List<Any?>): T
/**
* [FunctionInvoker] implementation, that is used for anonymous [functions][Function], like lambdas.
*
* This class uses [MethodHandle] to invoke the function.
*
* It is recommended to use [FunctionInvoker.from] instead of directly initializing this class.
*/
class LambdaInvoker<T> internal constructor(
lambda: Function<T>,
parameterTypes: Array<out Class<*>>
) : FunctionInvoker<T> {
override val parameters: List<Parameter<*>>
/**
* Used for invoking the function.
*/
private val mHandle: MethodHandle
init {
try {
val lambdaClass = lambda.javaClass
val invokeMethod = lambdaClass.declaredMethods.firstOrNull { method ->
!method.isSynthetic &&
!method.isBridge &&
method.name != "equals" &&
method.name != "hashCode" &&
method.name != "toString"
} ?: throw IllegalStateException("No invoke method found in lambda")
if (!invokeMethod.isAccessible) invokeMethod.isAccessible = true
mHandle = MethodHandles.lookup().unreflect(invokeMethod).bindTo(lambda)
if (parameterTypes.isEmpty()) {
parameters = invokeMethod.parameterTypes.mapIndexed { index, type ->
Parameter("param$index", type, false)
}
} else {
parameters = parameterTypes.mapIndexed { index, type ->
Parameter("param$index", type, false)
}
}
} catch (e: Exception) {
throw Exception("[Lattice-Commodore] Error creating Function Invoker.", e)
}
}
@Suppress("UNCHECKED_CAST")
override fun invoke(arguments: List<Any?>): T {
return mHandle.invokeWithArguments(arguments) as T
}
}
companion object {
/**
* Creates a function invoker with explicit parameter types.
* If no parameter types are provided, will attempt to extract them from the lambda's method signature.
*/
fun <T> from(function: Function<T>, vararg parameterTypes: Class<*>): FunctionInvoker<T> {
return LambdaInvoker(function, parameterTypes)
}
}
}

View file

@ -0,0 +1,9 @@
package xyz.meowing.lattice.command.functions
/**
* Represents a parameter for a function.
*
* @see FunctionInvoker
* @author Stivais
*/
data class Parameter<T>(val name: String, val type: Class<T>, val isNullable: Boolean)

View file

@ -0,0 +1,179 @@
@file:Suppress("UNUSED")
package xyz.meowing.lattice.command.nodes
import xyz.meowing.lattice.command.functions.FunctionInvoker
import xyz.meowing.lattice.command.parsers.CommandParser
import xyz.meowing.lattice.command.parsers.CommandParser.Companion.getParser
import xyz.meowing.lattice.command.parsers.ParserArgumentType
import xyz.meowing.lattice.command.parsers.impl.FunctionParser
import com.mojang.brigadier.Command
/**
* # Executable
*
* This [Node] implementation, acts as an exit point for a command tree.
*
* It takes a function's (or lambda's) parameters with reflection,
* to create [parsers][CommandParser] that are used to read a string.
*
* You can define custom parsers for certain parameters using [param] before the function is defined with [runs].
* For it to build correctly, you must have a function defined with [runs].
*
* @author Stivais
*/
class Executable : Node() {
/**
* [FunctionInvoker], representing this Executable's function.
*/
private lateinit var funInvoker: FunctionInvoker<Unit>
/**
* List of [ParserArgumentType], which retrieves data to invoke [funInvoker].
*/
val parsers: ArrayList<ParserArgumentType<*>> = arrayListOf()
/**
* Map, where key is the parameter name and value is the [parameter modifier][ParameterModifier].
*
* This gets cleared once the function is defined using [runs].
*/
private var parameterModifiers = mutableMapOf<String, ParameterModifier>()
/**
* Defines a function for this executable.
*
* This is primarily intended to be used with lambda like:
* ```kotlin
* runs { x: Float, y: Double, z: Int -> println("$x $y $z") }
* ```
* however it can be used with a function like:
* ```kotlin
* fun example(x: Float, y: Double, z: Int) {
* println("$x $y $z")
* }
* runs(::example)
* ```
*
* The function's parameters are used as command inputs.
* The parameter must have a [CommandParser] available.
*
* Classes that already have a parser provided: [String], [GreedyString][xyz.meowing.lattice.command.utils.GreedyString],
* [Int], [Long], [Float], [Double], [Boolean].
*
* To create custom parsers,
* you can either:
*
* - Apply the [CommandParsable][xyz.meowing.lattice.command.parsers.CommandParsable]
* annotation to class for it to generate a parser using the parameters of the primary constructor.
* (All parameters for that class must be also parsable).
*
* - Use a [ParameterModifier] to apply a custom parser to a certain parameter.
* You need to ensure you're parsing for the correct class.
*/
fun runs(function: Function<Unit>) {
funInvoker = FunctionInvoker.from(function)
for (parameter in funInvoker.parameters) {
val modifier = parameterModifiers[parameter.name]
val parser = modifier?.customParser ?: getParser(parameter.type)
if (parser == null) {
throw IllegalStateException(
"No parser found for parameter: ${parameter.name}(type = ${parameter.type}). " +
"Consider creating a parser for ${parameter.type} or applying @CommandParsable annotation to that class if possible."
)
}
val argumentType = ParserArgumentType(parameter, parser)
if (modifier != null) argumentType.suggestionCallback = modifier.suggestionCallback
parsers.add(argumentType)
}
// not used after it is built, so no reason to keep the data.
parameterModifiers.clear()
}
override fun build(parent: LiteralNode) {
if (!::funInvoker.isInitialized) {
throw IllegalStateException(
"Executable for ${parent.name} must have a defined function to work."
)
}
val brigadierCommand = Command<Any?> { ctx ->
funInvoker.invoke(List(parsers.size) { index ->
parsers[index].getValue(ctx) }
)
// return value is supposed to represent success level of a command.
// however, im not sure if returning a different actually changes anything.
Command.SINGLE_SUCCESS
}
val allOptional = parsers.reversed().all { parser ->
parser.builder.executes(brigadierCommand)
parser.optional()
}
if (allOptional) parent.builder.executes(brigadierCommand)
// has to be done in reverse unfortunately
for (i in parsers.size - 1 downTo 0) {
val parser = parsers[i]
val nextBuilder = if (i == 0) parent.builder else parsers[i - 1].builder
nextBuilder.then(parser.builder)
}
}
/**
* Creates and adds a modifier for the parameter with the specified [name].
*/
fun param(name: String): ParameterModifier {
val modifier = ParameterModifier()
parameterModifiers[name] = modifier
return modifier
}
/**
* Creates and adds a modifier for the parameter with the specified [name].
*/
inline fun param(name: String, block: ParameterModifier.() -> Unit): ParameterModifier {
val param = param(name)
param.block()
return param
}
/**
* ## ParameterModifier
*
* A modifier is used to apply either a custom parser, and or custom suggestions to a parameter.
*
* @see Executable
*/
class ParameterModifier(
var customParser: CommandParser<*>? = null,
var suggestionCallback: (() -> Collection<String>)? = null
) {
/**
* Specifies a custom parser for a parameter.
*
* Make sure the value it returns is same as parameter you're applying this parser to.
*/
fun <T> parser(function: Function<T>) {
customParser = FunctionParser(function)
}
/**
* Adds a callback to give mutable suggestions for a parameter.
*/
fun suggests(callback: () -> Collection<String>) {
suggestionCallback = callback
}
/**
* Adds constant suggestions for a parameter.
*/
fun suggests(vararg option: String) {
val list = option.toList()
suggestionCallback = { list }
}
}
}

View file

@ -0,0 +1,135 @@
@file:Suppress("UNUSED", "MemberVisibilityCanBePrivate")
package xyz.meowing.lattice.command.nodes
import com.mojang.brigadier.Command.SINGLE_SUCCESS
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
/**
* # LiteralNode
*
* Represents a node within a [Commodore][xyz.meowing.lattice.command.Commodore] command tree.
*
* A `LiteralNode` can either branch into more nodes that progress down the tree
* or contain an [Executable], which acts as an exit-point for the command, which executes a function.
*
* The input for this node is its [name] (and any defined aliases), and it is used
* for fixed, non-varying input in a command structure.
*/
open class LiteralNode(val name: String) : Node() {
constructor(name: String, aliases: List<String>) : this(name) {
this.aliases = aliases
}
/**
* Aliases for this current node.
*/
var aliases: List<String> = listOf()
/**
* Nodes, which branch from the current one.
*/
var children: ArrayList<Node> = arrayListOf()
/**
* Brigadier builder, representing this node.
*
* Used to construct/finalize the command tree and make it usable on top of brigadier.
*/
val builder: LiteralArgumentBuilder<Any?> = literal<Any?>(name)
/**
* Sets up the node and its children
*
* NOTE: Once you build, you are unable to make any changes to the command-tree.
*/
override fun build(parent: LiteralNode) {
for (node in children) {
node.build(this)
}
parent.builder.then(builder)
val builtNode = builder.build()
for (alias in aliases) {
val aliasBuilder = literal<Any?>(alias).redirect(builtNode)
parent.builder.then(aliasBuilder)
}
}
/**
* Creates a new literal node, branching from the current one.
*/
fun literal(string: String, block: LiteralNode.() -> Unit = {}): LiteralNode {
val node = LiteralNode(string)
node.block()
return addNode(node)
}
/**
* Creates a new literal node, branching from the current one.
*
* @param names vararg for the name and aliases of the node.
*/
fun literal(vararg names: String, block: LiteralNode.() -> Unit = {}): LiteralNode {
val node = LiteralNode(names[0], names.drop(1))
node.block()
return addNode(node)
}
/**
* DSL for [literal] using operator overloading.
*
* Example:
* ```
* "node" { /* ... */ }
* // is the same as
* literal("node") { /* ... */ }
* ```
*/
operator fun String.invoke(block: LiteralNode.() -> Unit = {}): LiteralNode {
return literal(this, block = block)
}
/**
* Creates an [Executable] and apply function directly.
*
* This won't allow you to apply [modifiers][Executable.ParameterModifier]
* to the function's parameters.
*
* @see runs
*/
fun runs(function: Function<Unit>) = executable {
runs(function)
}
/**
* Create and adds a [Executable] node to the current literal node.
*
* @see Executable
* @see runs
*/
fun executable(block: Executable.() -> Unit): Executable {
val executable = Executable()
executable.block()
return addNode(executable)
}
/**
* Faster alternative to [runs], for lambdas which don't have any parameters.
*/
inline fun runs(crossinline block: () -> Unit) {
builder.executes {
block()
SINGLE_SUCCESS
}
}
private fun <N : Node> addNode(node: N): N {
children.add(node)
node.parent = this
return node
}
}

View file

@ -0,0 +1,22 @@
package xyz.meowing.lattice.command.nodes
/**
* # Node
*
* Base class for all nodes for [Commodore][xyz.meowing.lattice.command.Commodore].
*
* @see LiteralNode
* @see Executable
* @author Stivais
*/
abstract class Node {
/**
* This node's parent.
*
* This will always be a [literal node][LiteralNode], since [Executable] can never have children nodes.
*/
var parent: LiteralNode? = null
internal abstract fun build(parent: LiteralNode)
}

View file

@ -0,0 +1,10 @@
package xyz.meowing.lattice.command.parsers
/**
* # CommandParsable
*
* This annotation indicates that a parser should attempt to be generated, based on the primary constructor parameters.
* If a parameter doesn't have a parser it will fail to generate.
* @author Stivais
*/
annotation class CommandParsable // todo: suggestions as a parameter in array

View file

@ -0,0 +1,141 @@
@file:Suppress("UNCHECKED_CAST")
package xyz.meowing.lattice.command.parsers
import com.mojang.brigadier.StringReader
import xyz.meowing.lattice.command.parsers.impl.BooleanParser
import xyz.meowing.lattice.command.parsers.impl.DoubleParser
import xyz.meowing.lattice.command.parsers.impl.FloatParser
import xyz.meowing.lattice.command.parsers.impl.FunctionParser
import xyz.meowing.lattice.command.parsers.impl.GreedyStringParser
import xyz.meowing.lattice.command.parsers.impl.IntParser
import xyz.meowing.lattice.command.parsers.impl.LongParser
import xyz.meowing.lattice.command.parsers.impl.StringParser
import xyz.meowing.lattice.command.utils.GreedyString
import xyz.meowing.lattice.command.utils.SyntaxException
import java.util.*
/**
* # CommandParser
*
* This interface is responsible for reading a string input and parsing it into a valid output
* that can be used for an [Executable][xyz.meowing.lattice.command.nodes.Executable].
*
* Classes with pre-defined parsers include: [String], [GreedyString], [Int], [Long], [Float],
* [Double], and [Boolean].
*
* @author Stivais
*/
interface CommandParser<T> {
/**
* Uses Brigadiers [StringReader] to parse an output.
*/
fun parse(reader: StringReader): T
/**
* Used to provide a collection of suggested strings for command completion.
*/
fun suggestions(): Collection<String> = Collections.emptyList()
// im not quite sure what it does in modern versions so ill keep this here just in case it has a use
fun examples(): Collection<String> = Collections.emptyList()
companion object {
/**
* Map of available parsers.
*
* Key == Class, Value == Parser for the class.
*/
private val parsers: MutableMap<Any?, CommandParser<*>> = mutableMapOf(
String::class.java to StringParser,
GreedyString::class.java to GreedyStringParser,
Integer::class.java to IntParser, Int::class.java to IntParser,
java.lang.Long::class.java to LongParser, Long::class.java to LongParser,
java.lang.Float::class.java to FloatParser, Float::class.java to FloatParser,
java.lang.Double::class.java to DoubleParser, Double::class.java to DoubleParser,
java.lang.Boolean::class.java to BooleanParser, Boolean::class.java to BooleanParser
)
/**
* Registers a [parser builder][CommandParser] to the [custom parsers map][parsers]].
*
* @param clazz the key for the map
* @param builder the parser builder
*/
fun <T> registerParser(clazz: Class<T>, builder: CommandParser<T>) {
parsers[clazz] = builder
}
/**
* Retrieves a parser for the given class type.
*
* If a parser for the class is already registered, it returns the existing parser.
* If no parser is found but the class is annotated with [CommandParsable],
* it attempts to create a parser using the primary constructor of the class.
* All parameters of the constructor must also be parsable.
*
* @throws IllegalArgumentException if the class annotated with [CommandParsable]
* does not have a primary constructor or if its parameters are not parsable
*/
fun getParser(type: Class<*>): CommandParser<*>? {
parsers[type]?.let { return it }
val annotation = type.getAnnotation(CommandParsable::class.java)
if (annotation != null) {
if (type.isEnum) {
val parser = object : CommandParser<Any> {
val enumMap = buildMap<String, Any> {
for (constant in type.enumConstants) {
val str = constant.toString().lowercase().replace("_", " ")
put(str, constant)
}
}
override fun parse(reader: StringReader): Any {
val builder = StringBuilder()
while (reader.canRead()) {
builder.append(reader.read())
val value = enumMap[builder.toString()]
if (value != null) return value
}
throw SyntaxException(
//#if MC == 1.8.9
//$$ "Invalid argument (valid arguments: ${enumMap.keys.joinToString(separator = ", ") { it }}) for command"
//#else
"Invalid argument for command"
//#endif
)
}
override fun suggestions(): Collection<String> {
return enumMap.keys
}
}
registerParser(
type as Class<Any?>,
parser as CommandParser<Any?>
)
return parser
} else {
try {
val constructors = type.constructors
if (constructors.isEmpty()) throw IllegalArgumentException("No constructor found")
val constructor = constructors[0]
val parser = FunctionParser { args: List<Any?> ->
constructor.newInstance(*args.toTypedArray())
}
registerParser(
type as Class<Any?>,
parser as CommandParser<Any?>
)
return parser
} catch (e: Exception) {
throw IllegalArgumentException("Failed to create parser for $type", e)
}
}
}
return null
}
}
}

View file

@ -0,0 +1,86 @@
package xyz.meowing.lattice.command.parsers
import xyz.meowing.lattice.command.functions.Parameter
import com.mojang.brigadier.StringReader
import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.exceptions.CommandExceptionType
import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.mojang.brigadier.suggestion.Suggestions
import com.mojang.brigadier.suggestion.SuggestionsBuilder
import xyz.meowing.lattice.command.utils.SyntaxException
import java.util.concurrent.CompletableFuture
/**
* Implementation of [ArgumentType], to integrate into Brigadier.
*
* @see CommandParser
* @author Stivais
*/
class ParserArgumentType<T>(
private val parameter: Parameter<T>,
parser: CommandParser<*>
) : ArgumentType<T> {
/**
* Parser for this argument type.
*/
@Suppress("UNCHECKED_CAST")
private val parser: CommandParser<T> = parser as CommandParser<T>
/**
* The [Argument builder][RequiredArgumentBuilder] tied to this class
*/
val builder: RequiredArgumentBuilder<Any?, T> = argument(parameter.name, this)
/**
* Callback for getting suggestions for this parameter.
*/
var suggestionCallback : (() -> Collection<String>)? = null
override fun parse(reader: StringReader): T {
try {
return parser.parse(reader)
} catch (e: SyntaxException) {
throw CommandSyntaxException(
object : CommandExceptionType {},
{ e.message },
reader.string,
reader.cursor
)
}
}
override fun <S : Any?> listSuggestions(context: CommandContext<S>, builder: SuggestionsBuilder): CompletableFuture<Suggestions> {
val suggestions = suggestionCallback?.invoke() ?: parser.suggestions()
for (str in suggestions) {
if (str.startsWith(builder.remaining)) {
builder.suggest(str)
}
}
return builder.buildFuture()
}
/**
* Gets the value from the [context][CommandContext]
*
* @return the value or null if it failed.
*/
fun getValue(ctx: CommandContext<Any?>): T? = try {
ctx.getArgument(parameter.name, parameter.type)
} catch (_: IllegalArgumentException) {
null
}
fun name() = parameter.name
/**
* Checks if this parameter is optional,
* meaning it isn't required for an [Executable][xyz.meowing.lattice.command.nodes.Executable], to run.
*
* A parameter is optional if it is nullable.
*/
fun optional() = parameter.isNullable
}

View file

@ -0,0 +1,13 @@
package xyz.meowing.lattice.command.parsers.impl
import xyz.meowing.lattice.command.parsers.CommandParser
import com.mojang.brigadier.StringReader
/**
* @author Stivais
*/
object BooleanParser : CommandParser<Boolean> {
override fun parse(reader: StringReader): Boolean {
return reader.readBoolean()
}
}

View file

@ -0,0 +1,82 @@
package xyz.meowing.lattice.command.parsers.impl
import xyz.meowing.lattice.command.functions.FunctionInvoker
import xyz.meowing.lattice.command.parsers.CommandParser
import xyz.meowing.lattice.command.parsers.CommandParser.Companion.getParser
import com.mojang.brigadier.StringReader
/**
* ## FunctionParser
*
* A parser that uses a provided function to process input parameters and output a value.
* The function's parameters define the expected input types, and its result is returned
* after processing the input.
*
* Example:
* ```kotlin
* // This command expects two float values. If they are the same, it throws an error.
* // The function outputs a Pair<Float, Float> if valid.
* parser { x: Float, y: Float ->
* if (x == y) throw SyntaxException("Values cannot be equal")
* Pair(x, y)
* }
* ```
*
* Unlike an [Executable][xyz.meowing.lattice.command.nodes.Executable] function, the parameters cannot be optional.
*
* @author Stivais
*/
class FunctionParser<T> : CommandParser<T> {
private val funInvoker: FunctionInvoker<T>?
private val constructorInvoker: ((List<Any?>) -> T)?
private val parsers: ArrayList<CommandParser<*>> = arrayListOf()
constructor(function: Function<T>, vararg parameterTypes: Class<*>) {
funInvoker = if (parameterTypes.isEmpty()) {
FunctionInvoker.from(function)
} else {
FunctionInvoker.from(function, *parameterTypes)
}
constructorInvoker = null
initParsers()
}
constructor(invoker: (List<Any?>) -> T) {
funInvoker = null
constructorInvoker = invoker
val constructor = invoker.javaClass.enclosingClass?.constructors?.firstOrNull()
?: throw IllegalStateException("Cannot find constructor")
for (param in constructor.parameters) {
val parser = getParser(param.type)
?: throw IllegalStateException("No parser found for parameter: ${param.name}(type=${param.type})")
parsers.add(parser)
}
require(parsers.isNotEmpty()) { "You need at least one parameter in the function for this parser." }
}
private fun initParsers() {
for (parameter in funInvoker!!.parameters) {
val parser = getParser(parameter.type)
?: throw IllegalStateException("No parser found for parameter: ${parameter.name}(type=${parameter.type})")
parsers.add(parser)
}
require(parsers.isNotEmpty()) { "You need at least one parameter in the function for this parser." }
}
override fun parse(reader: StringReader): T {
val arguments = mutableListOf<Any?>()
for (parser in parsers) {
reader.skipWhitespace()
arguments.add(parser.parse(reader))
}
return if (funInvoker != null) {
funInvoker.invoke(arguments)
} else {
constructorInvoker!!(arguments)
}
}
}

View file

@ -0,0 +1,40 @@
package xyz.meowing.lattice.command.parsers.impl
import xyz.meowing.lattice.command.parsers.CommandParser
import com.mojang.brigadier.StringReader
/**
* @author Stivais
*/
object LongParser : CommandParser<Long> {
override fun parse(reader: StringReader): Long {
return reader.readLong()
}
}
/**
* @author Stivais
*/
object IntParser : CommandParser<Int> {
override fun parse(reader: StringReader): Int {
return reader.readInt()
}
}
/**
* @author Stivais
*/
object FloatParser : CommandParser<Float> {
override fun parse(reader: StringReader): Float {
return reader.readFloat()
}
}
/**
* @author Stivais
*/
object DoubleParser : CommandParser<Double> {
override fun parse(reader: StringReader): Double {
return reader.readDouble()
}
}

View file

@ -0,0 +1,25 @@
package xyz.meowing.lattice.command.parsers.impl
import xyz.meowing.lattice.command.parsers.CommandParser
import com.mojang.brigadier.StringReader
import xyz.meowing.lattice.command.utils.GreedyString
/**
* @author Stivais
*/
object GreedyStringParser : CommandParser<GreedyString> {
override fun parse(reader: StringReader): GreedyString {
val string = reader.remaining
reader.cursor = reader.totalLength
return GreedyString(string)
}
}
/**
* @author Stivais
*/
object StringParser : CommandParser<String> {
override fun parse(reader: StringReader): String {
return reader.readString()
}
}

View file

@ -0,0 +1,22 @@
package xyz.meowing.lattice.command.utils
/**
* Used as a command argument, that takes all remaining strings from the input
* ```
* runs { greedy: GreedyString ->
* println("$greedy") // when given the input of "hello world" it accepts it and prints "hello world"
* }
*
* runs { string: String ->
* println("$string") // when given the input of "hello world" it doesn't accept it because too many arguments
* }
* ```
*
* NOTE: You can't use any more parameters past a greedy string as it takes all remaining strings,
* causing the command to error because it is unable to fulfill the remaining parameters
*
* @author Stivais
*/
data class GreedyString(val string: String) {
override fun toString(): String = string
}

View file

@ -0,0 +1,8 @@
package xyz.meowing.lattice.command.utils
/**
* Used to indicate and error in parsing.
*
* @author Stivais
*/
class SyntaxException(override val message: String) : Exception()

View file

@ -0,0 +1,30 @@
package xyz.meowing.lattice.command.utils
import xyz.meowing.lattice.command.nodes.LiteralNode
import com.mojang.brigadier.ParseResults
import com.mojang.brigadier.tree.LiteralCommandNode
/**
* Returns the latest [LiteralNode] from a string.
*
* @return The corresponding node
* @author Stivais
*/
fun findCorrespondingNode(node: LiteralNode, name: String): LiteralNode? {
for (child in node.children) {
if (child !is LiteralNode) continue
findCorrespondingNode(child, name)?.let { return it }
}
return if (node.name == name) node else null
}
/**
* Returns the latest [LiteralNode] from a [parse result][ParseResults]
*
* @return The corresponding node
* @author Stivais
*/
fun findCorrespondingNode(node: LiteralNode, results: ParseResults<Any?>): LiteralNode? {
val last = results.context.nodes.last { it.node is LiteralCommandNode }.node.name
return findCorrespondingNode(node, last)
}

View file

@ -0,0 +1,12 @@
package xyz.meowing.lattice.event
abstract class Event
abstract class CancellableEvent : Event() {
var cancelled = false
private set
fun cancel() {
cancelled = true
}
}

View file

@ -0,0 +1,78 @@
package xyz.meowing.lattice.event
import org.jetbrains.annotations.ApiStatus
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
open class EventBus {
val subscribers: MutableMap<Class<*>, MutableList<PrioritizedCallback<*>>> = ConcurrentHashMap()
inline fun <reified T : Event> register(
priority: Int = 0,
add: Boolean = true,
noinline callback: (T) -> Unit
): EventCall {
return register(T::class.java, priority, add, callback)
}
@JvmOverloads
fun <T : Event> register(
eventClass: Class<T>,
priority: Int = 0,
add: Boolean = true,
callback: (T) -> Unit
): EventCall {
val handlers = subscribers.getOrPut(eventClass) { CopyOnWriteArrayList() }
val prioritizedCallback = PrioritizedCallback(priority, callback)
if (add) addSorted(handlers, prioritizedCallback)
return EventCallImpl(prioritizedCallback, handlers, this, add)
}
fun <T : Event> post(event: T): Boolean {
val handlers = subscribers[event::class.java] ?: return false
if (handlers.isEmpty()) return false
for (handler in handlers) {
if (event is CancellableEvent && event.cancelled) return true
try {
@Suppress("UNCHECKED_CAST")
(handler.callback as (T) -> Unit)(event)
} catch (e: Exception) {
handleException(event, e)
}
}
return event is CancellableEvent && event.cancelled
}
/** Builds and posts the event only if it has subscribers. */
inline fun <reified T : Event> post(supplier: () -> T): Boolean {
if (!hasSubscribers(T::class.java)) return false
return post(supplier())
}
open fun handleException(event: Any, exception: Exception) {
exception.printStackTrace()
}
fun hasSubscribers(eventClass: Class<*>): Boolean {
return subscribers[eventClass]?.isNotEmpty() == true
}
inline fun <reified T : Event> hasSubscribers(): Boolean = hasSubscribers(T::class.java)
fun clear() {
subscribers.clear()
}
@ApiStatus.Internal
fun <T : Event> addSorted(
list: MutableList<PrioritizedCallback<*>>,
callback: PrioritizedCallback<T>
) {
val index = list.binarySearch { it.priority.compareTo(callback.priority) }
val insertIndex = if (index < 0) -(index + 1) else index
list.add(insertIndex, callback)
}
}

View file

@ -0,0 +1,45 @@
package xyz.meowing.lattice.event
interface EventCall {
fun unregister(): Boolean
fun register(): Boolean
fun isRegistered(): Boolean
}
data class PrioritizedCallback<T : Event>(
val priority: Int,
val callback: (T) -> Unit
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PrioritizedCallback<*>) return false
return callback === other.callback
}
override fun hashCode(): Int = callback.hashCode()
}
internal class EventCallImpl<T : Event>(
private val callback: PrioritizedCallback<T>,
private val handlers: MutableList<PrioritizedCallback<*>>,
private val eventBus: EventBus,
initiallyRegistered: Boolean
) : EventCall {
private var registered = initiallyRegistered
override fun unregister(): Boolean {
if (!registered) return false
val removed = handlers.remove(callback)
if (removed) registered = false
return removed
}
override fun register(): Boolean {
if (registered) return false
eventBus.addSorted(handlers, callback)
registered = true
return true
}
override fun isRegistered(): Boolean = registered
}

View file

@ -0,0 +1,16 @@
package xyz.meowing.lattice.event
sealed class ClientEvent {
class Start : Event()
class Stop : Event()
}
sealed class TickEvent {
class Start : Event()
class End : Event()
}
sealed class RenderEvent {
/** Posted every frame after vanilla GUI rendering, inside a renderer frame. */
class Gui : CancellableEvent()
}

View file

@ -0,0 +1,15 @@
package xyz.meowing.lattice.input
sealed interface InputCode {
val code: Int
val isPressed: Boolean
val displayName: String get() = Inputs.getDisplayName(code)
}
data class Key(override val code: Int) : InputCode {
override val isPressed: Boolean get() = Keyboard.isPressed(code)
}
data class MouseButton(override val code: Int) : InputCode {
override val isPressed: Boolean get() = Mouse.isPressed(code)
}

View file

@ -0,0 +1,29 @@
package xyz.meowing.lattice.input
import com.mojang.blaze3d.platform.InputConstants
import org.lwjgl.glfw.GLFW
object Inputs {
fun get(code: Int): InputCode {
return when {
Keyboard.isKeyboardButton(code) -> Key(code)
Mouse.isMouseButton(code) -> MouseButton(code)
else -> throw IllegalArgumentException("Code $code is not a valid key or mouse button")
}
}
@JvmStatic
@JvmOverloads
fun getDisplayName(code: Int, scanCode: Int = -1): String {
val keyName = GLFW.glfwGetKeyName(code, scanCode)
if (keyName != null) {
return if (keyName.length == 1) keyName.uppercase() else keyName
}
val name = (if (code == -1) {
InputConstants.Type.SCANCODE.getOrCreate(scanCode)
} else {
InputConstants.Type.KEYSYM.getOrCreate(code)
}).displayName.string
return if (name.length == 1) name.uppercase() else name
}
}

View file

@ -0,0 +1,34 @@
package xyz.meowing.lattice.input
import org.lwjgl.glfw.GLFW
import xyz.meowing.lattice.render.Resolution.windowHandle
object Keyboard {
@JvmStatic
val isShiftKeyPressed: Boolean
get() = Keys.KEY_LEFT_SHIFT.isPressed || Keys.KEY_RIGHT_SHIFT.isPressed
@JvmStatic
val isCtrlKeyPressed: Boolean
get() = Keys.KEY_LEFT_CONTROL.isPressed || Keys.KEY_RIGHT_CONTROL.isPressed
@JvmStatic
val isAltKeyPressed: Boolean
get() = Keys.KEY_LEFT_ALT.isPressed || Keys.KEY_RIGHT_ALT.isPressed
@JvmStatic
val isSuperKeyPressed: Boolean
get() = Keys.KEY_LEFT_SUPER.isPressed || Keys.KEY_RIGHT_SUPER.isPressed
@JvmStatic
fun isKeyboardButton(code: Int): Boolean {
return code in 0 until GLFW.GLFW_KEY_LAST
}
@JvmStatic
fun isPressed(code: Int): Boolean {
if (!isKeyboardButton(code)) return false
val state = GLFW.glfwGetKey(windowHandle, code)
return state == GLFW.GLFW_PRESS || state == GLFW.GLFW_REPEAT
}
}

View file

@ -0,0 +1,42 @@
package xyz.meowing.lattice.input
import org.lwjgl.glfw.GLFW
data class KeyboardModifiers(
val isShift: Boolean,
val isCtrl: Boolean,
val isAlt: Boolean,
val isSuper: Boolean,
val isCapsLock: Boolean = false,
val isNumLock: Boolean = false
) {
companion object {
val current: KeyboardModifiers
get() = KeyboardModifiers(
isShift = Keyboard.isShiftKeyPressed,
isCtrl = Keyboard.isCtrlKeyPressed,
isAlt = Keyboard.isAltKeyPressed,
isSuper = Keyboard.isSuperKeyPressed,
)
fun wrap(mods: Int): KeyboardModifiers {
return KeyboardModifiers(
isShift = (mods and GLFW.GLFW_MOD_SHIFT) != 0,
isCtrl = (mods and GLFW.GLFW_MOD_CONTROL) != 0,
isAlt = (mods and GLFW.GLFW_MOD_ALT) != 0,
isSuper = (mods and GLFW.GLFW_MOD_SUPER) != 0,
)
}
}
fun toMods(): Int {
return listOf(
isShift to GLFW.GLFW_MOD_SHIFT,
isCtrl to GLFW.GLFW_MOD_CONTROL,
isAlt to GLFW.GLFW_MOD_ALT,
isSuper to GLFW.GLFW_MOD_SUPER,
isCapsLock to GLFW.GLFW_MOD_CAPS_LOCK,
isNumLock to GLFW.GLFW_MOD_NUM_LOCK
).sumOf { (value, mod) -> if (value) mod else 0 }
}
}

View file

@ -0,0 +1,125 @@
package xyz.meowing.lattice.input
import org.lwjgl.glfw.GLFW
object Keys {
val KEY_NONE = Key(GLFW.GLFW_KEY_UNKNOWN)
val KEY_ESCAPE = Key(GLFW.GLFW_KEY_ESCAPE)
val KEY_F1 = Key(GLFW.GLFW_KEY_F1)
val KEY_F2 = Key(GLFW.GLFW_KEY_F2)
val KEY_F3 = Key(GLFW.GLFW_KEY_F3)
val KEY_F4 = Key(GLFW.GLFW_KEY_F4)
val KEY_F5 = Key(GLFW.GLFW_KEY_F5)
val KEY_F6 = Key(GLFW.GLFW_KEY_F6)
val KEY_F7 = Key(GLFW.GLFW_KEY_F7)
val KEY_F8 = Key(GLFW.GLFW_KEY_F8)
val KEY_F9 = Key(GLFW.GLFW_KEY_F9)
val KEY_F10 = Key(GLFW.GLFW_KEY_F10)
val KEY_F11 = Key(GLFW.GLFW_KEY_F11)
val KEY_F12 = Key(GLFW.GLFW_KEY_F12)
val KEY_F13 = Key(GLFW.GLFW_KEY_F13)
val KEY_F14 = Key(GLFW.GLFW_KEY_F14)
val KEY_F15 = Key(GLFW.GLFW_KEY_F15)
val KEY_LEFT_SHIFT = Key(GLFW.GLFW_KEY_LEFT_SHIFT)
val KEY_RIGHT_SHIFT = Key(GLFW.GLFW_KEY_RIGHT_SHIFT)
val KEY_LEFT_CONTROL = Key(GLFW.GLFW_KEY_LEFT_CONTROL)
val KEY_RIGHT_CONTROL = Key(GLFW.GLFW_KEY_RIGHT_CONTROL)
val KEY_LEFT_ALT = Key(GLFW.GLFW_KEY_LEFT_ALT)
val KEY_RIGHT_ALT = Key(GLFW.GLFW_KEY_RIGHT_ALT)
val KEY_LEFT_SUPER = Key(GLFW.GLFW_KEY_LEFT_SUPER)
val KEY_RIGHT_SUPER = Key(GLFW.GLFW_KEY_RIGHT_SUPER)
val KEY_CAPS_LOCK = Key(GLFW.GLFW_KEY_CAPS_LOCK)
val KEY_NUM_LOCK = Key(GLFW.GLFW_KEY_NUM_LOCK)
val KEY_SCROLL_LOCK = Key(GLFW.GLFW_KEY_SCROLL_LOCK)
val KEY_TAB = Key(GLFW.GLFW_KEY_TAB)
val KEY_ENTER = Key(GLFW.GLFW_KEY_ENTER)
val KEY_BACKSPACE = Key(GLFW.GLFW_KEY_BACKSPACE)
val KEY_DELETE = Key(GLFW.GLFW_KEY_DELETE)
val KEY_INSERT = Key(GLFW.GLFW_KEY_INSERT)
val KEY_PAGE_UP = Key(GLFW.GLFW_KEY_PAGE_UP)
val KEY_PAGE_DOWN = Key(GLFW.GLFW_KEY_PAGE_DOWN)
val KEY_HOME = Key(GLFW.GLFW_KEY_HOME)
val KEY_END = Key(GLFW.GLFW_KEY_END)
val KEY_LEFT = Key(GLFW.GLFW_KEY_LEFT)
val KEY_RIGHT = Key(GLFW.GLFW_KEY_RIGHT)
val KEY_UP = Key(GLFW.GLFW_KEY_UP)
val KEY_DOWN = Key(GLFW.GLFW_KEY_DOWN)
val KEY_0 = Key(GLFW.GLFW_KEY_0)
val KEY_1 = Key(GLFW.GLFW_KEY_1)
val KEY_2 = Key(GLFW.GLFW_KEY_2)
val KEY_3 = Key(GLFW.GLFW_KEY_3)
val KEY_4 = Key(GLFW.GLFW_KEY_4)
val KEY_5 = Key(GLFW.GLFW_KEY_5)
val KEY_6 = Key(GLFW.GLFW_KEY_6)
val KEY_7 = Key(GLFW.GLFW_KEY_7)
val KEY_8 = Key(GLFW.GLFW_KEY_8)
val KEY_9 = Key(GLFW.GLFW_KEY_9)
val KEY_A = Key(GLFW.GLFW_KEY_A)
val KEY_B = Key(GLFW.GLFW_KEY_B)
val KEY_C = Key(GLFW.GLFW_KEY_C)
val KEY_D = Key(GLFW.GLFW_KEY_D)
val KEY_E = Key(GLFW.GLFW_KEY_E)
val KEY_F = Key(GLFW.GLFW_KEY_F)
val KEY_G = Key(GLFW.GLFW_KEY_G)
val KEY_H = Key(GLFW.GLFW_KEY_H)
val KEY_I = Key(GLFW.GLFW_KEY_I)
val KEY_J = Key(GLFW.GLFW_KEY_J)
val KEY_K = Key(GLFW.GLFW_KEY_K)
val KEY_L = Key(GLFW.GLFW_KEY_L)
val KEY_M = Key(GLFW.GLFW_KEY_M)
val KEY_N = Key(GLFW.GLFW_KEY_N)
val KEY_O = Key(GLFW.GLFW_KEY_O)
val KEY_P = Key(GLFW.GLFW_KEY_P)
val KEY_Q = Key(GLFW.GLFW_KEY_Q)
val KEY_R = Key(GLFW.GLFW_KEY_R)
val KEY_S = Key(GLFW.GLFW_KEY_S)
val KEY_T = Key(GLFW.GLFW_KEY_T)
val KEY_U = Key(GLFW.GLFW_KEY_U)
val KEY_V = Key(GLFW.GLFW_KEY_V)
val KEY_W = Key(GLFW.GLFW_KEY_W)
val KEY_X = Key(GLFW.GLFW_KEY_X)
val KEY_Y = Key(GLFW.GLFW_KEY_Y)
val KEY_Z = Key(GLFW.GLFW_KEY_Z)
val KEY_SPACE = Key(GLFW.GLFW_KEY_SPACE)
val KEY_APOSTROPHE = Key(GLFW.GLFW_KEY_APOSTROPHE)
val KEY_COMMA = Key(GLFW.GLFW_KEY_COMMA)
val KEY_MINUS = Key(GLFW.GLFW_KEY_MINUS)
val KEY_PERIOD = Key(GLFW.GLFW_KEY_PERIOD)
val KEY_SLASH = Key(GLFW.GLFW_KEY_SLASH)
val KEY_SEMICOLON = Key(GLFW.GLFW_KEY_SEMICOLON)
val KEY_EQUAL = Key(GLFW.GLFW_KEY_EQUAL)
val KEY_LEFT_BRACKET = Key(GLFW.GLFW_KEY_LEFT_BRACKET)
val KEY_BACKSLASH = Key(GLFW.GLFW_KEY_BACKSLASH)
val KEY_RIGHT_BRACKET = Key(GLFW.GLFW_KEY_RIGHT_BRACKET)
val KEY_GRAVE_ACCENT = Key(GLFW.GLFW_KEY_GRAVE_ACCENT)
val KEY_NUMPAD_0 = Key(GLFW.GLFW_KEY_KP_0)
val KEY_NUMPAD_1 = Key(GLFW.GLFW_KEY_KP_1)
val KEY_NUMPAD_2 = Key(GLFW.GLFW_KEY_KP_2)
val KEY_NUMPAD_3 = Key(GLFW.GLFW_KEY_KP_3)
val KEY_NUMPAD_4 = Key(GLFW.GLFW_KEY_KP_4)
val KEY_NUMPAD_5 = Key(GLFW.GLFW_KEY_KP_5)
val KEY_NUMPAD_6 = Key(GLFW.GLFW_KEY_KP_6)
val KEY_NUMPAD_7 = Key(GLFW.GLFW_KEY_KP_7)
val KEY_NUMPAD_8 = Key(GLFW.GLFW_KEY_KP_8)
val KEY_NUMPAD_9 = Key(GLFW.GLFW_KEY_KP_9)
val KEY_NUMPAD_DECIMAL = Key(GLFW.GLFW_KEY_KP_DECIMAL)
val KEY_NUMPAD_DIVIDE = Key(GLFW.GLFW_KEY_KP_DIVIDE)
val KEY_NUMPAD_MULTIPLY = Key(GLFW.GLFW_KEY_KP_MULTIPLY)
val KEY_NUMPAD_SUBTRACT = Key(GLFW.GLFW_KEY_KP_SUBTRACT)
val KEY_NUMPAD_ADD = Key(GLFW.GLFW_KEY_KP_ADD)
val KEY_NUMPAD_ENTER = Key(GLFW.GLFW_KEY_KP_ENTER)
val KEY_NUMPAD_EQUAL = Key(GLFW.GLFW_KEY_KP_EQUAL)
val KEY_PRINT_SCREEN = Key(GLFW.GLFW_KEY_PRINT_SCREEN)
val KEY_PAUSE = Key(GLFW.GLFW_KEY_PAUSE)
val KEY_MENU = Key(GLFW.GLFW_KEY_MENU)
}

View file

@ -0,0 +1,33 @@
package xyz.meowing.lattice.input
import org.lwjgl.glfw.GLFW
import xyz.meowing.lattice.client.Client.minecraft
import xyz.meowing.lattice.render.Resolution
import xyz.meowing.lattice.render.Resolution.windowHandle
import kotlin.math.max
object Mouse {
object Raw {
val x: Double get() = minecraft.mouseHandler.xpos()
val y: Double get() = minecraft.mouseHandler.ypos()
}
object Scaled {
val x: Double get() = Raw.x * Resolution.scaledWidth / max(1, Resolution.windowWidth)
val y: Double get() = Raw.y * Resolution.scaledHeight / max(1, Resolution.windowHeight)
}
var isCursorGrabbed: Boolean
get() = minecraft.mouseHandler.isMouseGrabbed
set(value) {
if (value) minecraft.mouseHandler.grabMouse() else minecraft.mouseHandler.releaseMouse()
}
fun isMouseButton(code: Int): Boolean = code in GLFW.GLFW_MOUSE_BUTTON_1..GLFW.GLFW_MOUSE_BUTTON_8
fun isPressed(code: Int): Boolean {
if (!isMouseButton(code)) return false
val state = GLFW.glfwGetMouseButton(windowHandle, code)
return state == GLFW.GLFW_PRESS || state == GLFW.GLFW_REPEAT
}
}

View file

@ -0,0 +1,14 @@
package xyz.meowing.lattice.input
import org.lwjgl.glfw.GLFW
object MouseButtons {
val LEFT = MouseButton(GLFW.GLFW_MOUSE_BUTTON_LEFT)
val RIGHT = MouseButton(GLFW.GLFW_MOUSE_BUTTON_RIGHT)
val MIDDLE = MouseButton(GLFW.GLFW_MOUSE_BUTTON_MIDDLE)
val BACK = MouseButton(GLFW.GLFW_MOUSE_BUTTON_4)
val FORWARD = MouseButton(GLFW.GLFW_MOUSE_BUTTON_5)
val BUTTON6 = MouseButton(GLFW.GLFW_MOUSE_BUTTON_6)
val BUTTON7 = MouseButton(GLFW.GLFW_MOUSE_BUTTON_7)
val BUTTON8 = MouseButton(GLFW.GLFW_MOUSE_BUTTON_8)
}

View file

@ -0,0 +1,119 @@
package xyz.meowing.lattice.render
// Derived from OdinFabric (BSD-3-Clause, (c) odtheking); see NOTICE.md.
class Color(hue: Float, saturation: Float, brightness: Float, alpha: Float = 1f) {
constructor(hsb: FloatArray, alpha: Float = 1f) : this(hsb[0], hsb[1], hsb[2], alpha)
constructor(r: Int, g: Int, b: Int, alpha: Float = 1f) : this(
java.awt.Color.RGBtoHSB(r, g, b, FloatArray(size = 3)), alpha
)
constructor(rgba: Int) : this(rgba.red, rgba.green, rgba.blue, alpha = rgba.alpha / 255f)
constructor(rgba: Int, alpha: Float) : this(rgba.red, rgba.green, rgba.blue, alpha)
constructor(hex: String) : this(
hex.take(2).toInt(16),
hex.substring(2, 4).toInt(16),
hex.substring(4, 6).toInt(16),
hex.substring(6, 8).toInt(16) / 255f
)
var hue = hue
set(value) {
field = value
needsUpdate = true
}
var saturation = saturation
set(value) {
field = value
needsUpdate = true
}
var brightness = brightness
set(value) {
field = value
needsUpdate = true
}
var alphaFloat = alpha
set(value) {
field = value
needsUpdate = true
}
private var needsUpdate = true
/** ARGB value, recomputed lazily when the HSBA components change. */
var rgba: Int = 0
get() {
if (needsUpdate) {
field = (java.awt.Color.HSBtoRGB(hue, saturation, brightness) and 0x00FFFFFF) or
((this.alphaFloat * 255).toInt() shl 24)
needsUpdate = false
}
return field
}
private set
inline val red get() = rgba.red
inline val green get() = rgba.green
inline val blue get() = rgba.blue
inline val alpha get() = rgba.alpha
inline val redFloat get() = red / 255f
inline val greenFloat get() = green / 255f
inline val blueFloat get() = blue / 255f
@OptIn(ExperimentalStdlibApi::class)
fun hex(includeAlpha: Boolean = true): String {
val hexString = rgba.toHexString(HexFormat.UpperCase)
return if (includeAlpha) hexString.substring(2) + hexString.take(2)
else hexString.substring(2)
}
inline val isTransparent: Boolean get() = this.alphaFloat == 0f
override fun toString(): String = "Color(red=$red,green=$green,blue=$blue,alpha=$alpha)"
override fun hashCode(): Int {
var result = hue.toInt()
result = 31 * result + saturation.toInt()
result = 31 * result + brightness.toInt()
result = 31 * result + this.alphaFloat.toInt()
return result
}
override fun equals(other: Any?): Boolean {
if (other === this) return true
return other is Color && rgba == other.rgba
}
fun copy(): Color = Color(this.rgba)
companion object {
inline val Int.red get() = this shr 16 and 0xFF
inline val Int.green get() = this shr 8 and 0xFF
inline val Int.blue get() = this and 0xFF
inline val Int.alpha get() = this shr 24 and 0xFF
fun Color.brighter(factor: Float = 1.3f): Color {
return Color(hue, saturation, (brightness * factor.coerceAtLeast(1f)).coerceAtMost(1f), this.alphaFloat)
}
fun Color.darker(factor: Float = 0.7f): Color {
return Color(hue, saturation, brightness * factor, this.alphaFloat)
}
fun Color.withAlpha(alpha: Float, newInstance: Boolean = true): Color {
return if (newInstance) Color(red, green, blue, alpha)
else {
this.alphaFloat = alpha
this
}
}
fun Color.multiplyAlpha(factor: Float): Color {
return Color(red, green, blue, (alphaFloat * factor).coerceIn(0f, 1f))
}
fun Color.hsbMax(): Color = Color(hue, 1f, 1f)
}
}

View file

@ -0,0 +1,42 @@
package xyz.meowing.lattice.render
import java.io.FileNotFoundException
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
// Derived from OdinFabric (BSD-3-Clause, (c) odtheking); see NOTICE.md.
class Font {
val name: String
private val resourcePath: String?
private val cachedBytes: ByteArray?
constructor(name: String, resourcePath: String) {
this.name = name
this.resourcePath = resourcePath
this.cachedBytes = null
}
constructor(name: String, inputStream: InputStream) {
this.name = name
this.resourcePath = null
this.cachedBytes = inputStream.use { it.readBytes() }
}
fun buffer(): ByteBuffer {
val bytes = cachedBytes ?: run {
val stream = this::class.java.getResourceAsStream(resourcePath!!)
?: throw FileNotFoundException(resourcePath)
stream.use { it.readBytes() }
}
return ByteBuffer.allocateDirect(bytes.size)
.order(ByteOrder.nativeOrder())
.put(bytes)
.flip() as ByteBuffer
}
override fun hashCode(): Int = name.hashCode()
override fun equals(other: Any?): Boolean = other is Font && name == other.name
}

View file

@ -0,0 +1,16 @@
package xyz.meowing.lattice.render
/** Tracks GL state that NanoVG frames must restore for vanilla rendering. */
object GLState {
@JvmStatic
var previousBoundTexture = -1
@JvmStatic
var previousActiveTexture = -1
@JvmStatic
var previousProgram = -1
@JvmStatic
var drawing = false
}

View file

@ -0,0 +1,7 @@
package xyz.meowing.lattice.render
enum class Gradient {
LeftToRight,
TopToBottom,
TopLeftToBottomRight,
}

View file

@ -0,0 +1,43 @@
package xyz.meowing.lattice.render
import org.lwjgl.system.MemoryUtil
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.file.Files
// Derived from OdinFabric (BSD-3-Clause, (c) odtheking); see NOTICE.md.
class Image(
val identifier: String,
var stream: InputStream = getStream(identifier),
private var buffer: ByteBuffer? = null
) {
val isSVG: Boolean = identifier.endsWith(".svg", true)
fun buffer(): ByteBuffer {
if (buffer == null) {
val bytes = stream.readBytes()
buffer = MemoryUtil.memAlloc(bytes.size).put(bytes).flip() as ByteBuffer
stream.close()
}
return buffer ?: throw IllegalStateException("Image has no data")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Image) return false
return identifier == other.identifier
}
override fun hashCode(): Int = identifier.hashCode()
companion object {
private fun getStream(path: String): InputStream {
val trimmedPath = path.trim()
val file = File(trimmedPath)
return if (file.exists() && file.isFile) Files.newInputStream(file.toPath())
else this::class.java.getResourceAsStream(trimmedPath) ?: throw FileNotFoundException(trimmedPath)
}
}
}

View file

@ -0,0 +1,484 @@
package xyz.meowing.lattice.render
import com.mojang.blaze3d.opengl.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import org.lwjgl.nanovg.NVGColor
import org.lwjgl.nanovg.NVGPaint
import org.lwjgl.nanovg.NanoSVG
import org.lwjgl.nanovg.NanoVG
import org.lwjgl.nanovg.NanoVGGL3
import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL13
import org.lwjgl.opengl.GL20
import org.lwjgl.opengl.GL30
import org.lwjgl.stb.STBImage
import org.lwjgl.system.MemoryUtil
import xyz.meowing.lattice.client.Client.minecraft
import xyz.meowing.lattice.render.Color.Companion.alpha
import xyz.meowing.lattice.render.Color.Companion.blue
import xyz.meowing.lattice.render.Color.Companion.green
import xyz.meowing.lattice.render.Color.Companion.red
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.nio.ByteBuffer
import kotlin.math.max
import kotlin.math.min
import kotlin.math.round
// Derived from OdinFabric (BSD-3-Clause, (c) odtheking); see NOTICE.md.
object NVGRenderer : Renderer {
private val nvgPaint = NVGPaint.malloc()
private val nvgColor = NVGColor.malloc()
private val nvgColor2 = NVGColor.malloc()
private val fontMap = HashMap<Font, NVGFont>()
private val fontBounds = FloatArray(4)
private val images = HashMap<Image, NVGImage>()
private var scissor: Scissor? = null
private var vg = -1L
init {
vg = NanoVGGL3.nvgCreate(NanoVGGL3.NVG_ANTIALIAS or NanoVGGL3.NVG_STENCIL_STROKES)
require(vg != -1L) { "Failed to initialize NanoVG" }
}
// Resolved reflectively once: GlTexture.getFbo(GlStateManager$DirectStateAccess, depth) is not public API.
private var fboLookup: Pair<Any, Method>? = null
private var fboLookupFailed = false
private fun mainFramebuffer(colorTex: Any): Int {
if (fboLookupFailed) return 0
try {
val lookup = fboLookup ?: run {
val device = RenderSystem.getDevice() ?: return 0
var dsaField: Field? = null
var clazz: Class<*>? = device.javaClass
while (clazz != null && dsaField == null) {
try {
dsaField = clazz.getDeclaredField("directStateAccess")
} catch (_: NoSuchFieldException) {}
clazz = clazz.superclass
}
dsaField?.isAccessible = true
val dsa = dsaField?.get(device) ?: throw IllegalStateException("no directStateAccess")
var getFbo: Method? = null
var texClazz: Class<*>? = colorTex.javaClass
while (texClazz != null && getFbo == null) {
getFbo = texClazz.declaredMethods.firstOrNull { it.name == "getFbo" && it.parameterCount == 2 }
texClazz = texClazz.superclass
}
getFbo?.isAccessible = true
if (getFbo == null) throw IllegalStateException("no getFbo")
Pair(dsa, getFbo).also { fboLookup = it }
}
return (lookup.second.invoke(colorTex, lookup.first, null) as? Int) ?: 0
} catch (_: Exception) {
fboLookupFailed = true
return 0
}
}
override fun beginFrame(width: Float, height: Float) {
check(!GLState.drawing) { "[NVGRenderer] Already drawing, but called beginFrame" }
GLState.previousActiveTexture = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE)
GLState.previousProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM)
val renderTarget = minecraft.mainRenderTarget ?: return
val colorTex = renderTarget.getColorTexture() ?: return
if (vg == -1L) return
GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, mainFramebuffer(colorTex))
GlStateManager._viewport(0, 0, renderTarget.width, renderTarget.height)
GlStateManager._activeTexture(GL30.GL_TEXTURE0)
NanoVG.nvgBeginFrame(vg, width, height, 1f)
NanoVG.nvgTextAlign(vg, NanoVG.NVG_ALIGN_LEFT or NanoVG.NVG_ALIGN_TOP)
GLState.drawing = true
}
override fun endFrame() {
check(GLState.drawing) { "[NVGRenderer] Not drawing, but called endFrame" }
NanoVG.nvgEndFrame(vg)
// Restore the default state vanilla expects after NanoVG's frame.
GlStateManager._disableCull()
GlStateManager._disableDepthTest()
GlStateManager._enableBlend()
GlStateManager._blendFuncSeparate(770, 771, 1, 0)
if (GLState.previousProgram != -1) GlStateManager._glUseProgram(GLState.previousProgram)
if (GLState.previousActiveTexture != -1) {
GlStateManager._activeTexture(GLState.previousActiveTexture)
if (GLState.previousBoundTexture != -1) GlStateManager._bindTexture(GLState.previousBoundTexture)
}
GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0)
GLState.drawing = false
}
override fun push() = NanoVG.nvgSave(vg)
override fun pop() = NanoVG.nvgRestore(vg)
override fun scale(x: Float, y: Float) = NanoVG.nvgScale(vg, x, y)
override fun translate(x: Float, y: Float) = NanoVG.nvgTranslate(vg, x, y)
override fun rotate(amount: Float) = NanoVG.nvgRotate(vg, amount)
override fun globalAlpha(amount: Float) = NanoVG.nvgGlobalAlpha(vg, amount.coerceIn(0f, 1f))
override fun pushScissor(x: Float, y: Float, w: Float, h: Float) {
scissor = Scissor(scissor, x, y, w + x, h + y)
scissor?.applyScissor()
}
override fun popScissor() {
NanoVG.nvgResetScissor(vg)
scissor = scissor?.previous
scissor?.applyScissor()
}
override fun line(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, color: Int) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgMoveTo(vg, x1, y1)
NanoVG.nvgLineTo(vg, x2, y2)
NanoVG.nvgStrokeWidth(vg, thickness)
color(color)
NanoVG.nvgStrokeColor(vg, nvgColor)
NanoVG.nvgStroke(vg)
}
override fun rect(x: Float, y: Float, w: Float, h: Float, color: Int, radius: Float, roundTop: Boolean) {
NanoVG.nvgBeginPath(vg)
if (roundTop) {
NanoVG.nvgMoveTo(vg, x, y + h)
NanoVG.nvgLineTo(vg, x + w, y + h)
NanoVG.nvgLineTo(vg, x + w, y + radius)
NanoVG.nvgArcTo(vg, x + w, y, x + w - radius, y, radius)
NanoVG.nvgLineTo(vg, x + radius, y)
NanoVG.nvgArcTo(vg, x, y, x, y + radius, radius)
NanoVG.nvgLineTo(vg, x, y + h)
} else {
NanoVG.nvgMoveTo(vg, x, y)
NanoVG.nvgLineTo(vg, x + w, y)
NanoVG.nvgLineTo(vg, x + w, y + h - radius)
NanoVG.nvgArcTo(vg, x + w, y + h, x + w - radius, y + h, radius)
NanoVG.nvgLineTo(vg, x + radius, y + h)
NanoVG.nvgArcTo(vg, x, y + h, x, y + h - radius, radius)
NanoVG.nvgLineTo(vg, x, y)
}
NanoVG.nvgClosePath(vg)
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgFill(vg)
}
override fun rect(x: Float, y: Float, w: Float, h: Float, color: Int, radius: Float) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRect(vg, x, y, w, h + .5f, radius)
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgFill(vg)
}
override fun rect(x: Float, y: Float, w: Float, h: Float, color: Int) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRect(vg, x, y, w, h + .5f)
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgFill(vg)
}
override fun rect(x: Float, y: Float, w: Float, h: Float, color: Int, topRight: Float, topLeft: Float, bottomRight: Float, bottomLeft: Float) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRectVarying(vg, round(x), round(y), round(w), round(h), topRight, topLeft, bottomRight, bottomLeft)
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgFill(vg)
}
override fun hollowRect(x: Float, y: Float, w: Float, h: Float, thickness: Float, color: Int, radius: Float) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRect(vg, x, y, w, h, radius)
NanoVG.nvgStrokeWidth(vg, thickness)
NanoVG.nvgPathWinding(vg, NanoVG.NVG_HOLE)
color(color)
NanoVG.nvgStrokeColor(vg, nvgColor)
NanoVG.nvgStroke(vg)
}
override fun hollowGradientRect(x: Float, y: Float, w: Float, h: Float, thickness: Float, color1: Int, color2: Int, gradient: Gradient, radius: Float) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRect(vg, x, y, w, h, radius)
NanoVG.nvgStrokeWidth(vg, thickness)
gradient(color1, color2, x, y, w, h, gradient)
NanoVG.nvgStrokePaint(vg, nvgPaint)
NanoVG.nvgStroke(vg)
}
override fun gradientRect(x: Float, y: Float, w: Float, h: Float, color1: Int, color2: Int, gradient: Gradient, radius: Float) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRect(vg, x, y, w, h, radius)
gradient(color1, color2, x, y, w, h, gradient)
NanoVG.nvgFillPaint(vg, nvgPaint)
NanoVG.nvgFill(vg)
}
override fun dropShadow(x: Float, y: Float, width: Float, height: Float, blur: Float, spread: Float, shadowColor: Int, radius: Float) {
val r = shadowColor.red.toByte()
val g = shadowColor.green.toByte()
val b = shadowColor.blue.toByte()
NanoVG.nvgRGBA(r, g, b, 125, nvgColor)
NanoVG.nvgRGBA(r, g, b, 0, nvgColor2)
NanoVG.nvgBoxGradient(
vg,
x - spread,
y - spread,
width + 2 * spread,
height + 2 * spread,
radius + spread,
blur,
nvgColor,
nvgColor2,
nvgPaint
)
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRect(
vg,
x - spread - blur,
y - spread - blur,
width + 2 * spread + 2 * blur,
height + 2 * spread + 2 * blur,
radius + spread
)
NanoVG.nvgRoundedRect(vg, x, y, width, height, radius)
NanoVG.nvgPathWinding(vg, NanoVG.NVG_HOLE)
NanoVG.nvgFillPaint(vg, nvgPaint)
NanoVG.nvgFill(vg)
}
override fun circle(x: Float, y: Float, radius: Float, color: Int) {
NanoVG.nvgBeginPath(vg)
NanoVG.nvgCircle(vg, x, y, radius)
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgFill(vg)
}
override fun text(text: String, x: Float, y: Float, size: Float, color: Int, font: Font) {
NanoVG.nvgFontSize(vg, size)
NanoVG.nvgFontFaceId(vg, getFontID(font))
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgText(vg, x, y + .5f, text)
}
override fun shadowedText(text: String, x: Float, y: Float, size: Float, color: Int, font: Font, shadowColor: Int, offsetX: Float, offsetY: Float, blur: Float) {
NanoVG.nvgFontFaceId(vg, getFontID(font))
NanoVG.nvgFontSize(vg, size)
NanoVG.nvgFontBlur(vg, blur)
color(shadowColor)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgText(vg, x + offsetX, y + offsetY, text)
NanoVG.nvgFontBlur(vg, 0f)
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgText(vg, x, y + .5f, text)
}
override fun textWidth(text: String, size: Float, font: Font): Float {
NanoVG.nvgFontSize(vg, size)
NanoVG.nvgFontFaceId(vg, getFontID(font))
return NanoVG.nvgTextBounds(vg, 0f, 0f, text, fontBounds)
}
override fun wrappedText(text: String, x: Float, y: Float, w: Float, size: Float, color: Int, font: Font, lineHeight: Float) {
NanoVG.nvgFontSize(vg, size)
NanoVG.nvgFontFaceId(vg, getFontID(font))
NanoVG.nvgTextLineHeight(vg, lineHeight)
color(color)
NanoVG.nvgFillColor(vg, nvgColor)
NanoVG.nvgTextBox(vg, x, y, w, text)
}
override fun textBounds(text: String, w: Float, size: Float, font: Font, lineHeight: Float): FloatArray {
val bounds = FloatArray(4)
NanoVG.nvgFontSize(vg, size)
NanoVG.nvgFontFaceId(vg, getFontID(font))
NanoVG.nvgTextLineHeight(vg, lineHeight)
NanoVG.nvgTextBoxBounds(vg, 0f, 0f, w, text, bounds)
return bounds // [minX, minY, maxX, maxY]
}
override fun image(image: Int, textureWidth: Int, textureHeight: Int, subX: Int, subY: Int, subW: Int, subH: Int, x: Float, y: Float, w: Float, h: Float, radius: Float) {
if (image == -1) return
val sx = subX.toFloat() / textureWidth
val sy = subY.toFloat() / textureHeight
val sw = subW.toFloat() / textureWidth
val sh = subH.toFloat() / textureHeight
val iw = w / sw
val ih = h / sh
val ix = x - iw * sx
val iy = y - ih * sy
NanoVG.nvgImagePattern(vg, ix, iy, iw, ih, 0f, image, 1f, nvgPaint)
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRect(vg, x, y, w, h + .5f, radius)
NanoVG.nvgFillPaint(vg, nvgPaint)
NanoVG.nvgFill(vg)
}
override fun image(image: Image, x: Float, y: Float, w: Float, h: Float, radius: Float) {
NanoVG.nvgImagePattern(vg, x, y, w, h, 0f, getImage(image), 1f, nvgPaint)
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRoundedRect(vg, x, y, w, h + .5f, radius)
NanoVG.nvgFillPaint(vg, nvgPaint)
NanoVG.nvgFill(vg)
}
override fun svg(id: String, x: Float, y: Float, w: Float, h: Float, alpha: Float) {
val nvg = getImage(Image(id))
NanoVG.nvgImagePattern(vg, x, y, w, h, 0f, nvg, alpha, nvgPaint)
NanoVG.nvgBeginPath(vg)
NanoVG.nvgRect(vg, x, y, w, h + .5f)
NanoVG.nvgFillPaint(vg, nvgPaint)
NanoVG.nvgFill(vg)
}
fun createNVGImage(glId: Int, width: Int, height: Int): Int {
if (vg == -1L) return -1
return NanoVGGL3.nvglCreateImageFromHandle(vg, glId, width, height, 0)
}
override fun createImage(resourcePath: String, width: Int, height: Int, color: Int): Image {
val image = Image(resourcePath)
if (image.isSVG) {
images.getOrPut(image) { NVGImage(0, loadSVG(image, width, height, color)) }.count++
} else {
images.getOrPut(image) { NVGImage(0, loadImage(image)) }.count++
}
return image
}
override fun cleanCache() {
val iter = images.entries.iterator()
while (iter.hasNext()) {
val entry = iter.next()
NanoVG.nvgDeleteImage(vg, entry.value.nvg)
iter.remove()
}
}
override fun deleteImage(image: Image) {
val nvgImage = images[image] ?: return
nvgImage.count--
if (nvgImage.count == 0) {
NanoVG.nvgDeleteImage(vg, nvgImage.nvg)
images.remove(image)
}
}
private fun getImage(image: Image): Int {
return images[image]?.nvg ?: throw IllegalStateException("Image (${image.identifier}) doesn't exist")
}
private fun loadImage(image: Image): Int {
val w = IntArray(1)
val h = IntArray(1)
val channels = IntArray(1)
val buffer = STBImage.stbi_load_from_memory(image.buffer(), w, h, channels, 4)
?: throw NullPointerException("Failed to load image: ${image.identifier}")
return NanoVG.nvgCreateImageRGBA(vg, w[0], h[0], 0, buffer)
}
private fun loadSVG(image: Image, svgWidth: Int, svgHeight: Int, color: Int): Int {
var vec = image.stream.use { it.bufferedReader().readText() }
val hexColor = "#%06X".format(color and 0xFFFFFF)
vec = vec.replace("currentColor", hexColor)
val svg = NanoSVG.nsvgParse(vec, "px", 96f)
?: throw IllegalStateException("Failed to parse ${image.identifier}")
val width = if (svgWidth > 0) svgWidth else svg.width().toInt()
val height = if (svgHeight > 0) svgHeight else svg.height().toInt()
val buffer = MemoryUtil.memAlloc(width * height * 4)
val previousTexture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D)
try {
val rasterizer = NanoSVG.nsvgCreateRasterizer()
NanoSVG.nsvgRasterize(rasterizer, svg, 0f, 0f, width.toFloat() / svg.width(), buffer, width, height, width * 4)
val nvgImage = NanoVG.nvgCreateImageRGBA(vg, width, height, 0, buffer)
NanoSVG.nsvgDeleteRasterizer(rasterizer)
GL11.glBindTexture(GL11.GL_TEXTURE_2D, previousTexture)
return nvgImage
} finally {
NanoSVG.nsvgDelete(svg)
MemoryUtil.memFree(buffer)
}
}
private fun color(color: Int) {
NanoVG.nvgRGBA(color.red.toByte(), color.green.toByte(), color.blue.toByte(), color.alpha.toByte(), nvgColor)
}
private fun color(color1: Int, color2: Int) {
NanoVG.nvgRGBA(color1.red.toByte(), color1.green.toByte(), color1.blue.toByte(), color1.alpha.toByte(), nvgColor)
NanoVG.nvgRGBA(color2.red.toByte(), color2.green.toByte(), color2.blue.toByte(), color2.alpha.toByte(), nvgColor2)
}
private fun gradient(color1: Int, color2: Int, x: Float, y: Float, w: Float, h: Float, direction: Gradient) {
color(color1, color2)
when (direction) {
Gradient.LeftToRight -> NanoVG.nvgLinearGradient(vg, x, y, x + w, y, nvgColor, nvgColor2, nvgPaint)
Gradient.TopToBottom -> NanoVG.nvgLinearGradient(vg, x, y, x, y + h, nvgColor, nvgColor2, nvgPaint)
Gradient.TopLeftToBottomRight -> NanoVG.nvgLinearGradient(vg, x, y, x + w, y + h, nvgColor, nvgColor2, nvgPaint)
}
}
private fun getFontID(font: Font): Int {
return fontMap.getOrPut(font) {
val buffer = font.buffer()
NVGFont(NanoVG.nvgCreateFontMem(vg, font.name, buffer, false), buffer)
}.id
}
private class Scissor(val previous: Scissor?, val x: Float, val y: Float, val maxX: Float, val maxY: Float) {
fun applyScissor() {
if (previous == null) NanoVG.nvgScissor(vg, x, y, maxX - x, maxY - y)
else {
val x = max(x, previous.x)
val y = max(y, previous.y)
val width = max(0f, (min(maxX, previous.maxX) - x))
val height = max(0f, (min(maxY, previous.maxY) - y))
NanoVG.nvgScissor(vg, x, y, width, height)
}
}
}
private data class NVGImage(var count: Int, val nvg: Int)
private data class NVGFont(val id: Int, val buffer: ByteBuffer)
}

View file

@ -0,0 +1,51 @@
package xyz.meowing.lattice.render
import xyz.meowing.lattice.Lattice.defaultFont
interface Renderer {
fun beginFrame(width: Float, height: Float)
fun endFrame()
fun push()
fun pop()
fun scale(x: Float, y: Float)
fun translate(x: Float, y: Float)
fun rotate(amount: Float)
fun globalAlpha(amount: Float)
fun pushScissor(x: Float, y: Float, w: Float, h: Float)
fun popScissor()
fun line(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, color: Int)
fun rect(x: Float, y: Float, w: Float, h: Float, color: Int)
fun rect(x: Float, y: Float, w: Float, h: Float, color: Int, radius: Float)
fun rect(x: Float, y: Float, w: Float, h: Float, color: Int, radius: Float, roundTop: Boolean)
fun rect(x: Float, y: Float, w: Float, h: Float, color: Int, topRight: Float, topLeft: Float, bottomRight: Float, bottomLeft: Float)
fun gradientRect(x: Float, y: Float, w: Float, h: Float, color1: Int, color2: Int, gradient: Gradient, radius: Float)
fun hollowRect(x: Float, y: Float, w: Float, h: Float, thickness: Float, color: Int, radius: Float)
fun hollowGradientRect(x: Float, y: Float, w: Float, h: Float, thickness: Float, color1: Int, color2: Int, gradient: Gradient, radius: Float)
fun circle(x: Float, y: Float, radius: Float, color: Int)
fun dropShadow(x: Float, y: Float, width: Float, height: Float, blur: Float, spread: Float, shadowColor: Int, radius: Float)
fun text(text: String, x: Float, y: Float, size: Float, color: Int, font: Font = defaultFont)
fun wrappedText(text: String, x: Float, y: Float, w: Float, size: Float, color: Int, font: Font = defaultFont, lineHeight: Float = 1f)
fun shadowedText(text: String, x: Float, y: Float, size: Float, color: Int, font: Font = defaultFont, shadowColor: Int = 0x80000000.toInt(), offsetX: Float = 1.5f, offsetY: Float = 1.5f, blur: Float = 2f)
fun textWidth(text: String, size: Float, font: Font = defaultFont): Float
fun textBounds(text: String, w: Float, size: Float, font: Font = defaultFont, lineHeight: Float = 1f): FloatArray
fun image(image: Int, textureWidth: Int, textureHeight: Int, subX: Int, subY: Int, subW: Int, subH: Int, x: Float, y: Float, w: Float, h: Float, radius: Float)
fun image(image: Image, x: Float, y: Float, w: Float, h: Float, radius: Float = 0f)
fun svg(id: String, x: Float, y: Float, w: Float, h: Float, alpha: Float = 1f)
fun createImage(resourcePath: String, width: Int = -1, height: Int = -1, color: Int = 0xFFFFFFFF.toInt()): Image
fun deleteImage(image: Image)
fun cleanCache()
}

View file

@ -0,0 +1,27 @@
package xyz.meowing.lattice.render
import com.mojang.blaze3d.platform.Window
import xyz.meowing.lattice.client.Client.minecraft
object Resolution {
@JvmStatic
val window: Window get() = minecraft.window
@JvmStatic
val windowHandle: Long get() = window.handle()
@JvmStatic
val windowWidth: Int get() = window.getWidth()
@JvmStatic
val windowHeight: Int get() = window.getHeight()
@JvmStatic
val scaledWidth: Int get() = window.getGuiScaledWidth()
@JvmStatic
val scaledHeight: Int get() = window.getGuiScaledHeight()
@JvmStatic
val scaleFactor: Double get() = window.getGuiScale().toDouble()
}

View file

@ -0,0 +1,85 @@
package xyz.meowing.lattice.scheduler
import org.apache.logging.log4j.LogManager
import xyz.meowing.lattice.Lattice
import xyz.meowing.lattice.event.TickEvent
import java.util.PriorityQueue
/** Runs actions on the client tick. Wired to the event bus on first use. */
object TickScheduler {
private val logger = LogManager.getLogger(TickScheduler::class.java)
private val queue = PriorityQueue<Task>(compareBy { it.executeTick })
private var nextId = 0L
var currentTick = 0L
private set
interface Handle {
val isCancelled: Boolean
fun cancel()
}
private class Task(
var executeTick: Long,
val action: () -> Unit,
val interval: () -> Long = { 0 }
) : Handle {
override var isCancelled = false
private set
override fun cancel() {
isCancelled = true
}
}
init {
Lattice.eventBus.register<TickEvent.Start> { onTick() }
}
private fun onTick() {
currentTick++
while (true) {
val task = synchronized(queue) {
queue.peek()?.takeIf { currentTick >= it.executeTick }?.let { queue.poll() }
} ?: break
if (task.isCancelled) continue
try {
task.action()
} catch (e: Exception) {
logger.error("Caught error while trying to run action", e)
}
val interval = task.interval()
if (interval > 0 && !task.isCancelled) {
task.executeTick = currentTick + interval
synchronized(queue) { queue.offer(task) }
}
}
}
fun post(action: () -> Unit) {
schedule(1, action)
}
fun schedule(delay: Long, action: () -> Unit): Handle {
val task = Task(currentTick + delay, action)
synchronized(queue) { queue.offer(task) }
return task
}
fun repeat(interval: Long, initialDelay: Long = interval, action: () -> Unit): Handle {
return repeatDynamic({ interval }, initialDelay, action)
}
fun repeatDynamic(
intervalProvider: () -> Long,
initialDelay: Long = intervalProvider(),
action: () -> Unit
): Handle {
val task = Task(currentTick + initialDelay, action, intervalProvider)
synchronized(queue) { queue.offer(task) }
return task
}
}

View file

@ -0,0 +1,93 @@
package xyz.meowing.lattice.scheduler
import org.apache.logging.log4j.LogManager
import org.jetbrains.annotations.ApiStatus
import xyz.meowing.lattice.Lattice
import xyz.meowing.lattice.event.ClientEvent
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
/** Runs actions on background threads using wall-clock delays. */
object TimeScheduler {
private val executor = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors())
private val tasks = ConcurrentHashMap<Long, ScheduledFuture<*>>()
private val logger = LogManager.getLogger(TimeScheduler::class.java)
private val nextId = AtomicLong()
interface Handle {
val isCancelled: Boolean
fun cancel()
}
init {
Lattice.eventBus.register<ClientEvent.Stop> { shutdown() }
}
fun schedule(delayMillis: Long, action: () -> Unit): Handle {
val id = nextId.getAndIncrement()
tasks[id] = executor.schedule({
runSafely(action)
tasks.remove(id)
}, delayMillis, TimeUnit.MILLISECONDS)
return createHandle(id)
}
fun repeat(
intervalMillis: Long,
initialDelayMillis: Long = intervalMillis,
stopCondition: () -> Boolean = { false },
action: () -> Unit
): Handle {
return repeatDynamic({ intervalMillis }, initialDelayMillis, stopCondition, action)
}
fun repeatDynamic(
intervalProvider: () -> Long,
initialDelayMillis: Long = intervalProvider(),
stopCondition: () -> Boolean = { false },
action: () -> Unit
): Handle {
val id = nextId.getAndIncrement()
fun scheduleNext(delay: Long) {
if (stopCondition()) {
tasks.remove(id)
return
}
tasks[id] = executor.schedule({
runSafely(action)
if (tasks.containsKey(id)) scheduleNext(intervalProvider())
}, delay, TimeUnit.MILLISECONDS)
}
scheduleNext(initialDelayMillis)
return createHandle(id)
}
@ApiStatus.Internal
fun shutdown() {
tasks.clear()
executor.shutdown()
}
private fun createHandle(id: Long): Handle {
return object : Handle {
override val isCancelled: Boolean get() = !tasks.containsKey(id)
override fun cancel() {
tasks.remove(id)?.cancel(false)
}
}
}
private fun runSafely(action: () -> Unit) {
try {
action()
} catch (e: Exception) {
logger.error("Caught error while trying to run action", e)
}
}
}

View file

@ -0,0 +1,42 @@
package xyz.meowing.lattice.text
import xyz.meowing.lattice.text.Texts
import net.minecraft.network.chat.Component as VanillaText
class ChainBuilder {
private val parts = mutableListOf<TextBuilder>()
fun text(content: String, builder: TextBuilder.() -> Unit = {}): ChainBuilder {
parts.add(Texts.literal(content).apply(builder))
return this
}
fun append(builder: TextBuilder): ChainBuilder {
parts.add(builder)
return this
}
fun newLine(): ChainBuilder {
parts.add(Texts.literal("\n"))
return this
}
fun space(): ChainBuilder {
parts.add(Texts.literal(" "))
return this
}
fun build(): TextBuilder {
if (parts.isEmpty()) return Texts.empty()
val result = parts.first()
for (i in 1 until parts.size) {
result.append(parts[i])
}
return result
}
fun toVanilla(): VanillaText {
return build().toVanilla()
}
}

View file

@ -0,0 +1,11 @@
package xyz.meowing.lattice.text
import java.net.URI
sealed interface ClickEvent {
data class OpenUrl(val url: URI) : ClickEvent
data class RunCommand(val command: String) : ClickEvent
data class SuggestCommand(val command: String) : ClickEvent
data class CopyToClipboard(val text: String) : ClickEvent
data class ChangePage(val page: Int) : ClickEvent
}

View file

@ -0,0 +1,23 @@
package xyz.meowing.lattice.text
object ColorCodes {
const val BLACK: Int = 0x000000
const val DARK_BLUE: Int = 0x0000AA
const val DARK_GREEN: Int = 0x00AA00
const val DARK_AQUA: Int = 0x00AAAA
const val DARK_RED: Int = 0xAA0000
const val DARK_PURPLE: Int = 0xAA00AA
const val GOLD: Int = 0xFFAA00
const val GRAY: Int = 0xAAAAAA
const val DARK_GRAY: Int = 0x555555
const val BLUE: Int = 0x5555FF
const val GREEN: Int = 0x55FF55
const val AQUA: Int = 0x55FFFF
const val RED: Int = 0xFF5555
const val LIGHT_PURPLE: Int = 0xFF55FF
const val YELLOW: Int = 0xFFFF55
const val WHITE: Int = 0xFFFFFF
@JvmStatic
fun hex(hex: String): Int = hex.removePrefix("#").toInt(16)
}

View file

@ -0,0 +1,42 @@
package xyz.meowing.lattice.text
object FormattingCodes {
const val SECTION = '§'
private val stripRegex = "$SECTION[0-9a-fk-or]".toRegex(RegexOption.IGNORE_CASE)
@JvmStatic
fun codeToColor(code: Char): Int? = when (code.lowercaseChar()) {
'0' -> ColorCodes.BLACK
'1' -> ColorCodes.DARK_BLUE
'2' -> ColorCodes.DARK_GREEN
'3' -> ColorCodes.DARK_AQUA
'4' -> ColorCodes.DARK_RED
'5' -> ColorCodes.DARK_PURPLE
'6' -> ColorCodes.GOLD
'7' -> ColorCodes.GRAY
'8' -> ColorCodes.DARK_GRAY
'9' -> ColorCodes.BLUE
'a' -> ColorCodes.GREEN
'b' -> ColorCodes.AQUA
'c' -> ColorCodes.RED
'd' -> ColorCodes.LIGHT_PURPLE
'e' -> ColorCodes.YELLOW
'f' -> ColorCodes.WHITE
else -> null
}
@JvmStatic
fun strip(text: String): String = text.replace(stripRegex, "")
@JvmStatic
fun translateAlternate(altChar: Char, text: String): String {
val chars = text.toCharArray()
for (i in 0 until chars.size - 1) {
if (chars[i] == altChar && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(chars[i + 1]) > -1) {
chars[i] = SECTION
chars[i + 1] = chars[i + 1].lowercaseChar()
}
}
return String(chars)
}
}

View file

@ -0,0 +1,9 @@
package xyz.meowing.lattice.text
import xyz.meowing.lattice.text.TextBuilder
import net.minecraft.world.item.ItemStackTemplate
sealed interface HoverEvent {
data class ShowText(val text: TextBuilder) : HoverEvent
data class ShowItem(val stack: ItemStackTemplate) : HoverEvent
}

View file

@ -0,0 +1,258 @@
package xyz.meowing.lattice.text
import net.minecraft.network.chat.ClickEvent as ModernClickEvent
import net.minecraft.network.chat.HoverEvent as ModernHoverEvent
import xyz.meowing.lattice.text.Texts
import xyz.meowing.lattice.text.ClickEvent
import xyz.meowing.lattice.text.HoverEvent
import xyz.meowing.lattice.text.ColorCodes
import java.net.URI
import net.minecraft.network.chat.Component as VanillaText
import net.minecraft.network.chat.TextColor
class TextBuilder internal constructor(
internal var text: String
) {
internal var vanilla: VanillaText? = null
private val siblings = mutableListOf<TextBuilder>()
private var color: Int? = null
private var bold: Boolean? = null
private var italic: Boolean? = null
private var underlined: Boolean? = null
private var strikethrough: Boolean? = null
private var obfuscated: Boolean? = null
private var clickEvent: ClickEvent? = null
private var hoverEvent: HoverEvent? = null
private var insertion: String? = null
fun color(hex: String): TextBuilder {
color = hex.removePrefix("#").toInt(16)
return this
}
fun color(rgb: Int): TextBuilder {
color = rgb
return this
}
fun black(): TextBuilder = color(ColorCodes.BLACK)
fun darkBlue(): TextBuilder = color(ColorCodes.DARK_BLUE)
fun darkGreen(): TextBuilder = color(ColorCodes.DARK_GREEN)
fun darkAqua(): TextBuilder = color(ColorCodes.DARK_AQUA)
fun darkRed(): TextBuilder = color(ColorCodes.DARK_RED)
fun darkPurple(): TextBuilder = color(ColorCodes.DARK_PURPLE)
fun gold(): TextBuilder = color(ColorCodes.GOLD)
fun gray(): TextBuilder = color(ColorCodes.GRAY)
fun darkGray(): TextBuilder = color(ColorCodes.DARK_GRAY)
fun blue(): TextBuilder = color(ColorCodes.BLUE)
fun green(): TextBuilder = color(ColorCodes.GREEN)
fun aqua(): TextBuilder = color(ColorCodes.AQUA)
fun red(): TextBuilder = color(ColorCodes.RED)
fun lightPurple(): TextBuilder = color(ColorCodes.LIGHT_PURPLE)
fun yellow(): TextBuilder = color(ColorCodes.YELLOW)
fun white(): TextBuilder = color(ColorCodes.WHITE)
fun bold(value: Boolean = true): TextBuilder {
bold = value
return this
}
fun italic(value: Boolean = true): TextBuilder {
italic = value
return this
}
fun underlined(value: Boolean = true): TextBuilder {
underlined = value
return this
}
fun strikethrough(value: Boolean = true): TextBuilder {
strikethrough = value
return this
}
fun obfuscated(value: Boolean = true): TextBuilder {
obfuscated = value
return this
}
fun reset(): TextBuilder {
color = null
bold = null
italic = null
underlined = null
strikethrough = null
obfuscated = null
return this
}
fun onClick(event: ClickEvent): TextBuilder {
clickEvent = event
return this
}
fun onClick(url: String): TextBuilder {
clickEvent = ClickEvent.OpenUrl(URI.create(url))
return this
}
fun onHover(event: HoverEvent): TextBuilder {
hoverEvent = event
return this
}
fun onHover(text: String): TextBuilder {
hoverEvent = HoverEvent.ShowText(Texts.literal(text))
return this
}
fun onHover(builder: TextBuilder): TextBuilder {
hoverEvent = HoverEvent.ShowText(builder)
return this
}
fun insertion(text: String): TextBuilder {
insertion = text
return this
}
fun append(builder: TextBuilder): TextBuilder {
siblings.add(builder)
return this
}
fun append(text: String): TextBuilder {
siblings.add(Texts.literal(text))
return this
}
fun appendLine(): TextBuilder {
siblings.add(Texts.literal("\n"))
return this
}
fun appendLine(text: String): TextBuilder {
siblings.add(Texts.literal(text))
siblings.add(Texts.literal("\n"))
return this
}
fun appendLine(builder: TextBuilder): TextBuilder {
siblings.add(builder)
siblings.add(Texts.literal("\n"))
return this
}
fun suggestCommand(command: String): TextBuilder {
clickEvent = ClickEvent.SuggestCommand(command)
return this
}
fun runCommand(command: String): TextBuilder {
clickEvent = ClickEvent.RunCommand(command)
return this
}
fun copyToClipboard(text: String): TextBuilder {
clickEvent = ClickEvent.CopyToClipboard(text)
return this
}
fun openUrl(url: String): TextBuilder {
clickEvent = ClickEvent.OpenUrl(URI.create(url))
return this
}
fun changePage(page: Int): TextBuilder {
clickEvent = ClickEvent.ChangePage(page)
return this
}
fun build(): VanillaText {
vanilla?.let { return it }
val base = VanillaText.literal(text)
var style = base.getStyle()
color?.let {
//#if MC >= 1.21.5
style = style.withColor(TextColor.fromRgb(it))
//#else
//$$ style = style.withColor(it)
//#endif
}
bold?.let { style = style.withBold(it) }
italic?.let { style = style.withItalic(it) }
underlined?.let { style = style.withUnderlined(it) }
strikethrough?.let { style = style.withStrikethrough(it) }
obfuscated?.let { style = style.withObfuscated(it) }
insertion?.let { style = style.withInsertion(it) }
clickEvent?.let {
style = style.withClickEvent(when (it) {
//#if MC >= 1.21.5
is ClickEvent.OpenUrl -> ModernClickEvent.OpenUrl(it.url)
is ClickEvent.RunCommand -> ModernClickEvent.RunCommand(it.command)
is ClickEvent.SuggestCommand -> ModernClickEvent.SuggestCommand(it.command)
is ClickEvent.CopyToClipboard -> ModernClickEvent.CopyToClipboard(it.text)
is ClickEvent.ChangePage -> ModernClickEvent.ChangePage(it.page)
//#else
//$$ is ClickEvent.OpenUrl -> ModernClickEvent(ModernClickEvent.Action.OPEN_URL, it.url.toString())
//$$ is ClickEvent.RunCommand -> ModernClickEvent(ModernClickEvent.Action.RUN_COMMAND, it.command)
//$$ is ClickEvent.SuggestCommand -> ModernClickEvent(ModernClickEvent.Action.SUGGEST_COMMAND, it.command)
//$$ is ClickEvent.CopyToClipboard -> ModernClickEvent(ModernClickEvent.Action.COPY_TO_CLIPBOARD, it.text)
//$$ is ClickEvent.ChangePage -> ModernClickEvent(ModernClickEvent.Action.CHANGE_PAGE, it.page.toString())
//#endif
})
}
hoverEvent?.let {
style = style.withHoverEvent(when (it) {
//#if MC >= 1.21.5
is HoverEvent.ShowText -> ModernHoverEvent.ShowText(it.text.build())
is HoverEvent.ShowItem -> ModernHoverEvent.ShowItem(it.stack)
//#else
//$$ is HoverEvent.ShowText -> ModernHoverEvent(ModernHoverEvent.Action.SHOW_TEXT, it.text.build())
//$$ is HoverEvent.ShowItem -> {
//$$ ModernHoverEvent(
//$$ ModernHoverEvent.Action.SHOW_ITEM,
//#if FABRIC
//$$ ModernHoverEvent.ItemStackContent(it.stack)
//#else
//$$ ModernHoverEvent.ItemStackInfo(it.stack)
//#endif
//$$ )
//$$ }
//#endif
})
}
base.setStyle(style)
siblings.forEach { base.append(it.build()) }
return base
}
fun toVanilla(): VanillaText {
return build()
}
fun string(): String {
return build().string
}
fun formatted(): String {
return build().string
}
operator fun plus(other: TextBuilder): TextBuilder {
return this.append(other)
}
operator fun plus(other: String): TextBuilder {
return this.append(other)
}
}

View file

@ -0,0 +1,100 @@
package xyz.meowing.lattice.text
import xyz.meowing.lattice.text.ChainBuilder
import xyz.meowing.lattice.text.TextBuilder
import xyz.meowing.lattice.text.FormattingCodes
import net.minecraft.network.chat.Component as VanillaText
object Texts {
@JvmStatic
fun literal(text: String): TextBuilder = TextBuilder(text)
@JvmStatic
fun empty(): TextBuilder = TextBuilder("")
@JvmStatic
fun fromVanilla(
text: VanillaText
): TextBuilder {
val builder = TextBuilder("")
builder.vanilla = text
return builder
}
@JvmStatic
fun fromFormatted(text: String): TextBuilder {
val parts = text.split(FormattingCodes.SECTION)
if (parts.size <= 1) return literal(text)
val root = empty()
var current = literal("")
for (i in parts.indices) {
if (i == 0 && parts[i].isNotEmpty()) {
root.append(literal(parts[i]))
continue
}
val part = parts[i]
if (part.isEmpty()) continue
val code = part[0].lowercaseChar()
val content = if (part.length > 1) part.substring(1) else ""
when (code) {
in '0'..'9', in 'a'..'f' -> {
if (current.text.isNotEmpty()) root.append(current)
current = literal(content)
FormattingCodes.codeToColor(code)?.let { current.color(it) }
}
'r' -> {
if (current.text.isNotEmpty()) root.append(current)
current = literal(content)
}
'l' -> {
current.bold()
current.text += content
}
'o' -> {
current.italic()
current.text += content
}
'n' -> {
current.underlined()
current.text += content
}
'm' -> {
current.strikethrough()
current.text += content
}
'k' -> {
current.obfuscated()
current.text += content
}
else -> current.text += FormattingCodes.SECTION + part
}
}
if (current.text.isNotEmpty()) root.append(current)
return root
}
@JvmStatic
fun builder(): ChainBuilder = ChainBuilder()
}
fun String.asText(): TextBuilder = Texts.literal(this)
fun String.asFormattedText(): TextBuilder = Texts.fromFormatted(this)
fun VanillaText.asBuilder(): TextBuilder = Texts.fromVanilla(this)
fun VanillaText.toBuilder(): TextBuilder = Texts.fromVanilla(this)
fun text(content: String, builder: TextBuilder.() -> Unit = {}): TextBuilder {
return Texts.literal(content).apply(builder)
}
fun buildText(builder: ChainBuilder.() -> Unit): TextBuilder {
return Texts.builder().apply(builder).build()
}

View file

@ -0,0 +1,351 @@
@file:Suppress("UNCHECKED_CAST")
package xyz.meowing.lattice.ui
import xyz.meowing.lattice.ui.theme.Theme
import xyz.meowing.lattice.Lattice.renderer
import xyz.meowing.lattice.animation.EasingType
import xyz.meowing.lattice.animation.fadeIn
import xyz.meowing.lattice.animation.fadeOut
/** Base for elements with padding and optional vertical scrolling. */
abstract class Box<T : Box<T>>(
var padding: FloatArray = floatArrayOf(0f, 0f, 0f, 0f),
var scrollable: Boolean = false,
widthType: Size = Size.Auto,
heightType: Size = Size.Auto
) : Element<T>(widthType, heightType) {
var scrollOffset: Float = 0f
var showScrollbar: Boolean = true
var scrollbarWidth: Float = 6f
var scrollbarColor: Int = Theme.scrollbar
var scrollbarRadius: Float = 3f
var scrollbarPadding: Float = 0f
var scissorBufferVertical: Float = 0f
var scissorBufferHorizontal: Float = 2f
private var isDraggingScrollbar = false
private var scrollbarDragOffset = 0f
private val viewWidth: Float get() = width - padding[1] - padding[3]
private val viewHeight: Float get() = height - padding[0] - padding[2]
private fun drawScrollbar() {
if (!scrollable || !showScrollbar) return
val contentHeight = getContentHeight()
if (contentHeight <= viewHeight) return
val scrollbarX = x + width - padding[1] - scrollbarWidth - scrollbarPadding
val scrollbarHeight = (viewHeight / contentHeight) * viewHeight
val scrollbarY = y + padding[0] + (scrollOffset / contentHeight) * viewHeight
renderer.rect(scrollbarX, scrollbarY, scrollbarWidth, scrollbarHeight, scrollbarColor, scrollbarRadius)
}
private fun isPointInScrollbar(mouseX: Float, mouseY: Float): Boolean {
if (!scrollable || !showScrollbar) return false
val contentHeight = getContentHeight()
if (contentHeight <= viewHeight) return false
val scrollbarX = x + width - padding[1] - scrollbarWidth - scrollbarPadding
val scrollbarHeight = (viewHeight / contentHeight) * viewHeight
val scrollbarY = y + padding[0] + (scrollOffset / contentHeight) * viewHeight
return mouseX >= scrollbarX && mouseX <= scrollbarX + scrollbarWidth &&
mouseY >= scrollbarY && mouseY <= scrollbarY + scrollbarHeight
}
protected fun updateHoverStates(mouseX: Float, mouseY: Float) {
val adjustedMouseY = if (scrollable) mouseY + scrollOffset else mouseY
val wasHovered = isHovered
isHovered = isPointInside(mouseX, mouseY)
when {
isHovered && !wasHovered -> {
listeners.mouseEnter.forEach { it(MouseEvent.Move.Enter(mouseX, mouseY, this)) }
onHoverChanged(true)
}
!isHovered && wasHovered -> {
listeners.mouseExit.forEach { it(MouseEvent.Move.Exit(mouseX, mouseY, this)) }
onHoverChanged(false)
}
}
if (isHovered) listeners.mouseMove.forEach { it(MouseEvent.Move(mouseX, mouseY, this)) }
children.reversed().forEach { child ->
if (scrollable && !isMouseOnVisible(mouseX, mouseY)) {
unhoverRecursive(child, mouseX, adjustedMouseY)
} else {
child.handleMouseMove(mouseX, adjustedMouseY)
}
}
}
protected open fun onHoverChanged(hovered: Boolean) {
tooltipElement?.let { tooltip ->
if (hovered) {
tooltip.fadeIn(Theme.animNormal, EasingType.EASE_OUT)
tooltip.innerText.fadeIn(Theme.animNormal, EasingType.EASE_OUT)
} else {
tooltip.fadeOut(Theme.animNormal, EasingType.EASE_OUT)
tooltip.innerText.fadeOut(Theme.animNormal, EasingType.EASE_OUT)
}
}
}
private fun unhoverRecursive(element: Element<*>, mouseX: Float, mouseY: Float) {
if (element.isHovered) {
element.isHovered = false
element.listeners.mouseExit.forEach { it(MouseEvent.Move.Exit(mouseX, mouseY, this)) }
}
element.children.forEach { unhoverRecursive(it, mouseX, mouseY) }
}
override fun handleMouseScroll(mouseX: Float, mouseY: Float, horizontal: Double, vertical: Double): Boolean {
if (!visible) return false
if (!isMouseOnVisible(mouseX, mouseY)) return false
val adjustedMouseY = if (scrollable) mouseY + scrollOffset else mouseY
val childHandled = children.reversed().any { it.handleMouseScroll(mouseX, adjustedMouseY, horizontal, vertical) }
if (!childHandled && scrollable && isPointInside(mouseX, mouseY)) {
val contentHeight = getContentHeight()
if (contentHeight > viewHeight) {
val scrollAmount = vertical.toFloat() * -30f
val maxScroll = contentHeight - viewHeight
scrollOffset = (scrollOffset + scrollAmount).coerceIn(0f, maxScroll)
updateHoverStates(mouseX, mouseY)
return true
}
}
return childHandled
}
override fun handleMouseMove(mouseX: Float, mouseY: Float): Boolean {
if (!visible) return false
if (isDraggingScrollbar) {
val contentHeight = getContentHeight()
val relativeY = mouseY - scrollbarDragOffset - (y + padding[0])
val scrollRatio = (relativeY / viewHeight).coerceIn(0f, 1f)
val maxScroll = contentHeight - viewHeight
scrollOffset = (scrollRatio * maxScroll).coerceIn(0f, maxScroll)
updateHoverStates(mouseX, mouseY)
return true
}
updateHoverStates(mouseX, mouseY)
return isHovered
}
override fun handleMouseClick(mouseX: Float, mouseY: Float, button: Int): Boolean {
if (!visible) return false
if (isPointInScrollbar(mouseX, mouseY)) {
isDraggingScrollbar = true
val contentHeight = getContentHeight()
val scrollbarY = y + padding[0] + (scrollOffset / contentHeight) * viewHeight
scrollbarDragOffset = mouseY - scrollbarY
return true
}
if (scrollable && !isMouseOnVisible(mouseX, mouseY)) return false
val adjustedMouseY = if (scrollable) mouseY + scrollOffset else mouseY
val childHandled = children.reversed().any { it.handleMouseClick(mouseX, adjustedMouseY, button) }
return when {
childHandled -> true
isPointInside(mouseX, mouseY) -> {
isPressed = true
focus()
listeners.mouseClick.any { it(MouseEvent.Click(mouseX, mouseY, button, this)) } || listeners.mouseClick.isEmpty()
}
else -> false
}
}
override fun handleMouseRelease(mouseX: Float, mouseY: Float, button: Int): Boolean {
if (!visible) return false
if (isDraggingScrollbar) {
isDraggingScrollbar = false
return true
}
val adjustedMouseY = if (scrollable) mouseY + scrollOffset else mouseY
val wasPressed = isPressed
isPressed = false
val childHandled = if (scrollable && !isMouseOnVisible(mouseX, mouseY)) {
false
} else {
children.reversed().any { it.handleMouseRelease(mouseX, adjustedMouseY, button) }
}
return childHandled ||
(
wasPressed &&
isPointInside(mouseX, mouseY) &&
(
listeners.mouseRelease.any { it(MouseEvent.Release(mouseX, mouseY, button, this)) } ||
listeners.mouseRelease.isEmpty()
)
)
}
fun getContentHeight(): Float {
val visibleChildren = children.filter { !it.isFloating }
if (visibleChildren.isEmpty()) return 0f
val bottomChild = visibleChildren.maxByOrNull { it.y + it.height } ?: return 0f
return bottomChild.y + bottomChild.height - (y + padding[0])
}
fun isMouseOnVisible(mouseX: Float, mouseY: Float): Boolean {
if (!scrollable) return true
val contentX = x + padding[3]
val contentY = y + padding[0]
return mouseX >= contentX && mouseX <= contentX + viewWidth && mouseY >= contentY && mouseY <= contentY + viewHeight
}
public override fun getAutoWidth(): Float {
val visibleChildren = children.filter { it.visible && !it.isFloating }
if (visibleChildren.isEmpty()) return padding[1] + padding[3]
val minX = visibleChildren.minOf { it.x }
val maxX = visibleChildren.maxOf { it.x + it.width }
val calculated = (maxX - minX) + padding[3] + padding[1]
return maxAutoWidth?.let { calculated.coerceAtMost(it) } ?: calculated
}
public override fun getAutoHeight(): Float {
val visibleChildren = children.filter { it.visible && !it.isFloating }
if (visibleChildren.isEmpty()) return padding[0] + padding[2]
val minY = visibleChildren.minOf { it.y }
val maxY = visibleChildren.maxOf { it.y + it.height }
val calculated = (maxY - minY) + padding[0] + padding[2]
return maxAutoHeight?.let { calculated.coerceAtMost(it) } ?: calculated
}
override fun renderChildren(mouseX: Float, mouseY: Float) {
if (scrollable) {
renderer.push()
renderer.pushScissor(
x + padding[3] - scissorBufferHorizontal,
y + padding[0] - scissorBufferVertical,
viewWidth + scissorBufferHorizontal * 2,
viewHeight + scissorBufferVertical * 2
)
renderer.translate(0f, -scrollOffset)
}
children.forEach { it.render(mouseX, mouseY) }
if (scrollable) {
renderer.popScissor()
renderer.pop()
}
if (showScrollbar && (isHovered || isDraggingScrollbar)) drawScrollbar()
}
fun isVisibleInScrollableParents(): Boolean {
var current: Any? = this
while (current != null) {
when (current) {
is Element<*> -> {
if (!current.visible) return false
if (current is Box<*> && current.scrollable) {
val centerX = getScreenX() + width / 2
val centerY = getScreenY() + height / 2
if (!current.isMouseOnVisible(centerX, centerY)) return false
}
current = current.parent
}
else -> break
}
}
return true
}
fun getScreenX(): Float = x
fun getScreenY(): Float {
var totalScrollOffset = 0f
var current = parent
while (current != null) {
when (current) {
is Box<*> -> totalScrollOffset += current.scrollOffset
is Window -> break
}
current = if (current is Element<*>) current.parent else null
}
return y - totalScrollOffset
}
fun scrollable(enabled: Boolean): T {
scrollable = enabled
return this as T
}
fun setScissorBuffer(vertical: Float, horizontal: Float): T {
scissorBufferVertical = vertical
scissorBufferHorizontal = horizontal
return this as T
}
fun showScrollbar(show: Boolean): T {
showScrollbar = show
return this as T
}
fun scrollbarWidth(width: Float): T {
scrollbarWidth = width
return this as T
}
fun scrollbarColor(color: Int): T {
scrollbarColor = color
return this as T
}
fun scrollbarRadius(radius: Float): T {
scrollbarRadius = radius
return this as T
}
fun scrollbarPadding(padding: Float): T {
scrollbarPadding = padding
return this as T
}
fun padding(top: Float = 0f, right: Float = 0f, bottom: Float = 0f, left: Float = 0f): T {
padding[0] = top
padding[1] = right
padding[2] = bottom
padding[3] = left
return this as T
}
fun padding(all: Float): T = padding(all, all, all, all)
fun width(newWidth: Float): T {
width = newWidth
return this as T
}
fun height(newHeight: Float): T {
height = newHeight
return this as T
}
}

View file

@ -0,0 +1,855 @@
@file:Suppress("UNCHECKED_CAST")
package xyz.meowing.lattice.ui
import xyz.meowing.lattice.Lattice.renderer
import xyz.meowing.lattice.animation.AnimationManager
import xyz.meowing.lattice.animation.EasingType
import xyz.meowing.lattice.animation.fadeIn
import xyz.meowing.lattice.animation.fadeOut
import xyz.meowing.lattice.input.Mouse
import xyz.meowing.lattice.render.Resolution
import xyz.meowing.lattice.ui.component.Tooltip
import xyz.meowing.lattice.ui.theme.Theme
abstract class Element<T : Element<T>>(
var widthType: Size = Size.Pixels,
var heightType: Size = Size.Pixels
) {
val children: MutableList<Element<*>> = mutableListOf()
open var renderHitbox = false
open var xPositionConstraint = Pos.ParentPixels
open var yPositionConstraint = Pos.ParentPixels
open var xAlignment = Alignment.None
open var yAlignment = Alignment.None
open var x: Float = 0f
set(value) {
field = value
invalidateChildrenPositions()
}
open var y: Float = 0f
set(value) {
field = value
invalidateChildrenPositions()
}
open var width: Float = 0f
set(value) {
field = value
invalidateChildrenPositions()
invalidateChildrenSizes()
}
open var height: Float = 0f
set(value) {
field = value
invalidateChildrenPositions()
invalidateChildrenSizes()
}
inner class Scaled {
val scaleFactor get() = Resolution.scaleFactor.toFloat()
val left: Float get() = x / scaleFactor
val top: Float get() = y / scaleFactor
val right: Float get() = (x + width) / scaleFactor
val bottom: Float get() = (y + height) / scaleFactor
val centerX: Float get() = (x + width / 2f) / scaleFactor
val centerY: Float get() = (y + height / 2f) / scaleFactor
val width: Float get() = this@Element.width / scaleFactor
val height: Float get() = this@Element.height / scaleFactor
}
inner class Raw {
val left get() = x
val top get() = y
val right get() = x + width
val bottom get() = y + height
val centerX get() = (left + right) / 2f
val centerY get() = (top + bottom) / 2f
val width get() = this@Element.width
val height get() = this@Element.height
}
val raw = Raw()
val scaled = Scaled()
var widthPercent: Float = 100f
var heightPercent: Float = 100f
open var visible: Boolean = true
set(value) {
if (field != value) {
field = value
cache.invalidate()
invalidateChildrenCache()
}
}
open var xConstraint: Float = 0f
open var yConstraint: Float = 0f
open var maxAutoWidth: Float? = null
open var maxAutoHeight: Float? = null
open var xOffset: Float = 0f
open var yOffset: Float = 0f
open var xOffsetType: Offset = Offset.Pixels
open var yOffsetType: Offset = Offset.Pixels
open var isHovered: Boolean = false
open var isPressed: Boolean = false
open var isFocused: Boolean = false
open var isFloating: Boolean = false
open var ignoreFocus: Boolean = false
open var requiresFocus: Boolean = false
val screenWidth: Int get() = Resolution.windowWidth
val screenHeight: Int get() = Resolution.windowHeight
var parent: Any? = null
set(value) {
field = value
cache.invalidate()
}
var tooltipElement: Tooltip? = null
val onValueChange = mutableListOf<(Any) -> Unit>()
val cache = ElementCache()
val listeners = ElementListeners()
// Bookkeeping for animation presets that restore the original geometry.
internal var presetOriginalSize: Pair<Float, Float>? = null
internal var presetOriginalPosition: Pair<Float, Float>? = null
open fun invalidateChildrenCache() {
for (child in children) child.cache.invalidate()
}
open fun invalidateChildrenPositions() {
for (child in children) child.cache.invalidatePosition()
}
open fun invalidateChildrenSizes() {
for (child in children) child.cache.invalidateSize()
}
private fun checkScreenResize() {
val resized = cache.lastScreenWidth != screenWidth || cache.lastScreenHeight != screenHeight
if (resized) {
cache.lastScreenWidth = screenWidth
cache.lastScreenHeight = screenHeight
val screenPosModes = setOf(Pos.ScreenPercent, Pos.ScreenPixels, Pos.ScreenCenter)
if (xPositionConstraint in screenPosModes || xAlignment != Alignment.None) cache.positionCacheValid = false
if (yPositionConstraint in screenPosModes || yAlignment != Alignment.None) cache.positionCacheValid = false
if (widthType == Size.Percent && parent !is Element<*>) cache.sizeCacheValid = false
if (heightType == Size.Percent && parent !is Element<*>) cache.sizeCacheValid = false
}
}
private fun renderDebugHitbox() {
if (!renderHitbox) return
children.forEach { it.enableDebugRendering() }
val color = if (isFocused) 0xFFFFA500.toInt() else if (isHovered) 0xFFFFFF00.toInt() else 0xFF00FFFF.toInt()
renderer.push()
renderer.hollowRect(x, y, width, height, 1f, color, 0f)
renderer.pop()
}
open fun destroy() {
children.toList().forEach { it.destroy() }
children.clear()
listeners.clear()
tooltipElement?.destroy()
tooltipElement = null
onValueChange.clear()
(parent as? Element<*>)?.children?.remove(this)
parent = null
}
fun drawAsRoot() {
renderer.beginFrame(screenWidth.toFloat(), screenHeight.toFloat())
renderer.push()
render(Mouse.Raw.x.toFloat(), Mouse.Raw.y.toFloat())
AnimationManager.update()
renderer.pop()
renderer.endFrame()
}
fun findFirstVisibleParent(): Element<*>? {
if (cache.parentCacheValid) return cache.cachedParent
var current = parent
while (current != null) {
if (current is Element<*> && current.visible) {
cache.cachedParent = current
cache.parentCacheValid = true
return current
}
if (current is Window) {
cache.cachedParent = null
cache.parentCacheValid = true
return null
}
current = (current as? Element<*>)?.parent
}
cache.cachedParent = null
cache.parentCacheValid = true
return null
}
private fun getParentPadding(): FloatArray {
return (findFirstVisibleParent() as? Box<*>)?.padding ?: ZERO_PADDING
}
private fun getSiblingsAfterSize(width: Boolean): Float {
val parentElement = parent as? Element<*> ?: return 0f
val myIndex = parentElement.children.indexOf(this)
if (myIndex == -1) return 0f
return parentElement.children
.drop(myIndex + 1)
.filter { it.visible && !it.isFloating }
.sumOf { (if (width) it.width else it.height).toDouble() }
.toFloat()
}
open fun updateWidth() {
if (cache.sizeCacheValid) {
width = cache.cachedWidth
return
}
width = when (widthType) {
Size.Auto -> getAutoWidth()
Size.Percent -> {
val parentElement = findFirstVisibleParent()
if (parentElement == null) {
screenWidth * (widthPercent / 100f)
} else {
val padding = getParentPadding()
val availableWidth = parentElement.width - (padding[1] + padding[3])
availableWidth * (widthPercent / 100f)
}
}
Size.Pixels -> width
Size.Fill -> {
val parentElement = findFirstVisibleParent()
if (parentElement == null) {
(screenWidth.toFloat() - x).coerceAtLeast(0f)
} else {
val padding = getParentPadding()
val parentRightEdge = parentElement.x + parentElement.width - padding[1]
(parentRightEdge - x - getSiblingsAfterSize(width = true)).coerceAtLeast(0f)
}
}
}
cache.cachedWidth = width
}
open fun updateHeight() {
if (cache.sizeCacheValid) {
height = cache.cachedHeight
return
}
height = when (heightType) {
Size.Auto -> getAutoHeight()
Size.Percent -> {
val parentElement = findFirstVisibleParent()
if (parentElement == null) {
screenHeight * (heightPercent / 100f)
} else {
val padding = getParentPadding()
val availableHeight = parentElement.height - (padding[0] + padding[2])
availableHeight * (heightPercent / 100f)
}
}
Size.Pixels -> height
Size.Fill -> {
val parentElement = findFirstVisibleParent()
if (parentElement == null) {
(screenHeight.toFloat() - y).coerceAtLeast(0f)
} else {
val padding = getParentPadding()
val parentBottomEdge = parentElement.y + parentElement.height - padding[2]
(parentBottomEdge - y - getSiblingsAfterSize(width = false)).coerceAtLeast(0f)
}
}
}
cache.cachedHeight = height
}
protected open fun getAutoWidth(): Float {
val maxWidth = children
.filter { it.visible && !it.isFloating }
.maxOfOrNull { (x - it.x) + it.width }
val calculated = maxWidth?.coerceAtLeast(0f) ?: 0f
return maxAutoWidth?.let { calculated.coerceAtMost(it) } ?: calculated
}
protected open fun getAutoHeight(): Float {
val maxHeight = children
.filter { it.visible && !it.isFloating }
.maxOfOrNull { (y - it.y) + it.height }
val calculated = maxHeight?.coerceAtLeast(0f) ?: 0f
return maxAutoHeight?.let { calculated.coerceAtMost(it) } ?: calculated
}
private fun computeOffset(offset: Float, offsetType: Offset, isWidth: Boolean): Float {
return when (offsetType) {
Offset.Pixels -> offset
Offset.Percent -> {
val parentElement = findFirstVisibleParent()
val base = if (isWidth) {
parentElement?.width ?: screenWidth.toFloat()
} else {
parentElement?.height ?: screenHeight.toFloat()
}
base * (offset / 100f)
}
}
}
fun updateX() {
if (cache.positionCacheValid) return
val visibleParent = findFirstVisibleParent()
val padding = getParentPadding()
val computedXOffset = computeOffset(xOffset, xOffsetType, true)
x = when (xPositionConstraint) {
Pos.ParentPercent -> {
val base = if (visibleParent != null) {
visibleParent.x + padding[3] + (visibleParent.width - padding[1] - padding[3]) * (xConstraint / 100f)
} else {
xConstraint
}
base + computedXOffset
}
Pos.ScreenPercent -> screenWidth * (xConstraint / 100f) + computedXOffset
Pos.ParentPixels -> {
val base = if (visibleParent != null) visibleParent.x + padding[3] + xConstraint else xConstraint
base + computedXOffset
}
Pos.ScreenPixels -> xConstraint + computedXOffset
Pos.ParentCenter -> {
val base = if (visibleParent != null) {
val availableWidth = visibleParent.width - padding[1] - padding[3]
visibleParent.x + padding[3] + (availableWidth - width) / 2f
} else {
xConstraint
}
base + computedXOffset
}
Pos.ScreenCenter -> (screenWidth / 2f) - (width / 2f) + xConstraint + computedXOffset
Pos.AfterSibling -> computeAfterSiblingX(visibleParent) + computedXOffset
Pos.MatchSibling -> computeMatchSiblingX() + computedXOffset
}
x = applyXAlignment(x, visibleParent, padding)
}
private fun computeAfterSiblingX(visibleParent: Element<*>?): Float {
val parentElement = parent as? Element<*> ?: return xConstraint
val padding = getParentPadding()
val index = parentElement.children.indexOf(this)
if (index <= 0) {
return if (visibleParent != null) visibleParent.x + padding[3] + xConstraint else xConstraint
}
val prevVisible = parentElement.children.subList(0, index).lastOrNull { it.visible }
val prev = prevVisible?.x ?: 0f
val width = prevVisible?.width ?: 0f
return prev + width + xConstraint
}
private fun computeMatchSiblingX(): Float {
val parentElement = parent as? Element<*> ?: return 0f
val index = parentElement.children.indexOf(this)
return if (index > 0) parentElement.children[index - 1].x else 0f
}
fun updateY() {
if (cache.positionCacheValid) return
val visibleParent = findFirstVisibleParent()
val padding = getParentPadding()
val computedYOffset = computeOffset(yOffset, yOffsetType, false)
y = when (yPositionConstraint) {
Pos.ParentPercent -> {
val base = if (visibleParent != null) {
visibleParent.y + padding[0] + (visibleParent.height - padding[0] - padding[2]) * (yConstraint / 100f)
} else {
yConstraint
}
base + computedYOffset
}
Pos.ScreenPercent -> screenHeight * (yConstraint / 100f) + computedYOffset
Pos.ParentPixels -> {
val base = if (visibleParent != null) visibleParent.y + padding[0] + yConstraint else yConstraint
base + computedYOffset
}
Pos.ScreenPixels -> yConstraint + computedYOffset
Pos.ParentCenter -> {
val base = if (visibleParent != null) {
val availableHeight = visibleParent.height - padding[0] - padding[2]
visibleParent.y + padding[0] + (availableHeight - height) / 2f
} else {
yConstraint
}
base + computedYOffset
}
Pos.ScreenCenter -> (screenHeight / 2f) - (height / 2f) + yConstraint + computedYOffset
Pos.AfterSibling -> computeAfterSiblingY(visibleParent) + computedYOffset
Pos.MatchSibling -> computeMatchSiblingY() + computedYOffset
}
y = applyYAlignment(y, visibleParent, padding)
}
private fun computeAfterSiblingY(visibleParent: Element<*>?): Float {
val parentElement = parent as? Element<*> ?: return yConstraint
val padding = getParentPadding()
val index = parentElement.children.indexOf(this)
if (index <= 0) {
return if (visibleParent != null) visibleParent.y + padding[0] + yConstraint else yConstraint
}
val prevVisible = parentElement.children.subList(0, index).lastOrNull { it.visible }
val prev = prevVisible?.y ?: 0f
val height = prevVisible?.height ?: 0f
return prev + height + yConstraint
}
private fun computeMatchSiblingY(): Float {
val parentElement = parent as? Element<*> ?: return yConstraint
val index = parentElement.children.indexOf(this)
return if (index > 0) parentElement.children[index - 1].y else yConstraint
}
private fun applyXAlignment(baseX: Float, visibleParent: Element<*>?, padding: FloatArray): Float {
return when (xAlignment) {
Alignment.None -> baseX
Alignment.Start -> {
val leftEdge = if (visibleParent != null) visibleParent.x + padding[3] else 0f
leftEdge + xConstraint
}
Alignment.End -> {
val rightEdge = if (visibleParent != null) visibleParent.x + visibleParent.width - padding[1] else screenWidth.toFloat()
rightEdge - width + xConstraint
}
}
}
private fun applyYAlignment(baseY: Float, visibleParent: Element<*>?, padding: FloatArray): Float {
return when (yAlignment) {
Alignment.None -> baseY
Alignment.Start -> {
val topEdge = if (visibleParent != null) visibleParent.y + padding[0] else 0f
topEdge + yConstraint
}
Alignment.End -> {
val bottomEdge = if (visibleParent != null) visibleParent.y + visibleParent.height - padding[2] else screenHeight.toFloat()
bottomEdge - height + yConstraint
}
}
}
open fun isPointInside(mouseX: Float, mouseY: Float): Boolean {
return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height
}
open fun handleMouseMove(mouseX: Float, mouseY: Float): Boolean {
if (!visible) return false
val wasHovered = isHovered
isHovered = isPointInside(mouseX, mouseY)
when {
isHovered && !wasHovered -> {
for (listener in listeners.mouseEnter) {
listener(MouseEvent.Move.Enter(mouseX, mouseY, this))
}
tooltipElement?.let { tooltip ->
tooltip.fadeIn(Theme.animNormal, EasingType.EASE_OUT)
tooltip.innerText.fadeIn(Theme.animNormal, EasingType.EASE_OUT)
}
}
!isHovered && wasHovered -> {
for (listener in listeners.mouseExit) {
listener(MouseEvent.Move.Exit(mouseX, mouseY, this))
}
tooltipElement?.let { tooltip ->
tooltip.fadeOut(Theme.animNormal, EasingType.EASE_OUT)
tooltip.innerText.fadeOut(Theme.animNormal, EasingType.EASE_OUT)
}
}
}
if (isHovered) {
for (listener in listeners.mouseMove) {
listener(MouseEvent.Move(mouseX, mouseY, this))
}
}
val childHandled = children.reversed().any { it.handleMouseMove(mouseX, mouseY) }
return childHandled || isHovered
}
open fun handleMouseClick(mouseX: Float, mouseY: Float, button: Int): Boolean {
if (!visible) return false
val childHandled = children.reversed().any {
it.handleMouseClick(mouseX, mouseY, button)
}
return when {
childHandled -> true
isPointInside(mouseX, mouseY) -> {
isPressed = true
focus()
val listenerHandled = listeners.mouseClick.any {
it(MouseEvent.Click(mouseX, mouseY, button, this))
}
listenerHandled || listeners.mouseClick.isEmpty()
}
else -> {
if (requiresFocus && isFocused) unfocus()
false
}
}
}
open fun handleMouseRelease(mouseX: Float, mouseY: Float, button: Int): Boolean {
if (!visible) return false
val wasPressed = isPressed
isPressed = false
val childHandled = children.reversed().any {
it.handleMouseRelease(mouseX, mouseY, button)
}
if (childHandled) return true
if (wasPressed && isPointInside(mouseX, mouseY)) {
val listenerHandled = listeners.mouseRelease.any {
it(MouseEvent.Release(mouseX, mouseY, button, this))
}
return listenerHandled || listeners.mouseRelease.isEmpty()
}
return false
}
open fun handleMouseScroll(mouseX: Float, mouseY: Float, horizontal: Double, vertical: Double): Boolean {
if (!visible) return false
val childHandled = children.reversed().any {
it.handleMouseScroll(mouseX, mouseY, horizontal, vertical)
}
if (childHandled) return true
if (isPointInside(mouseX, mouseY)) {
return listeners.mouseScroll.any {
it(MouseEvent.Scroll(mouseX, mouseY, horizontal, vertical, this))
}
}
return false
}
open fun handleCharType(keyCode: Int, scanCode: Int, charTyped: Char): Boolean {
if (!visible) return false
val childHandled = children.reversed().any {
it.handleCharType(keyCode, scanCode, charTyped)
}
val selfHandled = if (isFocused || ignoreFocus) {
listeners.charType.any { it(KeyEvent.Type(keyCode, scanCode, charTyped, this)) }
} else false
return childHandled || selfHandled
}
fun focus() {
getRootElement().unfocusAll()
isFocused = true
}
fun unfocus() {
isFocused = false
}
private fun unfocusAll() {
if (isFocused) unfocus()
children.forEach { it.unfocusAll() }
}
fun getRootElement(): Element<*> {
var current: Element<*> = this
while (current.parent is Element<*>) {
current = current.parent as Element<*>
}
return current
}
open fun onWindowResize() {
cache.invalidate()
for (child in children) child.onWindowResize()
}
open fun render(mouseX: Float, mouseY: Float) {
if (!visible) return
checkScreenResize()
updateHeight()
updateWidth()
updateX()
updateY()
cache.sizeCacheValid = true
cache.positionCacheValid = true
onRender(mouseX, mouseY)
renderChildren(mouseX, mouseY)
renderDebugHitbox()
}
protected open fun renderChildren(mouseX: Float, mouseY: Float) {
children.forEach { it.render(mouseX, mouseY) }
}
protected abstract fun onRender(mouseX: Float, mouseY: Float)
fun childOf(parent: Element<*>): T {
parent.addChild(this)
return this as T
}
fun childOf(parent: Window): T {
parent.addChild(this)
return this as T
}
fun addChild(child: Element<*>): T {
child.parent = this
children.add(child)
return this as T
}
fun setMaxAutoSize(maxWidth: Float? = null, maxHeight: Float? = null): T {
this.maxAutoWidth = maxWidth
this.maxAutoHeight = maxHeight
cache.sizeCacheValid = false
return this as T
}
fun setSizing(widthType: Size, heightType: Size): T {
this.widthType = widthType
this.heightType = heightType
cache.sizeCacheValid = false
return this as T
}
fun setSizing(width: Float, widthType: Size, height: Float, heightType: Size): T {
this.widthType = widthType
this.heightType = heightType
if (widthType == Size.Pixels) this.width = width else this.widthPercent = width
if (heightType == Size.Pixels) this.height = height else this.heightPercent = height
cache.sizeCacheValid = false
return this as T
}
fun setPositioning(xConstraint: Pos, yConstraint: Pos): T {
this.xPositionConstraint = xConstraint
this.yPositionConstraint = yConstraint
cache.positionCacheValid = false
return this as T
}
fun setPositioning(xVal: Float, xPos: Pos, yVal: Float, yPos: Pos): T {
this.xConstraint = xVal
this.xPositionConstraint = xPos
this.yConstraint = yVal
this.yPositionConstraint = yPos
cache.positionCacheValid = false
return this as T
}
fun setAlignment(xAlignment: Alignment, yAlignment: Alignment): T {
this.xAlignment = xAlignment
this.yAlignment = yAlignment
cache.positionCacheValid = false
return this as T
}
fun alignLeft(): T {
this.xAlignment = Alignment.Start
cache.positionCacheValid = false
return this as T
}
fun alignRight(): T {
this.xAlignment = Alignment.End
cache.positionCacheValid = false
return this as T
}
fun alignTop(): T {
this.yAlignment = Alignment.Start
cache.positionCacheValid = false
return this as T
}
fun alignBottom(): T {
this.yAlignment = Alignment.End
cache.positionCacheValid = false
return this as T
}
fun setOffset(xOffset: Float, xOffsetType: Offset, yOffset: Float, yOffsetType: Offset): T {
this.xOffset = xOffset
this.xOffsetType = xOffsetType
this.yOffset = yOffset
this.yOffsetType = yOffsetType
cache.positionCacheValid = false
return this as T
}
fun setOffset(xOffset: Float, yOffset: Float): T {
return setOffset(xOffset, Offset.Pixels, yOffset, Offset.Pixels)
}
fun addTooltip(tooltip: String): T {
tooltipElement = Tooltip().apply {
innerText.text = tooltip
childOf(this@Element)
}
return this as T
}
fun onMouseEnter(callback: (MouseEvent.Move.Enter) -> Unit): T {
listeners.mouseEnter.add(callback)
return this as T
}
fun onMouseExit(callback: (MouseEvent.Move.Exit) -> Unit): T {
listeners.mouseExit.add(callback)
return this as T
}
fun onMouseMove(callback: (MouseEvent.Move) -> Unit): T {
listeners.mouseMove.add(callback)
return this as T
}
fun onHover(onEnter: (MouseEvent.Move.Enter) -> Unit = { }, onExit: (MouseEvent.Move.Exit) -> Unit = { }): T {
onMouseEnter(onEnter)
onMouseExit(onExit)
return this as T
}
fun onMouseClick(callback: (MouseEvent.Click) -> Boolean): T {
listeners.mouseClick.add(callback)
return this as T
}
fun onClick(callback: (MouseEvent.Click) -> Boolean): T = onMouseClick(callback)
fun onMouseRelease(callback: (MouseEvent.Release) -> Boolean): T {
listeners.mouseRelease.add(callback)
return this as T
}
fun onRelease(callback: (MouseEvent.Release) -> Boolean): T = onMouseRelease(callback)
fun onMouseScroll(callback: (MouseEvent.Scroll) -> Boolean): T {
listeners.mouseScroll.add(callback)
return this as T
}
fun onScroll(callback: (MouseEvent.Scroll) -> Boolean): T = onMouseScroll(callback)
fun onCharType(callback: (KeyEvent.Type) -> Boolean): T {
listeners.charType.add(callback)
return this as T
}
fun onValueChange(callback: (Any) -> Unit): T {
this.onValueChange.add(callback)
return this as T
}
fun ignoreMouseEvents(): T {
listeners.mouseClick.add { false }
listeners.mouseRelease.add { false }
listeners.mouseScroll.add { false }
listeners.mouseMove.add { }
listeners.mouseEnter.add { }
listeners.mouseExit.add { }
return this as T
}
fun ignoreFocus(): T {
ignoreFocus = true
return this as T
}
fun setFloating(): T {
isFloating = true
return this as T
}
fun setRequiresFocus(): T {
requiresFocus = true
return this as T
}
fun show(): T {
visible = true
return this as T
}
fun hide(): T {
visible = false
return this as T
}
fun enableDebugRendering(): T {
renderHitbox = true
return this as T
}
private companion object {
val ZERO_PADDING = floatArrayOf(0f, 0f, 0f, 0f)
}
}

View file

@ -0,0 +1,26 @@
package xyz.meowing.lattice.ui
class ElementCache {
var cachedParent: Element<*>? = null
var parentCacheValid = false
var positionCacheValid = false
var sizeCacheValid = false
var cachedWidth: Float = 0f
var cachedHeight: Float = 0f
var lastScreenWidth = 0
var lastScreenHeight = 0
fun invalidate() {
parentCacheValid = false
positionCacheValid = false
sizeCacheValid = false
}
fun invalidatePosition() {
positionCacheValid = false
}
fun invalidateSize() {
sizeCacheValid = false
}
}

View file

@ -0,0 +1,21 @@
package xyz.meowing.lattice.ui
class ElementListeners {
val mouseEnter = mutableListOf<(MouseEvent.Move.Enter) -> Unit>()
val mouseExit = mutableListOf<(MouseEvent.Move.Exit) -> Unit>()
val mouseMove = mutableListOf<(MouseEvent.Move) -> Unit>()
val mouseScroll = mutableListOf<(MouseEvent.Scroll) -> Boolean>()
val mouseClick = mutableListOf<(MouseEvent.Click) -> Boolean>()
val mouseRelease = mutableListOf<(MouseEvent.Release) -> Boolean>()
val charType = mutableListOf<(KeyEvent.Type) -> Boolean>()
fun clear() {
mouseEnter.clear()
mouseExit.clear()
mouseMove.clear()
mouseScroll.clear()
mouseClick.clear()
mouseRelease.clear()
charType.clear()
}
}

View file

@ -0,0 +1,104 @@
package xyz.meowing.lattice.ui
import net.minecraft.client.gui.GuiGraphicsExtractor
import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.input.CharacterEvent
import net.minecraft.client.input.MouseButtonEvent
import net.minecraft.network.chat.Component
import xyz.meowing.lattice.Lattice.eventBus
import xyz.meowing.lattice.client.Client.minecraft
import xyz.meowing.lattice.event.EventCall
import xyz.meowing.lattice.event.RenderEvent
import xyz.meowing.lattice.input.Keys
import xyz.meowing.lattice.input.Mouse
import xyz.meowing.lattice.scheduler.TimeScheduler
/** Base screen backed by a Lattice [Window], rendered through the NanoVG overlay hook. */
abstract class UIScreen(screenName: String = "Lattice-Screen") : Screen(Component.literal(screenName)) {
val window = Window()
private var renderEvent: EventCall? = null
private var hasInitialized = false
private var lastX = -1.0
private var lastY = -1.0
/** Called once, the first time the screen initializes. Build the UI here. */
open fun afterInitialization() {}
/** Called every frame after the window and animations render. */
open fun onRenderGui() {}
open fun onResizeGui() {}
open fun onCloseGui() {}
final override fun init() {
if (!hasInitialized) {
hasInitialized = true
afterInitialization()
renderEvent = eventBus.register<RenderEvent.Gui> {
if (minecraft.screen == this) {
window.draw()
onRenderGui()
}
}
} else {
onResizeGui()
window.onWindowResize()
}
super.init()
}
override fun extractRenderState(context: GuiGraphicsExtractor, mouseX: Int, mouseY: Int, deltaTicks: Float) {
val newX = Mouse.Raw.x
val newY = Mouse.Raw.y
if (newX != lastX || newY != lastY) {
window.mouseMove()
lastX = newX
lastY = newY
}
}
override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean {
return window.mouseClick(click.button()) || super.mouseClicked(click, doubled)
}
override fun mouseReleased(click: MouseButtonEvent): Boolean {
return window.mouseRelease(click.button()) || super.mouseReleased(click)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
window.mouseScroll(horizontalAmount, verticalAmount)
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun keyPressed(input: net.minecraft.client.input.KeyEvent): Boolean {
val handled = window.charType(input.key(), input.scancode(), ' ')
if (!handled && input.key() == Keys.KEY_ESCAPE.code) {
onClose()
return true
}
return handled || super.keyPressed(input)
}
override fun charTyped(input: CharacterEvent): Boolean {
return window.charType(0, 0, input.codepoint().toChar()) || super.charTyped(input)
}
override fun onClose() {
window.cleanup()
renderEvent?.unregister()
renderEvent = null
hasInitialized = false
onCloseGui()
super.onClose()
}
/** Opens this screen on the next client tick, safe to call from any thread. */
fun display() {
TimeScheduler.schedule(50) {
minecraft.setScreen(this@UIScreen)
}
}
}

View file

@ -0,0 +1,56 @@
package xyz.meowing.lattice.ui
import xyz.meowing.lattice.animation.AnimationManager
import xyz.meowing.lattice.input.Mouse
class Window {
val children: MutableList<Element<*>> = mutableListOf()
private val mouseX: Float get() = Mouse.Raw.x.toFloat()
private val mouseY: Float get() = Mouse.Raw.y.toFloat()
fun addChild(element: Element<*>) {
element.parent = this
children.add(element)
}
fun removeChild(element: Element<*>) {
element.parent = null
children.remove(element)
}
fun draw() {
children.forEach { it.render(mouseX, mouseY) }
AnimationManager.update()
}
fun mouseClick(button: Int): Boolean {
return children.reversed().any { it.handleMouseClick(mouseX, mouseY, button) }
}
fun mouseRelease(button: Int): Boolean {
return children.reversed().any { it.handleMouseRelease(mouseX, mouseY, button) }
}
fun mouseMove() {
children.reversed().forEach { it.handleMouseMove(mouseX, mouseY) }
}
fun mouseScroll(horizontalDelta: Double, verticalDelta: Double) {
children.reversed().forEach { it.handleMouseScroll(mouseX, mouseY, horizontalDelta, verticalDelta) }
}
fun charType(keyCode: Int, scanCode: Int, charTyped: Char): Boolean {
return children.reversed().any { it.handleCharType(keyCode, scanCode, charTyped) }
}
fun onWindowResize() {
children.forEach { it.onWindowResize() }
}
fun cleanup() {
children.toList().forEach { it.destroy() }
children.clear()
AnimationManager.clear()
}
}

View file

@ -0,0 +1,14 @@
package xyz.meowing.lattice.ui.component
import xyz.meowing.lattice.ui.Box
import xyz.meowing.lattice.ui.Size
/** Invisible layout box: padding and optional scrolling with no drawing of its own. */
open class Container(
padding: FloatArray = floatArrayOf(0f, 0f, 0f, 0f),
scrollable: Boolean = false,
widthType: Size = Size.Auto,
heightType: Size = Size.Auto
) : Box<Container>(padding, scrollable, widthType, heightType) {
override fun onRender(mouseX: Float, mouseY: Float) {}
}

View file

@ -0,0 +1,161 @@
package xyz.meowing.lattice.ui.component
import xyz.meowing.lattice.Lattice.renderer
import xyz.meowing.lattice.animation.EasingType
import xyz.meowing.lattice.animation.animateFloat
import xyz.meowing.lattice.render.Gradient
import xyz.meowing.lattice.ui.Box
import xyz.meowing.lattice.ui.Size
open class Rectangle(
var backgroundColor: Int = 0x80000000.toInt(),
var borderColor: Int = 0xFFFFFFFF.toInt(),
var borderRadius: Float = 0f,
var borderThickness: Float = 0f,
padding: FloatArray = floatArrayOf(0f, 0f, 0f, 0f),
var hoverColor: Int? = null,
var pressedColor: Int? = null,
widthType: Size = Size.Auto,
heightType: Size = Size.Auto,
scrollable: Boolean = false
) : Box<Rectangle>(padding, scrollable, widthType, heightType) {
var secondBorderColor: Int = -1
var secondBackgroundColor: Int = -1
var gradientType: Gradient = Gradient.TopLeftToBottomRight
var dropShadow: Boolean = false
var rotation: Float = 0f
var shadowBlur = 30f
var shadowSpread = 1f
var shadowColor = 0x80000000.toInt()
var borderRadiusTopLeft: Float? = null
var borderRadiusTopRight: Float? = null
var borderRadiusBottomLeft: Float? = null
var borderRadiusBottomRight: Float? = null
public override fun onRender(mouseX: Float, mouseY: Float) {
if (!visible || (height - (padding[0] + padding[2])) == 0f || (width - (padding[1] + padding[3])) == 0f) return
val currentBgColor = when {
isPressed && pressedColor != null -> pressedColor!!
isHovered && hoverColor != null -> hoverColor!!
else -> backgroundColor
}
val centerX = x + width / 2f
val centerY = y + height / 2f
if (rotation != 0f) {
renderer.push()
renderer.translate(centerX, centerY)
renderer.rotate(Math.toRadians(rotation.toDouble()).toFloat())
renderer.translate(-centerX, -centerY)
}
if (dropShadow) {
renderer.dropShadow(x, y, width, height, shadowBlur, shadowSpread, shadowColor, borderRadius)
}
if (currentBgColor != 0) {
if (hasVaryingRadius()) {
renderer.rect(
x, y, width, height, currentBgColor,
borderRadiusTopRight ?: borderRadius,
borderRadiusTopLeft ?: borderRadius,
borderRadiusBottomRight ?: borderRadius,
borderRadiusBottomLeft ?: borderRadius
)
} else if (currentBgColor == backgroundColor && secondBackgroundColor != -1) {
renderer.gradientRect(x, y, width, height, backgroundColor, secondBackgroundColor, gradientType, borderRadius)
} else {
renderer.rect(x, y, width, height, currentBgColor, borderRadius)
}
}
if (borderThickness > 0f) {
if (secondBorderColor != -1) {
renderer.hollowGradientRect(x, y, width, height, borderThickness, borderColor, secondBorderColor, gradientType, borderRadius)
} else {
renderer.hollowRect(x, y, width, height, borderThickness, borderColor, borderRadius)
}
}
if (rotation != 0f) {
renderer.pop()
}
}
private fun hasVaryingRadius(): Boolean {
return borderRadiusTopLeft != null || borderRadiusTopRight != null ||
borderRadiusBottomLeft != null || borderRadiusBottomRight != null
}
fun rotateTo(angle: Float, duration: Long = 300, type: EasingType = EasingType.EASE_OUT, onComplete: (() -> Unit)? = null): Rectangle {
animateFloat({ rotation }, { rotation = it }, angle, duration, type, onComplete = onComplete)
return this
}
fun dropShadow(shadowBlur: Float = 30f, shadowSpread: Float = 1f, shadowColor: Int = 0x000000): Rectangle = apply {
dropShadow = true
this.shadowBlur = shadowBlur
this.shadowSpread = shadowSpread
this.shadowColor = shadowColor
}
open fun backgroundColor(color: Int): Rectangle = apply {
backgroundColor = color
secondBackgroundColor = -1
}
open fun setBackgroundGradientColor(color1: Int, color2: Int): Rectangle = apply {
backgroundColor = color1
secondBackgroundColor = color2
}
open fun setBorderGradientColor(color1: Int, color2: Int): Rectangle = apply {
borderColor = color1
secondBorderColor = color2
}
open fun borderColor(color: Int): Rectangle = apply {
borderColor = color
secondBorderColor = -1
}
open fun borderGradient(type: Gradient): Rectangle = apply {
gradientType = type
}
open fun borderRadius(radius: Float): Rectangle = apply {
borderRadius = radius
borderRadiusTopLeft = null
borderRadiusTopRight = null
borderRadiusBottomLeft = null
borderRadiusBottomRight = null
}
open fun borderRadiusVarying(
topLeft: Float = borderRadius,
topRight: Float = borderRadius,
bottomLeft: Float = borderRadius,
bottomRight: Float = borderRadius
): Rectangle = apply {
borderRadiusTopLeft = topLeft
borderRadiusTopRight = topRight
borderRadiusBottomLeft = bottomLeft
borderRadiusBottomRight = bottomRight
}
open fun borderThickness(thickness: Float): Rectangle = apply {
borderThickness = thickness
}
open fun hoverColor(color: Int): Rectangle = apply {
hoverColor = color
}
open fun pressedColor(color: Int): Rectangle = apply {
pressedColor = color
}
}

View file

@ -0,0 +1,67 @@
package xyz.meowing.lattice.ui.component
import xyz.meowing.lattice.Lattice.renderer
import xyz.meowing.lattice.animation.EasingType
import xyz.meowing.lattice.animation.animateFloat
import xyz.meowing.lattice.ui.Element
import xyz.meowing.lattice.ui.Pos
import xyz.meowing.lattice.ui.Size
class SvgImage(
var svgPath: String = "",
startingWidth: Float = 80f,
startingHeight: Float = 80f,
color: Int = 0xFFFFFFFF.toInt()
) : Element<SvgImage>() {
var color = color
private set
var image = renderer.createImage(svgPath, startingWidth.toInt(), startingHeight.toInt(), color)
private set
var rotation: Float = 0f
init {
setPositioning(Pos.ParentPixels, Pos.ParentPixels)
ignoreMouseEvents()
setSizing(startingWidth, Size.Pixels, startingHeight, Size.Pixels)
}
override fun onRender(mouseX: Float, mouseY: Float) {
if (svgPath.isEmpty()) return
val centerX = x + width / 2f
val centerY = y + height / 2f
if (rotation != 0f) {
renderer.push()
renderer.translate(centerX, centerY)
renderer.rotate(Math.toRadians(rotation.toDouble()).toFloat())
renderer.translate(-centerX, -centerY)
}
renderer.image(image, x, y, width, height)
if (rotation != 0f) {
renderer.pop()
}
}
override fun destroy() {
renderer.deleteImage(image)
super.destroy()
}
fun rotateTo(angle: Float, duration: Long = 300, type: EasingType = EasingType.EASE_OUT, onComplete: (() -> Unit)? = null): SvgImage {
animateFloat({ rotation }, { rotation = it }, angle, duration, type, onComplete = onComplete)
return this
}
fun setSvgColor(newColor: Int) {
if (color != newColor) {
color = newColor
renderer.deleteImage(image)
image = renderer.createImage(svgPath, width.toInt(), height.toInt(), color)
}
}
}

View file

@ -0,0 +1,54 @@
package xyz.meowing.lattice.ui.component
import xyz.meowing.lattice.Lattice
import xyz.meowing.lattice.Lattice.renderer
import xyz.meowing.lattice.render.Font
import xyz.meowing.lattice.ui.Element
import xyz.meowing.lattice.ui.Pos
import xyz.meowing.lattice.ui.Size
class Text(
var text: String = "",
var textColor: Int = 0xFFFFFFFF.toInt(),
var fontSize: Float = 12f,
var shadowEnabled: Boolean = false,
var font: Font = Lattice.defaultFont
) : Element<Text>() {
init {
setSizing(Size.Auto, Size.Auto)
setPositioning(Pos.ParentPixels, Pos.ParentPixels)
ignoreMouseEvents()
}
override fun onRender(mouseX: Float, mouseY: Float) {
if (text.isEmpty()) return
if (shadowEnabled) renderer.shadowedText(text, x, y, fontSize, textColor, font)
else renderer.text(text, x, y, fontSize, textColor, font)
}
override fun getAutoWidth(): Float = renderer.textWidth(text, fontSize, font)
override fun getAutoHeight(): Float = fontSize
fun text(newText: String): Text = apply {
text = newText
}
fun color(color: Int): Text = apply {
textColor = color
}
fun fontSize(size: Float): Text = apply {
fontSize = size
}
fun font(newFont: Font): Text = apply {
font = newFont
}
fun shadow(enabled: Boolean = true): Text = apply {
shadowEnabled = enabled
}
}

View file

@ -0,0 +1,97 @@
package xyz.meowing.lattice.ui.component
import xyz.meowing.lattice.ui.theme.Theme
import xyz.meowing.lattice.ui.Box
import xyz.meowing.lattice.ui.Element
import xyz.meowing.lattice.ui.Pos
import xyz.meowing.lattice.ui.Size
import xyz.meowing.lattice.ui.TooltipPosition
class Tooltip(
backgroundColor: Int = Theme.surface,
borderColor: Int = Theme.surfaceBorder,
borderRadius: Float = 4f,
borderThickness: Float = 1f,
var padding: FloatArray = floatArrayOf(4f, 4f, 4f, 4f),
hoverColor: Int? = Theme.surface,
pressedColor: Int? = Theme.surface,
widthType: Size = Size.Auto,
heightType: Size = Size.Auto,
var position: TooltipPosition = TooltipPosition.Top
) : Element<Tooltip>(widthType, heightType) {
val backgroundRect = Rectangle(
backgroundColor,
borderColor,
borderRadius,
borderThickness,
padding,
hoverColor,
pressedColor,
widthType,
heightType
)
.childOf(this)
val innerText = Text("Tooltip", 0xFFFFFFFF.toInt(), 12f)
.setPositioning(Pos.ParentCenter, Pos.ParentCenter)
.childOf(backgroundRect)
override var visible: Boolean = false
set(value) {
if (field != value) {
field = value
cache.invalidate()
invalidateChildrenCache()
}
if (value) {
backgroundRect.visible = true
backgroundRect.width = width
backgroundRect.height = height
} else {
backgroundRect.visible = false
}
}
init {
setSizing(Size.Auto, Size.Auto)
updatePosition()
ignoreMouseEvents()
setFloating()
backgroundRect.visible = false
innerText.visible = false
}
fun setPosition(newPosition: TooltipPosition): Tooltip {
position = newPosition
updatePosition()
return this
}
private fun updatePosition() {
val parentPadding = (parent as? Box<*>)?.padding ?: floatArrayOf(0f, 0f, 0f, 0f)
val offset = 24f
when (position) {
TooltipPosition.Top -> {
setPositioning(0f, Pos.ParentCenter, -offset - parentPadding[0], Pos.ParentPixels)
}
TooltipPosition.Bottom -> {
setPositioning(0f, Pos.ParentCenter, offset + parentPadding[2], Pos.ParentPixels)
alignBottom()
}
TooltipPosition.Left -> {
setPositioning(-offset - parentPadding[3], Pos.ParentPixels, 0f, Pos.ParentCenter)
}
TooltipPosition.Right -> {
setPositioning(offset + parentPadding[1], Pos.ParentPixels, 0f, Pos.ParentCenter)
alignRight()
}
}
}
override fun getAutoWidth(): Float = backgroundRect.getAutoWidth()
override fun getAutoHeight(): Float = backgroundRect.getAutoHeight()
override fun onRender(mouseX: Float, mouseY: Float) {}
}

View file

@ -0,0 +1,18 @@
package xyz.meowing.lattice.ui
sealed class MouseEvent {
class Move(val x: Float, val y: Float, val element: Element<*>) {
class Enter(val x: Float, val y: Float, val element: Element<*>)
class Exit(val x: Float, val y: Float, val element: Element<*>)
}
class Scroll(val x: Float, val y: Float, val horizontal: Double, val vertical: Double, val element: Element<*>?)
class Click(val x: Float, val y: Float, val button: Int, val element: Element<*>?)
class Release(val x: Float, val y: Float, val button: Int, val element: Element<*>?)
}
sealed class KeyEvent {
class Type(val keyCode: Int, val scanCode: Int, val char: Char, val element: Element<*>)
}

Some files were not shown because too many files have changed in this diff Show more