init commit
This commit is contained in:
commit
e6ca5fce3b
119 changed files with 10326 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.gradle/
|
||||
build/
|
||||
run/
|
||||
out/
|
||||
.idea/
|
||||
*.iml
|
||||
.direnv/
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal 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
41
NOTICE.md
Normal 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
43
README.md
Normal 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
65
build.gradle
Normal 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
16
docs/README.md
Normal 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
39
docs/animation.md
Normal 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
106
docs/architecture.md
Normal 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
42
docs/commands.md
Normal 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
62
docs/components.md
Normal 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
50
docs/events.md
Normal 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
71
docs/getting-started.md
Normal 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
54
docs/layout.md
Normal 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
49
docs/migration.md
Normal 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
58
docs/rendering.md
Normal 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
36
docs/text.md
Normal 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
28
docs/theming.md
Normal 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
40
docs/widgets.md
Normal 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
22
example/README.md
Normal 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
47
example/build.gradle
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
25
example/src/main/resources/fabric.mod.json
Normal file
25
example/src/main/resources/fabric.mod.json
Normal 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
21
gradle.properties
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
248
gradlew
vendored
Executable 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
13
settings.gradle
Normal 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
15
shell.nix
Normal 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
|
||||
];
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
33
src/main/java/xyz/meowing/lattice/mixins/MixinMinecraft.java
Normal file
33
src/main/java/xyz/meowing/lattice/mixins/MixinMinecraft.java
Normal 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());
|
||||
}
|
||||
}
|
||||
30
src/main/kotlin/xyz/meowing/lattice/Lattice.kt
Normal file
30
src/main/kotlin/xyz/meowing/lattice/Lattice.kt
Normal 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
|
||||
}
|
||||
}
|
||||
118
src/main/kotlin/xyz/meowing/lattice/animation/Animation.kt
Normal file
118
src/main/kotlin/xyz/meowing/lattice/animation/Animation.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/main/kotlin/xyz/meowing/lattice/animation/extensions.kt
Normal file
70
src/main/kotlin/xyz/meowing/lattice/animation/extensions.kt
Normal 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
|
||||
}
|
||||
164
src/main/kotlin/xyz/meowing/lattice/animation/presets.kt
Normal file
164
src/main/kotlin/xyz/meowing/lattice/animation/presets.kt
Normal 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
|
||||
}
|
||||
51
src/main/kotlin/xyz/meowing/lattice/client/Chat.kt
Normal file
51
src/main/kotlin/xyz/meowing/lattice/client/Chat.kt
Normal 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
|
||||
}
|
||||
}
|
||||
73
src/main/kotlin/xyz/meowing/lattice/client/Client.kt
Normal file
73
src/main/kotlin/xyz/meowing/lattice/client/Client.kt
Normal 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
|
||||
}
|
||||
11
src/main/kotlin/xyz/meowing/lattice/client/Clipboard.kt
Normal file
11
src/main/kotlin/xyz/meowing/lattice/client/Clipboard.kt
Normal 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) }
|
||||
}
|
||||
23
src/main/kotlin/xyz/meowing/lattice/client/Desktop.kt
Normal file
23
src/main/kotlin/xyz/meowing/lattice/client/Desktop.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
21
src/main/kotlin/xyz/meowing/lattice/client/Loader.kt
Normal file
21
src/main/kotlin/xyz/meowing/lattice/client/Loader.kt
Normal 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)
|
||||
}
|
||||
23
src/main/kotlin/xyz/meowing/lattice/client/Player.kt
Normal file
23
src/main/kotlin/xyz/meowing/lattice/client/Player.kt
Normal 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
|
||||
}
|
||||
82
src/main/kotlin/xyz/meowing/lattice/command/Commodore.kt
Normal file
82
src/main/kotlin/xyz/meowing/lattice/command/Commodore.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
179
src/main/kotlin/xyz/meowing/lattice/command/nodes/Executable.kt
Normal file
179
src/main/kotlin/xyz/meowing/lattice/command/nodes/Executable.kt
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/main/kotlin/xyz/meowing/lattice/command/nodes/LiteralNode.kt
Normal file
135
src/main/kotlin/xyz/meowing/lattice/command/nodes/LiteralNode.kt
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/main/kotlin/xyz/meowing/lattice/command/nodes/Node.kt
Normal file
22
src/main/kotlin/xyz/meowing/lattice/command/nodes/Node.kt
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
30
src/main/kotlin/xyz/meowing/lattice/command/utils/Utils.kt
Normal file
30
src/main/kotlin/xyz/meowing/lattice/command/utils/Utils.kt
Normal 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)
|
||||
}
|
||||
12
src/main/kotlin/xyz/meowing/lattice/event/Event.kt
Normal file
12
src/main/kotlin/xyz/meowing/lattice/event/Event.kt
Normal 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
|
||||
}
|
||||
}
|
||||
78
src/main/kotlin/xyz/meowing/lattice/event/EventBus.kt
Normal file
78
src/main/kotlin/xyz/meowing/lattice/event/EventBus.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
45
src/main/kotlin/xyz/meowing/lattice/event/EventCall.kt
Normal file
45
src/main/kotlin/xyz/meowing/lattice/event/EventCall.kt
Normal 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
|
||||
}
|
||||
16
src/main/kotlin/xyz/meowing/lattice/event/Events.kt
Normal file
16
src/main/kotlin/xyz/meowing/lattice/event/Events.kt
Normal 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()
|
||||
}
|
||||
15
src/main/kotlin/xyz/meowing/lattice/input/InputCode.kt
Normal file
15
src/main/kotlin/xyz/meowing/lattice/input/InputCode.kt
Normal 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)
|
||||
}
|
||||
29
src/main/kotlin/xyz/meowing/lattice/input/Inputs.kt
Normal file
29
src/main/kotlin/xyz/meowing/lattice/input/Inputs.kt
Normal 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
|
||||
}
|
||||
}
|
||||
34
src/main/kotlin/xyz/meowing/lattice/input/Keyboard.kt
Normal file
34
src/main/kotlin/xyz/meowing/lattice/input/Keyboard.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
125
src/main/kotlin/xyz/meowing/lattice/input/Keys.kt
Normal file
125
src/main/kotlin/xyz/meowing/lattice/input/Keys.kt
Normal 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)
|
||||
}
|
||||
33
src/main/kotlin/xyz/meowing/lattice/input/Mouse.kt
Normal file
33
src/main/kotlin/xyz/meowing/lattice/input/Mouse.kt
Normal 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
|
||||
}
|
||||
}
|
||||
14
src/main/kotlin/xyz/meowing/lattice/input/MouseButtons.kt
Normal file
14
src/main/kotlin/xyz/meowing/lattice/input/MouseButtons.kt
Normal 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)
|
||||
}
|
||||
119
src/main/kotlin/xyz/meowing/lattice/render/Color.kt
Normal file
119
src/main/kotlin/xyz/meowing/lattice/render/Color.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
42
src/main/kotlin/xyz/meowing/lattice/render/Font.kt
Normal file
42
src/main/kotlin/xyz/meowing/lattice/render/Font.kt
Normal 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
|
||||
}
|
||||
16
src/main/kotlin/xyz/meowing/lattice/render/GLState.kt
Normal file
16
src/main/kotlin/xyz/meowing/lattice/render/GLState.kt
Normal 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
|
||||
}
|
||||
7
src/main/kotlin/xyz/meowing/lattice/render/Gradient.kt
Normal file
7
src/main/kotlin/xyz/meowing/lattice/render/Gradient.kt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package xyz.meowing.lattice.render
|
||||
|
||||
enum class Gradient {
|
||||
LeftToRight,
|
||||
TopToBottom,
|
||||
TopLeftToBottomRight,
|
||||
}
|
||||
43
src/main/kotlin/xyz/meowing/lattice/render/Image.kt
Normal file
43
src/main/kotlin/xyz/meowing/lattice/render/Image.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
484
src/main/kotlin/xyz/meowing/lattice/render/NVGRenderer.kt
Normal file
484
src/main/kotlin/xyz/meowing/lattice/render/NVGRenderer.kt
Normal 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)
|
||||
}
|
||||
51
src/main/kotlin/xyz/meowing/lattice/render/Renderer.kt
Normal file
51
src/main/kotlin/xyz/meowing/lattice/render/Renderer.kt
Normal 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()
|
||||
}
|
||||
27
src/main/kotlin/xyz/meowing/lattice/render/Resolution.kt
Normal file
27
src/main/kotlin/xyz/meowing/lattice/render/Resolution.kt
Normal 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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/main/kotlin/xyz/meowing/lattice/text/ChainBuilder.kt
Normal file
42
src/main/kotlin/xyz/meowing/lattice/text/ChainBuilder.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
11
src/main/kotlin/xyz/meowing/lattice/text/ClickEvent.kt
Normal file
11
src/main/kotlin/xyz/meowing/lattice/text/ClickEvent.kt
Normal 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
|
||||
}
|
||||
23
src/main/kotlin/xyz/meowing/lattice/text/ColorCodes.kt
Normal file
23
src/main/kotlin/xyz/meowing/lattice/text/ColorCodes.kt
Normal 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)
|
||||
}
|
||||
42
src/main/kotlin/xyz/meowing/lattice/text/FormattingCodes.kt
Normal file
42
src/main/kotlin/xyz/meowing/lattice/text/FormattingCodes.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
9
src/main/kotlin/xyz/meowing/lattice/text/HoverEvent.kt
Normal file
9
src/main/kotlin/xyz/meowing/lattice/text/HoverEvent.kt
Normal 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
|
||||
}
|
||||
258
src/main/kotlin/xyz/meowing/lattice/text/TextBuilder.kt
Normal file
258
src/main/kotlin/xyz/meowing/lattice/text/TextBuilder.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
100
src/main/kotlin/xyz/meowing/lattice/text/Texts.kt
Normal file
100
src/main/kotlin/xyz/meowing/lattice/text/Texts.kt
Normal 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()
|
||||
}
|
||||
351
src/main/kotlin/xyz/meowing/lattice/ui/Box.kt
Normal file
351
src/main/kotlin/xyz/meowing/lattice/ui/Box.kt
Normal 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
|
||||
}
|
||||
}
|
||||
855
src/main/kotlin/xyz/meowing/lattice/ui/Element.kt
Normal file
855
src/main/kotlin/xyz/meowing/lattice/ui/Element.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/xyz/meowing/lattice/ui/ElementCache.kt
Normal file
26
src/main/kotlin/xyz/meowing/lattice/ui/ElementCache.kt
Normal 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
|
||||
}
|
||||
}
|
||||
21
src/main/kotlin/xyz/meowing/lattice/ui/ElementListeners.kt
Normal file
21
src/main/kotlin/xyz/meowing/lattice/ui/ElementListeners.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
104
src/main/kotlin/xyz/meowing/lattice/ui/UIScreen.kt
Normal file
104
src/main/kotlin/xyz/meowing/lattice/ui/UIScreen.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/main/kotlin/xyz/meowing/lattice/ui/Window.kt
Normal file
56
src/main/kotlin/xyz/meowing/lattice/ui/Window.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
161
src/main/kotlin/xyz/meowing/lattice/ui/component/Rectangle.kt
Normal file
161
src/main/kotlin/xyz/meowing/lattice/ui/component/Rectangle.kt
Normal 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
|
||||
}
|
||||
}
|
||||
67
src/main/kotlin/xyz/meowing/lattice/ui/component/SvgImage.kt
Normal file
67
src/main/kotlin/xyz/meowing/lattice/ui/component/SvgImage.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/main/kotlin/xyz/meowing/lattice/ui/component/Text.kt
Normal file
54
src/main/kotlin/xyz/meowing/lattice/ui/component/Text.kt
Normal 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
|
||||
}
|
||||
}
|
||||
97
src/main/kotlin/xyz/meowing/lattice/ui/component/Tooltip.kt
Normal file
97
src/main/kotlin/xyz/meowing/lattice/ui/component/Tooltip.kt
Normal 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) {}
|
||||
}
|
||||
18
src/main/kotlin/xyz/meowing/lattice/ui/events.kt
Normal file
18
src/main/kotlin/xyz/meowing/lattice/ui/events.kt
Normal 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
Loading…
Add table
Reference in a new issue