Compare commits
241 Commits
nes-emulat
...
main
Author | SHA1 | Date |
---|---|---|
mars | 73d524cddf | |
mars | f842ae608b | |
mars | 3a778c0aa6 | |
Iris Pupo | 38fc390e10 | |
mars | 4b89e781b0 | |
mars | 25eedc0a0d | |
mars | ad0feb2b31 | |
mars | 508abcb0cc | |
mars | 9660ebb4dd | |
mars | e944caa358 | |
mars | 7adc356c41 | |
mars | 482a0de030 | |
mars | a6b675dd11 | |
mars | d073dde8ca | |
mars | 2d098f8dea | |
mars | c06cd2d999 | |
mars | 780f13a015 | |
mars | c951c95650 | |
mars | c58ca051f4 | |
mars | ae7271a479 | |
mars | 4b46142f90 | |
mars | 03e596b219 | |
mars | b9416b922f | |
mars | 14f077ff97 | |
mars | 109af8793b | |
mars | 38674c2580 | |
mars | 70f9ca4405 | |
mars | c7981b5064 | |
mars | e4a279c230 | |
mars | 9920fea900 | |
mars | feec4de657 | |
mars | a5f279dfd1 | |
mars | d62f8f746a | |
mars | c8a6aa6fbd | |
Iris Pupo | 0768a7dc61 | |
Iris Pupo | 01e2aee164 | |
mars | 72d7e703c1 | |
mars | ce187a0381 | |
mars | 4007c40ba6 | |
mars | 0d4eb5d188 | |
mars | 44d2ac693a | |
mars | f8965b802c | |
mars | 8947041417 | |
mars | 01a37153af | |
mars | 734ad8d630 | |
mars | 6054949694 | |
mars | b0c29818ee | |
mars | b774115d20 | |
mars | b1b7d1d4e7 | |
mars | 7ab55cf13b | |
mars | a9de3ddc5b | |
mars | d43a62dfda | |
mars | 57c6275958 | |
mars | 2bef47a65a | |
Iris Pupo | 57768d041a | |
mars | 448435eb8c | |
mars | 1453ab0c46 | |
mars | f37ec4251d | |
mars | 238f4f878d | |
mars | 2b058c7202 | |
mars | 0fe8139e02 | |
mars | 415d9e7845 | |
mars | e0ec4190fc | |
mars | 4a78743c40 | |
mars | 80ba46a0d4 | |
mars | a73869aae5 | |
mars | b005176bcf | |
mars | 2d242520c1 | |
mars | ac5d6475f1 | |
mars | c244834004 | |
mars | 763c55d3e4 | |
mars | 6c2bfc0ea5 | |
mars | f40d251ca4 | |
mars | d5b168ebc9 | |
mars | d99c64f33e | |
mars | 452cb9c708 | |
mars | 9d4d5cca91 | |
mars | 2144a2ab3d | |
mars | 2df2bd3f8f | |
mars | 1fe366ce9f | |
mars | 9eb2c1c431 | |
mars | 13a9735bdc | |
mars | a65164d70b | |
mars | 7e4900c59b | |
mars | 15f9c7ea99 | |
mars | a2432f77d2 | |
mars | 55859ab5c0 | |
mars | 756238feab | |
mars | d7323323f8 | |
mars | 59c34d6aca | |
mars | 046aede0fa | |
mars | 4b126ed2cf | |
mars | e35ee1be4c | |
mars | d7d4ed9519 | |
mars | e924a17073 | |
mars | 25532f4f9e | |
mars | ef6a81e142 | |
mars | bc8032aaaf | |
mars | e8c9088327 | |
mars | 59a673128c | |
mars | ce7d641795 | |
mars | 3f7ebeaf7e | |
mars | a477c3c385 | |
mars | 85e1fbd6b6 | |
mars | 6db0c72f4d | |
mars | 1ac70e653b | |
mars | 948dfa77c4 | |
mars | 2f5d25a3f4 | |
mars | 218e2fde74 | |
mars | f33100cfa8 | |
mars | 2aadd0d57a | |
mars | a809af7c64 | |
mars | d020caa477 | |
mars | 7de22f9bd4 | |
mars | 50410c7f4f | |
mars | f81f55485b | |
mars | e1ebfab247 | |
mars | cf95b52f7e | |
mars | efae627263 | |
mars | 808d62421c | |
mars | 7d751f27c9 | |
mars | 32cf5e3359 | |
mars | 0cc55b80a8 | |
mars | 14540db59c | |
mars | a84f11ec4a | |
mars | 3ddda5f0c1 | |
mars | 317541486c | |
mars | f58e0d686d | |
mars | 53eccd7411 | |
mars | f8f59cb265 | |
mars | 4e9b01810a | |
mars | 0b8ce299b5 | |
mars | f4a366a215 | |
Emma Tebibyte | 3ec3dd97a5 | |
Emma Tebibyte | 5670793f90 | |
mars | cedd5503e6 | |
mars | 7c738c24ec | |
mars | e5f9985bb9 | |
mars | 71a4eb2cf0 | |
mars | 7f7a88cb15 | |
mars | a925d6b94d | |
mars | f192983319 | |
mars | 7b2a110c12 | |
mars | a648ef360e | |
mars | 58af1e341f | |
mars | da0b2f6eb1 | |
mars | d4d8cab9ad | |
mars | fe6d9ac1a7 | |
mars | 2b3baf2198 | |
mars | 3271c35e41 | |
Emma Tebibyte | 42abb1791d | |
Emma Tebibyte | 31d8bb1fd6 | |
Emma Tebibyte | 359b6897a7 | |
Emma Tebibyte | 1bdfb798ea | |
Emma Tebibyte | 1f8d301103 | |
Emma Tebibyte | 77f5d5c7ce | |
Iris Pupo | 05d68ab3c1 | |
mars | 066430ccba | |
mars | 4b05f55725 | |
mars | fe9ea34a23 | |
mars | 8c9f821835 | |
mars | 4a091bd206 | |
mars | 1e43b0a2c4 | |
mars | 87d70ee6d1 | |
mars | 75fb80adf3 | |
mars | e39d16a516 | |
mars | 69318a02e5 | |
mars | ab50a8e130 | |
mars | 75e0aca668 | |
mars | eb32163b24 | |
mars | 681b884b74 | |
mars | 4798d43f6c | |
mars | 7294c0402c | |
mars | aa333b0fe4 | |
mars | 325a85eb39 | |
mars | 47f0894cbd | |
mars | a2d51b2fa8 | |
mars | 226b54f19e | |
lilith | 27d7bdeb91 | |
lilith | 4b2396d137 | |
mars | 0da4c7d900 | |
mars | 58c7d9192d | |
lilith | d4fc420b18 | |
lilith | 9f77dc8262 | |
mars | b3c60eb73f | |
mars | 44024ccdef | |
mars | 4061af264d | |
mars | 70a4d5b49b | |
mars | 9614eecb82 | |
mars | 0fc2f0b76c | |
mars | 18b882f0f3 | |
mars | 1d2ff14492 | |
mars | 62380e745e | |
mars | 6aa3ca6f33 | |
mars | 7e1ec1ed39 | |
mars | 412a9bf480 | |
mars | 118c881161 | |
mars | 4d9d050720 | |
mars | 23d699dfef | |
mars | 4a5f27a95d | |
mars | 8c8d89b34d | |
mars | ea1631d24d | |
mars | bf2b8360c0 | |
mars | 4bbf60a737 | |
mars | 839f8c6c6e | |
mars | a058d027ef | |
mars | 28c6a3e1e6 | |
mars | e624edc36f | |
mars | 63691ebc28 | |
mars | 4e46832cd0 | |
mars | e1230c9710 | |
mars | a67ee571b2 | |
mars | 786413ed0c | |
mars | 40e8507d83 | |
mars | 97c5a76227 | |
mars | 825f4b28c8 | |
mars | 5459cff113 | |
mars | d5b7ffc9d2 | |
mars | 25e60c683b | |
mars | dea255cf1d | |
mars | b7c5fa0417 | |
mars | e56c76f034 | |
mars | 9d1bd3a256 | |
mars | 1067b9ad0e | |
mars | 5196f0db5f | |
Emma Tebibyte | 4173e75b81 | |
Emma Tebibyte | d1620f1751 | |
Emma Tebibyte | eb87376f90 | |
mars | 5e977c0d55 | |
mars | 4b551b1bc6 | |
Emma Tebibyte | 9a5dd093ef | |
Emma Tebibyte | 35c004c5bf | |
mars | 2ba24a85dc | |
mars | 81bd65cf29 | |
mars | ecbff9975b | |
lilith | 77a99427c8 | |
mars | 33274f6c77 | |
mars | 0903255eda | |
mars | ce05070a92 | |
mars | 6f185ae344 | |
marceline-cramer | 5ab5fb079a |
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are 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.
|
||||
|
||||
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.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
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 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 work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero 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 your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
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 AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,165 @@
|
|||
GNU LESSER 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.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser 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
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
19
Cargo.toml
19
Cargo.toml
|
@ -1,26 +1,33 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/egui",
|
||||
"crates/sao-ui-rs",
|
||||
"apps/magpie",
|
||||
"apps/music-player",
|
||||
"apps/notifications",
|
||||
"apps/sandbox",
|
||||
"crates/script",
|
||||
"crates/types",
|
||||
"crates/textwrap",
|
||||
"scripts/music-player",
|
||||
"scripts/sao-ui",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "canary"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "LGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
allsorts = "0.10"
|
||||
anyhow = "1"
|
||||
bytemuck = "1"
|
||||
canary_types = { path = "crates/types" }
|
||||
canary-script = { path = "crates/script" }
|
||||
log = "0.4"
|
||||
lyon = "1"
|
||||
ouroboros = "^0.15"
|
||||
parking_lot = "0.12"
|
||||
prehash = "0.3.3"
|
||||
slab = "0.4"
|
||||
wasmtime = "0.38"
|
||||
wasmtime = "3"
|
||||
|
||||
[dependencies.font-kit]
|
||||
version = "0.11"
|
||||
version = "*"
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
# Canary
|
||||
|
||||
Canary is a post-structuralist graphical user interface (GUI) framework that
|
||||
uses standardized message-passing protocols to represent UI state instead of a
|
||||
typical DOM-based GUI workflow.
|
||||
|
||||
Canary scripts (executed as WebAssembly) implement all of the rendering and
|
||||
layout of the GUIs, so the host has little to no involvement in their
|
||||
appearance. This allows virtually unlimited customization of Canary GUIs, as
|
||||
scripts can be reloaded by applications with no impact on application behavior.
|
||||
|
||||
Canary's development has been documented on Tebibyte Media's blog:
|
||||
https://tebibyte.media/blog/project/canary/
|
||||
|
||||
# Screenshots
|
||||
|
||||
<div align="center">
|
||||
<figure>
|
||||
<img src="./resources/sandbox-screenshot.jpg"/>
|
||||
<figcaption>A screenshot of the Canary sandbox and Sword Art Online script.</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="./resources/music-player-screenshot.jpg"/>
|
||||
<figcaption>A screenshot of the desktop music player controller using the Sword Art Online script.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
# Using `canary-rs`
|
||||
|
||||
This repository (`canary-rs`) is the reference implementation for Canary. It is
|
||||
written in Rust, and is licensed under the LGPLv3.
|
||||
|
||||
`canary-rs` is the central hub for Canary's development. It includes host-side
|
||||
Rust code, helper crates for Canary hosts, wrapper crates for scripts
|
||||
authored in Rust, and the documentation that you're currently reading.
|
||||
|
||||
`canary-rs` provides a graphical "sandbox" that embeds the Canary runtime
|
||||
into a lightweight graphical app. It has two purposes: first, to give
|
||||
script authors a playground independent of a larger framework to safely debug,
|
||||
benchmark, and experiment with their scripts, and second, to give Canary
|
||||
embedders a live, functioning example of how Canary can be integrated into their
|
||||
applications.
|
||||
|
||||
## Running the `canary-rs` sandbox
|
||||
|
||||
The sandbox requires a Canary script to run. If you don't already have one,
|
||||
you can follow [these instructions](optional-building-the-sword-art-online-demonstration-ui-script)
|
||||
to build the example script provided by `canary-rs`.
|
||||
|
||||
### Building the sandbox
|
||||
|
||||
To build the sandbox from source, first make sure that you have
|
||||
[installed the standard Rust toolchain](https://www.rustlang.org/tools/install),
|
||||
including `rustup`, `rustc`, and `cargo`, as well as a frontend to
|
||||
[Git](https://git-scm.com/). This guide assumes that you are using the Git
|
||||
command-line interface (CLI).
|
||||
|
||||
Next, clone the upstream repository:
|
||||
```sh
|
||||
$ git clone https://git.tebibyte.media/canary/canary-rs.git
|
||||
$ cd canary-rs
|
||||
```
|
||||
|
||||
Then, run `cargo` to build the sandbox package:
|
||||
```sh
|
||||
$ cargo build --release -p canary_sandbox
|
||||
```
|
||||
|
||||
Now, the sandbox can be ran with a script:
|
||||
```sh
|
||||
$ cargo run --release -p canary_sandbox -- <path-to-script>
|
||||
```
|
||||
|
||||
## Running Magpie
|
||||
|
||||
## (Optional) Building the Sword Art Online demonstration UI script
|
||||
|
||||
`canary-rs` provides an example of a fully-functioning script which, optionally,
|
||||
can be built and loaded into the sandbox to ensure its functioning.
|
||||
|
||||
To build it, you must first follow [the instructions above](#building-the-test-harness)
|
||||
to clone and build the sandbox and to set up the Rust toolchain.
|
||||
|
||||
Then, add the `wasm32-unknown-unknown` target so that Rust can compile to
|
||||
WebAssembly:
|
||||
```sh
|
||||
$ rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
Next, compile the example script:
|
||||
```sh
|
||||
$ cargo build --release -p sao-ui-rs --target wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
The path to the built example script is `target/wasm32-unknown-unknown/release/sao_ui_rs.wasm`.
|
||||
Now it can be run using the sandbox:
|
||||
```sh
|
||||
$ cargo run --release -p canary_sandbox -- target/wasm32-unknown-unknown/release/sao_ui_rs.wasm
|
||||
```
|
||||
|
||||
## Using `canary-rs` as a Rust library
|
||||
|
||||
***WARNING***: `canary-rs` is still in alpha development so both its API and its
|
||||
version number are unstable. It is not recommended to use it in your own
|
||||
projects unless you are involved with Canary's development.
|
||||
|
||||
`canary-rs` is not yet available on [crates.io](https://crates.io), so to add it
|
||||
as a dependency, you must add its [upstream git repository](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories)
|
||||
instead:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
canary = { git = "https://git.tebibyte.media/canary/canary-rs", rev = "deadbeef" }
|
||||
```
|
||||
|
||||
Because `canary-rs` is still under active development, it is recommended to
|
||||
pull a fixed, specific commit using the `rev` key. That can be a specific tag,
|
||||
some point in the commit history, or whatever the latest commit on `main` is.
|
||||
|
||||
[Tebibyte Media](https://tebibyte.media) is not capable of hosting rustdocs yet,
|
||||
so to learn how the API works, you can read the source code for the test
|
||||
harness, or dig through the source code itself.
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "canary-magpie"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[[bin]]
|
||||
name = "magpie"
|
||||
path = "src/main.rs"
|
||||
required-features = ["service"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", optional = true }
|
||||
byteorder = "1.4"
|
||||
canary = { path = "../..", optional = true }
|
||||
env_logger = { version = "0.10", optional = true }
|
||||
futures-util = { version = "0.3", optional = true, features = ["io"] }
|
||||
glium = { version = "0.32", optional = true}
|
||||
log = "0.4"
|
||||
mio = { version = "0.8", features = ["net", "os-poll"], optional = true }
|
||||
mio-signals = { version = "0.2", optional = true }
|
||||
parking_lot = { version = "0.12", optional = true}
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
slab = { version = "0.4", optional = true}
|
||||
|
||||
[features]
|
||||
async = ["dep:futures-util"]
|
||||
service = ["dep:anyhow", "dep:canary", "dep:env_logger", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod protocol;
|
||||
|
||||
#[cfg(feature = "service")]
|
||||
pub mod service;
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use glium::glutin::event_loop::EventLoopBuilder;
|
||||
|
||||
use canary_magpie::service::*;
|
||||
use ipc::Ipc;
|
||||
use window::{WindowMessage, WindowStore};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
env_logger::Builder::new()
|
||||
.filter(None, log::LevelFilter::Info) // By default logs all info messages.
|
||||
.parse_default_env()
|
||||
.init();
|
||||
|
||||
log::info!("Initializing Magpie...");
|
||||
let event_loop = EventLoopBuilder::<WindowMessage>::with_user_event().build();
|
||||
let window_sender = event_loop.create_proxy();
|
||||
let (ipc, ipc_sender) = Ipc::new(window_sender)?;
|
||||
let _ipc_thread = std::thread::spawn(|| ipc.run());
|
||||
let window_store = WindowStore::new(ipc_sender);
|
||||
window_store.run(event_loop)
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{Read, Write};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
/// The name of the Magpie server socket.
|
||||
pub const MAGPIE_SOCK: &str = "magpie.sock";
|
||||
|
||||
/// An identifier for a Magpie panel.
|
||||
///
|
||||
/// Only valid on a connection between a single client and its server. Clients
|
||||
/// are allowed to use arbitrary values for [PanelId].
|
||||
pub type PanelId = u32;
|
||||
|
||||
/// Creates a new Magpie panel with a given ID.
|
||||
///
|
||||
/// If the given [PanelId] is already being used on this connection, the server
|
||||
/// will delete the old panel using that [PanelId].
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CreatePanel {
|
||||
pub id: PanelId,
|
||||
pub protocol: String,
|
||||
pub script: PathBuf,
|
||||
pub init_msg: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Closes a Magpie panel with a given ID.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ClosePanel {
|
||||
pub id: PanelId,
|
||||
}
|
||||
|
||||
/// Sends a panel a message.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SendMessage {
|
||||
pub id: PanelId,
|
||||
pub msg: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A message sent from a Magpie client to the server.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum MagpieServerMsg {
|
||||
CreatePanel(CreatePanel),
|
||||
ClosePanel(ClosePanel),
|
||||
SendMessage(SendMessage),
|
||||
}
|
||||
|
||||
/// A message sent from a script's panel to a client.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct RecvMessage {
|
||||
pub id: PanelId,
|
||||
pub msg: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A message sent from the Magpie server to a client.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum MagpieClientMsg {
|
||||
RecvMessage(RecvMessage),
|
||||
}
|
||||
|
||||
/// A [Messenger] specialized for Magpie clients.
|
||||
pub type ClientMessenger<T> = Messenger<T, MagpieClientMsg, MagpieServerMsg>;
|
||||
|
||||
impl<T: Write> ClientMessenger<T> {
|
||||
pub fn send_panel_json<O: Serialize>(&mut self, id: PanelId, msg: &O) {
|
||||
let msg = serde_json::to_string(msg).unwrap();
|
||||
|
||||
let _ = self.send(&MagpieServerMsg::SendMessage(SendMessage {
|
||||
id,
|
||||
msg: msg.into_bytes(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Messenger] specialized for Magpie servers.
|
||||
pub type ServerMessenger<T> = Messenger<T, MagpieServerMsg, MagpieClientMsg>;
|
||||
|
||||
/// Piecewise packet assembler for [Messenger].
|
||||
pub struct MessageQueue<I> {
|
||||
expected_len: Option<usize>,
|
||||
received_buf: VecDeque<u8>,
|
||||
received_queue: VecDeque<I>,
|
||||
}
|
||||
|
||||
impl<I> Default for MessageQueue<I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
expected_len: None,
|
||||
received_buf: Default::default(),
|
||||
received_queue: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: DeserializeOwned> MessageQueue<I> {
|
||||
pub fn on_data(&mut self, data: &[u8]) -> std::io::Result<()> {
|
||||
self.received_buf.write_all(data)?;
|
||||
|
||||
loop {
|
||||
if let Some(expected_len) = self.expected_len {
|
||||
if self.received_buf.len() < expected_len {
|
||||
break;
|
||||
}
|
||||
|
||||
self.expected_len = None;
|
||||
let mut buf = vec![0u8; expected_len];
|
||||
self.received_buf.read_exact(&mut buf)?;
|
||||
match serde_json::from_slice::<I>(&buf) {
|
||||
Ok(received) => self.received_queue.push_front(received),
|
||||
Err(e) => {
|
||||
let kind = std::io::ErrorKind::InvalidData;
|
||||
let payload = Box::new(e);
|
||||
let error = std::io::Error::new(kind, payload);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
} else if self.received_buf.len() >= 4 {
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
let expected_len = self.received_buf.read_u32::<LittleEndian>()?;
|
||||
self.expected_len = Some(expected_len as usize);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recv(&mut self) -> Option<I> {
|
||||
self.received_queue.pop_back()
|
||||
}
|
||||
}
|
||||
|
||||
/// Bidirectional, transport-agnostic Magpie IO wrapper struct.
|
||||
pub struct Messenger<T, I, O> {
|
||||
transport: T,
|
||||
queue: MessageQueue<I>,
|
||||
closed: bool,
|
||||
_output: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<T, I, O> Messenger<T, I, O> {
|
||||
pub fn new(transport: T) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
queue: Default::default(),
|
||||
closed: false,
|
||||
_output: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.closed
|
||||
}
|
||||
|
||||
/// Destroys this messenger and returns the inner transport.
|
||||
pub fn into_transport(self) -> T {
|
||||
self.transport
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Write, I, O: Serialize> Messenger<T, I, O> {
|
||||
pub fn send(&mut self, msg: &O) -> std::io::Result<()> {
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
let payload = serde_json::to_vec(msg).unwrap();
|
||||
let len = payload.len() as u32;
|
||||
self.transport.write_u32::<LittleEndian>(len)?;
|
||||
self.transport.write_all(&payload)?;
|
||||
self.transport.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read, I: DeserializeOwned, O> Messenger<T, I, O> {
|
||||
/// Synchronously receives all pending messages and queues them for [recv].
|
||||
///
|
||||
/// This function only works if the transport is in non-blocking mode.
|
||||
/// Otherwise, this may block while waiting for more data, even if the
|
||||
/// data it receives does not add up to a full message.
|
||||
pub fn flush_recv(&mut self) -> std::io::Result<()> {
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
loop {
|
||||
match self.transport.read(&mut buf) {
|
||||
Ok(0) => {
|
||||
self.closed = true;
|
||||
break;
|
||||
}
|
||||
Err(ref err) if err.kind() == std::io::ErrorKind::ConnectionReset => {
|
||||
self.closed = true;
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
self.queue.on_data(&buf[..n])?;
|
||||
}
|
||||
Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
|
||||
Err(ref err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to receive a single input packet.
|
||||
///
|
||||
/// For messages to be received here, [flush_recv] must be called to
|
||||
/// continuously read pending data from the transport.
|
||||
pub fn try_recv(&mut self) -> Option<I> {
|
||||
self.queue.recv()
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquires the path to the Magpie socket.
|
||||
///
|
||||
/// Currently only joins XDG_RUNTIME_DIR with [MAGPIE_SOCK].
|
||||
pub fn find_socket() -> PathBuf {
|
||||
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
|
||||
let sock_dir = Path::new(&sock_dir);
|
||||
let sock_path = sock_dir.join(MAGPIE_SOCK);
|
||||
sock_path
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
mod async_messages {
|
||||
use super::*;
|
||||
use futures_util::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::marker::Unpin;
|
||||
|
||||
impl<T: AsyncWriteExt + Unpin> ClientMessenger<T> {
|
||||
pub async fn send_panel_json_async<O: Serialize>(&mut self, id: PanelId, msg: &O) {
|
||||
let msg = serde_json::to_string(msg).unwrap();
|
||||
|
||||
let _ = self
|
||||
.send_async(&MagpieServerMsg::SendMessage(SendMessage {
|
||||
id,
|
||||
msg: msg.into_bytes(),
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncWriteExt + Unpin, I, O: Serialize> Messenger<T, I, O> {
|
||||
pub async fn send_async(&mut self, msg: &O) -> std::io::Result<()> {
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
let payload = serde_json::to_vec(msg).unwrap();
|
||||
let len = payload.len() as u32;
|
||||
let mut msg = Vec::with_capacity(4 + payload.len());
|
||||
msg.write_u32::<LittleEndian>(len)?;
|
||||
msg.extend_from_slice(&payload);
|
||||
self.transport.write_all(&msg).await?;
|
||||
self.transport.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncReadExt + Unpin, I: DeserializeOwned, O> Messenger<T, I, O> {
|
||||
pub async fn recv(&mut self) -> std::io::Result<I> {
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
loop {
|
||||
if let Some(msg) = self.queue.recv() {
|
||||
return Ok(msg);
|
||||
}
|
||||
|
||||
let num = self.transport.read(&mut buf).await?;
|
||||
self.queue.on_data(&buf[..num])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary::{DrawCommand, Vec2, PX_PER_MM};
|
||||
use glium::{program::ProgramCreationInput, Surface};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 2],
|
||||
pub color: [u8; 4],
|
||||
}
|
||||
|
||||
glium::implement_vertex!(Vertex, position normalize(false), color normalize(true));
|
||||
|
||||
impl Vertex {
|
||||
pub fn from_canary(size: Vec2, v: &canary::MeshVertex) -> Self {
|
||||
// TODO do this in the vertex shader with a size uniform
|
||||
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
||||
Self {
|
||||
position: [
|
||||
(v.position.x / size.x) * 2.0 - 1.0,
|
||||
(v.position.y / size.y) * -2.0 + 1.0,
|
||||
],
|
||||
color: [r, g, b, a],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const VERTEX_SHADER_SRC: &str = r#"
|
||||
#version 330
|
||||
|
||||
in vec2 position;
|
||||
in vec4 color;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
frag_color = color;
|
||||
}
|
||||
"#;
|
||||
|
||||
const FRAGMENT_SHADER_SRC: &str = r#"
|
||||
#version 330
|
||||
|
||||
in vec4 frag_color;
|
||||
|
||||
out vec4 fb_color;
|
||||
|
||||
void main() {
|
||||
fb_color = frag_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
pub struct Graphics {
|
||||
pub display: glium::Display,
|
||||
pub program: glium::Program,
|
||||
}
|
||||
|
||||
impl Graphics {
|
||||
pub fn new(display: glium::Display) -> Self {
|
||||
let program = glium::Program::new(
|
||||
&display,
|
||||
ProgramCreationInput::SourceCode {
|
||||
vertex_shader: VERTEX_SHADER_SRC,
|
||||
tessellation_control_shader: None,
|
||||
tessellation_evaluation_shader: None,
|
||||
geometry_shader: None,
|
||||
fragment_shader: FRAGMENT_SHADER_SRC,
|
||||
transform_feedback_varyings: None,
|
||||
outputs_srgb: true, // don't automatically apply gamma correction
|
||||
uses_point_size: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Self { display, program }
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, commands: &[DrawCommand]) {
|
||||
let mut joined_vs: Vec<Vertex> = Vec::new();
|
||||
let mut joined_is = Vec::new();
|
||||
|
||||
let (width, height) = {
|
||||
let size = self.display.gl_window().window().inner_size();
|
||||
let (width, height) = (size.width as f32, size.height as f32);
|
||||
(width * PX_PER_MM, height * PX_PER_MM)
|
||||
};
|
||||
|
||||
let size = Vec2 {
|
||||
x: width,
|
||||
y: height,
|
||||
};
|
||||
|
||||
for command in commands.iter() {
|
||||
match command {
|
||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||
let voff = joined_vs.len() as canary::MeshIndex;
|
||||
joined_vs.extend(vertices.iter().map(|v| Vertex::from_canary(size, v)));
|
||||
joined_is.extend(indices.iter().map(|i| i + voff));
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
let vertex_buffer = glium::VertexBuffer::new(&self.display, &joined_vs).unwrap();
|
||||
let index_buffer = glium::IndexBuffer::new(
|
||||
&self.display,
|
||||
glium::index::PrimitiveType::TrianglesList,
|
||||
&joined_is,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let params = glium::DrawParameters {
|
||||
blend: glium::Blend::alpha_blending(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut target = self.display.draw();
|
||||
target.clear_color(0.0, 0.0, 0.0, 0.0);
|
||||
target
|
||||
.draw(
|
||||
&vertex_buffer,
|
||||
&index_buffer,
|
||||
&self.program,
|
||||
&glium::uniforms::EmptyUniforms,
|
||||
¶ms,
|
||||
)
|
||||
.unwrap();
|
||||
target.finish().unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use mio::net::{UnixListener, UnixStream};
|
||||
use mio::{Events, Interest, Poll, Token, Waker};
|
||||
use mio_signals::{Signal, Signals};
|
||||
use parking_lot::RwLock;
|
||||
use slab::Slab;
|
||||
|
||||
use crate::protocol::*;
|
||||
use crate::service::window::{WindowMessage, WindowMessageSender};
|
||||
|
||||
const SOCK_NAME: &str = "magpie.sock";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IpcMessage {
|
||||
PanelMessage { window: usize, message: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IpcMessageSender {
|
||||
waker: Arc<Waker>,
|
||||
sender: Sender<IpcMessage>,
|
||||
}
|
||||
|
||||
impl IpcMessageSender {
|
||||
pub fn send(&self, msg: IpcMessage) {
|
||||
let _ = self.sender.send(msg);
|
||||
let _ = self.waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps [mio::net::UnixListener] with automatic file deletion on drop.
|
||||
pub struct Listener {
|
||||
pub uds: UnixListener,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for Listener {
|
||||
fn drop(&mut self) {
|
||||
match std::fs::remove_file(&self.path) {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("Could not delete UnixListener {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Listener {
|
||||
type Target = UnixListener;
|
||||
|
||||
fn deref(&self) -> &UnixListener {
|
||||
&self.uds
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Listener {
|
||||
fn deref_mut(&mut self) -> &mut UnixListener {
|
||||
&mut self.uds
|
||||
}
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
fn new() -> std::io::Result<Self> {
|
||||
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
|
||||
let sock_dir = Path::new(&sock_dir);
|
||||
let sock_path = sock_dir.join(SOCK_NAME);
|
||||
|
||||
use std::io::{Error, ErrorKind};
|
||||
match UnixStream::connect(&sock_path) {
|
||||
Ok(_) => {
|
||||
log::warn!("Socket is already in use. Another instance of Magpie may be running.");
|
||||
let kind = ErrorKind::AddrInUse;
|
||||
let error = Error::new(kind, "Socket is already in use.");
|
||||
return Err(error);
|
||||
}
|
||||
Err(ref err) if err.kind() == ErrorKind::ConnectionRefused => {
|
||||
log::warn!("Found leftover socket; removing.");
|
||||
std::fs::remove_file(&sock_path)?;
|
||||
}
|
||||
Err(ref err) if err.kind() == ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
log::info!("Making socket at: {:?}", sock_path);
|
||||
let uds = UnixListener::bind(&sock_path)?;
|
||||
let path = sock_path.to_path_buf();
|
||||
Ok(Self { uds, path })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IpcData {
|
||||
poll: Poll,
|
||||
window_to_client_panel: HashMap<usize, (usize, PanelId)>,
|
||||
next_window_id: usize,
|
||||
}
|
||||
|
||||
impl IpcData {
|
||||
pub fn new_window_id(&mut self) -> usize {
|
||||
let id = self.next_window_id;
|
||||
self.next_window_id = self.next_window_id.wrapping_add(1);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
data: Arc<RwLock<IpcData>>,
|
||||
window_sender: WindowMessageSender,
|
||||
messenger: ServerMessenger<UnixStream>,
|
||||
token: Token,
|
||||
id_to_window: HashMap<u32, usize>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn on_readable(&mut self) -> std::io::Result<bool> {
|
||||
if let Err(err) = self.messenger.flush_recv() {
|
||||
log::error!("flush_recv() error: {:?}", err);
|
||||
}
|
||||
|
||||
while let Some(msg) = self.messenger.try_recv() {
|
||||
log::debug!("Client #{}: {:?}", self.token.0, msg);
|
||||
match msg {
|
||||
MagpieServerMsg::CreatePanel(CreatePanel {
|
||||
id,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
}) => {
|
||||
let mut data = self.data.write();
|
||||
|
||||
let window = data.new_window_id();
|
||||
data.window_to_client_panel
|
||||
.insert(window, (self.token.0, id));
|
||||
|
||||
if let Some(old_id) = self.id_to_window.insert(id, window) {
|
||||
let msg = WindowMessage::CloseWindow { id: old_id };
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
}
|
||||
|
||||
let msg = WindowMessage::OpenWindow {
|
||||
id: window,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
};
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
}
|
||||
MagpieServerMsg::ClosePanel(ClosePanel { id }) => {
|
||||
if let Some(id) = self.id_to_window.get(&id).copied() {
|
||||
let msg = WindowMessage::CloseWindow { id };
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
}
|
||||
}
|
||||
MagpieServerMsg::SendMessage(SendMessage { id, msg }) => {
|
||||
if let Some(id) = self.id_to_window.get(&id).cloned() {
|
||||
let msg = WindowMessage::SendMessage { id, msg };
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.messenger.is_closed())
|
||||
}
|
||||
|
||||
pub fn disconnect(mut self) {
|
||||
log::info!("Client #{} disconnected", self.token.0);
|
||||
|
||||
let mut transport = self.messenger.into_transport();
|
||||
let mut data = self.data.write();
|
||||
let _ = data.poll.registry().deregister(&mut transport);
|
||||
|
||||
for (_id, window) in self.id_to_window.drain() {
|
||||
let msg = WindowMessage::CloseWindow { id: window };
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
data.window_to_client_panel.remove(&window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ipc {
|
||||
pub data: Arc<RwLock<IpcData>>,
|
||||
pub window_sender: WindowMessageSender,
|
||||
pub message_recv: Receiver<IpcMessage>,
|
||||
pub quit: bool,
|
||||
pub listener: Listener,
|
||||
pub signals: Signals,
|
||||
pub listener_token: Token,
|
||||
pub signals_token: Token,
|
||||
pub message_recv_token: Token,
|
||||
pub clients: Slab<Client>,
|
||||
}
|
||||
|
||||
impl Ipc {
|
||||
pub fn new(window_sender: WindowMessageSender) -> std::io::Result<(Self, IpcMessageSender)> {
|
||||
let mut listener = Listener::new()?;
|
||||
|
||||
let mut signals = Signals::new(Signal::Interrupt | Signal::Quit)?;
|
||||
|
||||
let poll = Poll::new()?;
|
||||
let listener_token = Token(usize::MAX);
|
||||
let signals_token = Token(listener_token.0 - 1);
|
||||
let message_recv_token = Token(signals_token.0 - 1);
|
||||
|
||||
let registry = poll.registry();
|
||||
let interest = Interest::READABLE;
|
||||
registry.register(&mut listener.uds, listener_token, interest)?;
|
||||
registry.register(&mut signals, signals_token, interest)?;
|
||||
|
||||
let (sender, message_recv) = channel();
|
||||
|
||||
let sender = IpcMessageSender {
|
||||
waker: Arc::new(Waker::new(registry, message_recv_token)?),
|
||||
sender,
|
||||
};
|
||||
|
||||
let data = IpcData {
|
||||
poll,
|
||||
window_to_client_panel: HashMap::new(),
|
||||
next_window_id: 0,
|
||||
};
|
||||
|
||||
let ipc = Self {
|
||||
data: Arc::new(RwLock::new(data)),
|
||||
window_sender,
|
||||
message_recv,
|
||||
quit: false,
|
||||
listener,
|
||||
signals,
|
||||
listener_token,
|
||||
signals_token,
|
||||
message_recv_token,
|
||||
clients: Default::default(),
|
||||
};
|
||||
|
||||
Ok((ipc, sender))
|
||||
}
|
||||
|
||||
pub fn on_message(&mut self, msg: IpcMessage) -> std::io::Result<()> {
|
||||
match msg {
|
||||
IpcMessage::PanelMessage { window, message } => {
|
||||
let data = self.data.read();
|
||||
let (client, panel) = *data.window_to_client_panel.get(&window).unwrap();
|
||||
let client = self.clients.get_mut(client).unwrap();
|
||||
let reply = RecvMessage {
|
||||
id: panel,
|
||||
msg: message,
|
||||
};
|
||||
client
|
||||
.messenger
|
||||
.send(&MagpieClientMsg::RecvMessage(reply))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, events: &mut Events, timeout: Option<Duration>) -> std::io::Result<()> {
|
||||
self.data.write().poll.poll(events, timeout)?;
|
||||
|
||||
for event in events.iter() {
|
||||
if event.token() == self.listener_token {
|
||||
loop {
|
||||
match self.listener.accept() {
|
||||
Ok((mut connection, address)) => {
|
||||
let token = Token(self.clients.vacant_key());
|
||||
log::info!(
|
||||
"Accepting connection (Client #{}) from {:?}",
|
||||
token.0,
|
||||
address
|
||||
);
|
||||
|
||||
let interest = Interest::READABLE;
|
||||
self.data.write().poll.registry().register(
|
||||
&mut connection,
|
||||
token,
|
||||
interest,
|
||||
)?;
|
||||
|
||||
self.clients.insert(Client {
|
||||
messenger: ServerMessenger::new(connection),
|
||||
token,
|
||||
data: self.data.clone(),
|
||||
window_sender: self.window_sender.clone(),
|
||||
id_to_window: Default::default(),
|
||||
});
|
||||
}
|
||||
Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
} else if event.token() == self.signals_token {
|
||||
while let Some(received) = self.signals.receive()? {
|
||||
log::info!("Received {:?} signal; exiting...", received);
|
||||
let _ = self.window_sender.send_event(WindowMessage::Quit);
|
||||
self.quit = true;
|
||||
}
|
||||
} else if event.token() == self.message_recv_token {
|
||||
while let Ok(received) = self.message_recv.try_recv() {
|
||||
self.on_message(received)?;
|
||||
}
|
||||
} else if let Some(client) = self.clients.get_mut(event.token().0) {
|
||||
let disconnected = client.on_readable()?;
|
||||
if disconnected {
|
||||
self.clients.remove(event.token().0).disconnect();
|
||||
}
|
||||
} else {
|
||||
log::error!("Unrecognized event token: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(mut self) {
|
||||
let mut events = Events::with_capacity(128);
|
||||
while !self.quit {
|
||||
let wait = Duration::from_millis(100);
|
||||
match self.poll(&mut events, Some(wait)) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("IPC poll error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod gl;
|
||||
pub mod ipc;
|
||||
pub mod window;
|
|
@ -0,0 +1,257 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use canary::{CursorEventKind, Panel, Runtime, Vec2, PX_PER_MM};
|
||||
use glium::backend::glutin::DisplayCreationError;
|
||||
use glium::{glutin, Surface};
|
||||
use glutin::event::{ElementState, Event, MouseButton, WindowEvent};
|
||||
use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget};
|
||||
use glutin::window::WindowId;
|
||||
|
||||
use crate::service::gl::Graphics;
|
||||
use crate::service::ipc::{IpcMessage, IpcMessageSender};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum WindowMessage {
|
||||
OpenWindow {
|
||||
id: usize,
|
||||
protocol: String,
|
||||
script: PathBuf,
|
||||
init_msg: Vec<u8>,
|
||||
},
|
||||
CloseWindow {
|
||||
id: usize,
|
||||
},
|
||||
Quit,
|
||||
SendMessage {
|
||||
id: usize,
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
|
||||
|
||||
pub struct Window {
|
||||
pub ipc_sender: IpcMessageSender,
|
||||
pub ipc_id: usize,
|
||||
pub graphics: Graphics,
|
||||
pub panel: Panel,
|
||||
pub last_update: Instant,
|
||||
pub cursor_pos: Vec2,
|
||||
pub cursor_down: bool,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(
|
||||
ipc_sender: IpcMessageSender,
|
||||
ipc_id: usize,
|
||||
panel: Panel,
|
||||
event_loop: &EventLoopWindowTarget<WindowMessage>,
|
||||
) -> Result<Self, DisplayCreationError> {
|
||||
let wb = glutin::window::WindowBuilder::new()
|
||||
.with_transparent(true)
|
||||
.with_decorations(false);
|
||||
let cb = glutin::ContextBuilder::new()
|
||||
.with_vsync(true)
|
||||
.with_multisampling(4);
|
||||
let display = glium::Display::new(wb, cb, &event_loop)?;
|
||||
let graphics = Graphics::new(display);
|
||||
let last_update = Instant::now();
|
||||
Ok(Self {
|
||||
ipc_sender,
|
||||
ipc_id,
|
||||
graphics,
|
||||
panel,
|
||||
last_update,
|
||||
cursor_pos: Vec2::ZERO,
|
||||
cursor_down: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> WindowId {
|
||||
self.graphics.display.gl_window().window().id()
|
||||
}
|
||||
|
||||
pub fn request_redraw(&mut self) {
|
||||
self.graphics.display.gl_window().window().request_redraw();
|
||||
}
|
||||
|
||||
/// Receives all messages from the script and forwards them to IPC.
|
||||
pub fn recv_messages(&mut self) {
|
||||
for message in self.panel.recv_messages() {
|
||||
self.ipc_sender.send(IpcMessage::PanelMessage {
|
||||
window: self.ipc_id,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
let now = Instant::now();
|
||||
let dt = now.duration_since(self.last_update).as_secs_f32();
|
||||
self.panel.update(dt);
|
||||
self.last_update = now;
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let commands = self.panel.draw();
|
||||
self.graphics.draw(&commands);
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn send_message(&mut self, msg: Vec<u8>) {
|
||||
self.panel.on_message(msg);
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, new_size: Vec2) {
|
||||
self.panel.on_resize(new_size);
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn on_event(&mut self, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
self.resize(Vec2::new(size.width as f32, size.height as f32) * PX_PER_MM);
|
||||
self.request_redraw()
|
||||
}
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
let x = position.x as f32 * PX_PER_MM;
|
||||
let y = position.y as f32 * PX_PER_MM;
|
||||
self.cursor_pos = Vec2::new(x, y);
|
||||
|
||||
let event = if self.cursor_down {
|
||||
CursorEventKind::Drag
|
||||
} else {
|
||||
CursorEventKind::Hover
|
||||
};
|
||||
|
||||
self.panel.on_cursor_event(event, self.cursor_pos);
|
||||
self.recv_messages();
|
||||
}
|
||||
WindowEvent::MouseInput {
|
||||
state,
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
} => {
|
||||
let event = match state {
|
||||
ElementState::Pressed => {
|
||||
self.cursor_down = true;
|
||||
CursorEventKind::Select
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.cursor_down = false;
|
||||
CursorEventKind::Deselect
|
||||
}
|
||||
};
|
||||
|
||||
self.panel.on_cursor_event(event, self.cursor_pos);
|
||||
self.recv_messages();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowStore {
|
||||
pub ipc_sender: IpcMessageSender,
|
||||
pub ipc_to_window: HashMap<usize, WindowId>,
|
||||
pub windows: HashMap<WindowId, Window>,
|
||||
pub runtime: Runtime,
|
||||
}
|
||||
|
||||
impl WindowStore {
|
||||
pub fn new(ipc_sender: IpcMessageSender) -> Self {
|
||||
let backend = canary::backend::make_default_backend().unwrap();
|
||||
let runtime = Runtime::new(backend).unwrap();
|
||||
|
||||
Self {
|
||||
ipc_sender,
|
||||
ipc_to_window: Default::default(),
|
||||
windows: Default::default(),
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ipc_window(&mut self, id: usize) -> Option<&mut Window> {
|
||||
self.ipc_to_window
|
||||
.get(&id)
|
||||
.map(|id| self.windows.get_mut(id))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn on_message(
|
||||
&mut self,
|
||||
event_loop: &EventLoopWindowTarget<WindowMessage>,
|
||||
message: WindowMessage,
|
||||
) -> anyhow::Result<bool> {
|
||||
match message {
|
||||
WindowMessage::OpenWindow {
|
||||
id,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
} => {
|
||||
log::debug!("Opening window {} with script {:?}...", id, script);
|
||||
let start = std::time::Instant::now();
|
||||
let module = std::fs::read(script)?;
|
||||
let mut script = self.runtime.load_module(&module)?;
|
||||
log::debug!("Instantiated window {} script in {:?}", id, start.elapsed());
|
||||
let panel = script.create_panel(&protocol, init_msg)?;
|
||||
log::debug!("Created window {} panel in {:?}", id, start.elapsed());
|
||||
let window = Window::new(self.ipc_sender.to_owned(), id, panel, &event_loop)?;
|
||||
let window_id = window.get_id();
|
||||
self.windows.insert(window_id, window);
|
||||
self.ipc_to_window.insert(id, window_id);
|
||||
log::debug!("Opened window {} in {:?}", id, start.elapsed());
|
||||
}
|
||||
WindowMessage::CloseWindow { id } => {
|
||||
if let Some(window_id) = self.ipc_to_window.remove(&id) {
|
||||
self.windows.remove(&window_id);
|
||||
}
|
||||
}
|
||||
WindowMessage::Quit => return Ok(true),
|
||||
WindowMessage::SendMessage { id, msg } => {
|
||||
if let Some(window) = self.get_ipc_window(id) {
|
||||
window.send_message(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn run(mut self, event_loop: EventLoop<WindowMessage>) -> ! {
|
||||
event_loop.run(move |event, event_loop, control_flow| match event {
|
||||
Event::WindowEvent { window_id, event } => {
|
||||
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||
window.on_event(event);
|
||||
}
|
||||
}
|
||||
Event::RedrawRequested(id) => {
|
||||
if let Some(window) = self.windows.get_mut(&id) {
|
||||
window.draw();
|
||||
}
|
||||
}
|
||||
Event::MainEventsCleared => {
|
||||
for (_id, window) in self.windows.iter_mut() {
|
||||
window.update();
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
Event::UserEvent(event) => match self.on_message(event_loop, event.clone()) {
|
||||
Ok(false) => {}
|
||||
Ok(true) => *control_flow = ControlFlow::Exit,
|
||||
Err(err) => {
|
||||
log::error!("Error while handling message {:?}:\n{}", event, err);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "canary-music-player"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[[bin]]
|
||||
name = "canary-music-player"
|
||||
path = "src/main.rs"
|
||||
required-features = ["bin"]
|
||||
|
||||
[dependencies]
|
||||
async-std = { version = "1.12", optional = true, features = ["attributes"] }
|
||||
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
|
||||
futures-util = { version = "0.3", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
zbus = { version = "3.5", optional = true }
|
||||
|
||||
[features]
|
||||
bin = ["dep:async-std", "dep:canary-magpie", "dep:futures-util", "dep:zbus"]
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use serde;
|
||||
pub use serde_json;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PlaybackStatus {
|
||||
/// A track is currently playing.
|
||||
Playing,
|
||||
|
||||
/// A track is currently paused.
|
||||
Paused,
|
||||
|
||||
/// No track is currently playing.
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum LoopStatus {
|
||||
/// The playback will stop when there are no more tracks to play.
|
||||
None,
|
||||
|
||||
/// The current track will start again from the beginning once it has finished playing.
|
||||
Track,
|
||||
|
||||
/// The playback loops through a list of tracks.
|
||||
Playlist,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ProgressChanged {
|
||||
/// Current position into the track in seconds.
|
||||
pub position: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct AlbumInfo {
|
||||
/// The title of the current album.
|
||||
pub title: Option<String>,
|
||||
|
||||
/// The list of artists of the album.
|
||||
pub artists: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TrackInfo {
|
||||
/// The title of the current track.
|
||||
pub title: Option<String>,
|
||||
|
||||
/// The list of artists on this track. May be empty.
|
||||
pub artists: Vec<String>,
|
||||
|
||||
/// The optional track number on the disc the album the track appears on.
|
||||
pub track_number: Option<i32>,
|
||||
|
||||
/// Length of the track in seconds.
|
||||
pub length: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum InMsg {
|
||||
Connected,
|
||||
Disconnected,
|
||||
PlaybackStatusChanged(PlaybackStatus),
|
||||
VolumeChanged { volume: f32 },
|
||||
ShuffleChanged { shuffle: bool },
|
||||
LoopingChanged(LoopStatus),
|
||||
ProgressChanged(ProgressChanged),
|
||||
AlbumChanged(AlbumInfo),
|
||||
TrackChanged(TrackInfo),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum OutMsg {
|
||||
/// Brings the media player's user interface to the front using any
|
||||
/// appropriate mechanism available.
|
||||
Raise,
|
||||
|
||||
/// Pauses playback.
|
||||
Pause,
|
||||
|
||||
/// Resumes playback.
|
||||
Play,
|
||||
|
||||
/// Toggles playback between paused and resumed.
|
||||
PlayPause,
|
||||
|
||||
/// Stops playback.
|
||||
Stop,
|
||||
|
||||
/// Skips to the next track in the tracklist. Stops playback if there is
|
||||
/// none.
|
||||
Next,
|
||||
|
||||
/// Skips to the previous track in the tracklist. Stops playback if there
|
||||
/// is no previous track and endless playback and track repeat are off.
|
||||
Previous,
|
||||
|
||||
/// Sets the volume. Values are clamped to 0.0 to 1.0.
|
||||
SetVolume { volume: f32 },
|
||||
|
||||
/// Seeks the current track's position in seconds.
|
||||
Seek { offset: f32 },
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary_magpie::protocol::{
|
||||
ClientMessenger, CreatePanel, MagpieClientMsg, MagpieServerMsg, RecvMessage, MAGPIE_SOCK,
|
||||
};
|
||||
use canary_music_player::*;
|
||||
|
||||
use async_std::os::unix::net::UnixStream;
|
||||
|
||||
pub type MagpieClient = ClientMessenger<UnixStream>;
|
||||
|
||||
pub mod mpris;
|
||||
|
||||
use mpris::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Metadata {
|
||||
pub album: AlbumInfo,
|
||||
pub track: TrackInfo,
|
||||
}
|
||||
|
||||
impl<'a> From<MetadataMap<'a>> for Metadata {
|
||||
fn from(map: MetadataMap<'a>) -> Self {
|
||||
let album = AlbumInfo {
|
||||
title: map
|
||||
.get("xesam:album")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
artists: map
|
||||
.get("xesam:albumArtist")
|
||||
.cloned()
|
||||
.and_then(|v| TryFrom::try_from(v).ok())
|
||||
.unwrap_or(Vec::new()),
|
||||
};
|
||||
|
||||
let track = TrackInfo {
|
||||
title: map
|
||||
.get("xesam:title")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
artists: map
|
||||
.get("xesam:artist")
|
||||
.cloned()
|
||||
.and_then(|v| TryFrom::try_from(v).ok())
|
||||
.unwrap_or(Vec::new()),
|
||||
track_number: map
|
||||
.get("xesam:trackNumber")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
length: map
|
||||
.get("mpris:length")
|
||||
.and_then(|v| i64::try_from(v).ok())
|
||||
.map(|us| us as f32 / 1_000_000.0), // 1,000,000 microseconds in a second
|
||||
};
|
||||
|
||||
Self { album, track }
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
pub async fn update_new(magpie: &mut MagpieClient, metadata: MetadataMap<'_>) -> Self {
|
||||
let new: Self = metadata.into();
|
||||
let msg = InMsg::AlbumChanged(new.album.clone());
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
let msg = InMsg::TrackChanged(new.track.clone());
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
new
|
||||
}
|
||||
|
||||
pub async fn update_diff(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap<'_>) {
|
||||
let new: Self = metadata.into();
|
||||
|
||||
if self.album != new.album {
|
||||
let msg = InMsg::AlbumChanged(new.album.clone());
|
||||
messenger.send_panel_json_async(0, &msg).await;
|
||||
}
|
||||
|
||||
if self.track != new.track {
|
||||
let msg = InMsg::TrackChanged(new.track.clone());
|
||||
messenger.send_panel_json_async(0, &msg).await;
|
||||
let progress = ProgressChanged { position: 0.0 };
|
||||
let msg = InMsg::ProgressChanged(progress);
|
||||
messenger.send_panel_json_async(0, &msg).await;
|
||||
}
|
||||
|
||||
*self = new;
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_message(
|
||||
player: &PlayerProxy<'_>,
|
||||
magpie: &mut MagpieClient,
|
||||
message: MagpieClientMsg,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let message = match message {
|
||||
MagpieClientMsg::RecvMessage(RecvMessage { id: 0, msg }) => msg,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let message: OutMsg = match serde_json::from_slice(&message) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("Panel message parse error: {:?}", err);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
match message {
|
||||
OutMsg::Pause => player.pause().await?,
|
||||
OutMsg::Play => player.play().await?,
|
||||
OutMsg::PlayPause => player.play_pause().await?,
|
||||
OutMsg::Stop => player.stop().await?,
|
||||
OutMsg::Previous => player.previous().await?,
|
||||
OutMsg::Next => player.next().await?,
|
||||
OutMsg::Seek { offset } => {
|
||||
let offset = (offset * 1_000_000.0) as i64; // Seconds to microseconds
|
||||
player.seek(offset).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn player_main(
|
||||
player: &PlayerProxy<'_>,
|
||||
magpie: &mut MagpieClient,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use futures_util::{FutureExt, StreamExt};
|
||||
let mut playback_status = player.receive_playback_status_changed().await.fuse();
|
||||
let mut metadata_tracker = player.receive_metadata_changed().await.fuse();
|
||||
let mut position_tracker = player.receive_position_changed().await.fuse();
|
||||
let mut metadata = Metadata::update_new(magpie, player.metadata().await?).await;
|
||||
|
||||
loop {
|
||||
futures_util::select! {
|
||||
msg = magpie.recv().fuse() => {
|
||||
match msg {
|
||||
Ok(msg) => on_message(player, magpie, msg).await?,
|
||||
Err(err) => eprintln!("Magpie recv error: {:?}", err),
|
||||
}
|
||||
}
|
||||
// TODO also update volume, shuffle status, and loop status
|
||||
status = playback_status.next() => {
|
||||
let status = match status {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let status = status.get().await?;
|
||||
let status = match status.as_str() {
|
||||
"Playing" => Some(PlaybackStatus::Playing),
|
||||
"Paused" => Some(PlaybackStatus::Paused),
|
||||
"Stopped" => Some(PlaybackStatus::Stopped),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(status) = status {
|
||||
let msg = InMsg::PlaybackStatusChanged(status);
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
}
|
||||
}
|
||||
position = position_tracker.next() => {
|
||||
let position = match position {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let position = position.get().await?;
|
||||
let position = position as f32 / 1_000_000.0; // Microseconds to seconds
|
||||
let progress = ProgressChanged { position };
|
||||
let msg = InMsg::ProgressChanged(progress);
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
}
|
||||
new_metadata = metadata_tracker.next() => {
|
||||
let new_metadata = match new_metadata {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let new_metadata = new_metadata.get().await?;
|
||||
metadata.update_diff(magpie, new_metadata).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let module_path = args
|
||||
.get(1)
|
||||
.expect("Please pass a path to a Canary script!")
|
||||
.to_owned();
|
||||
|
||||
let sock_path = canary_magpie::protocol::find_socket();
|
||||
let socket = UnixStream::connect(sock_path).await.unwrap();
|
||||
let mut magpie = MagpieClient::new(socket);
|
||||
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||
let script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel {
|
||||
id: 0,
|
||||
protocol,
|
||||
script,
|
||||
init_msg: vec![],
|
||||
};
|
||||
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.send_async(&msg).await.unwrap();
|
||||
|
||||
let dbus = zbus::Connection::session().await.unwrap();
|
||||
|
||||
let mut first_loop = true;
|
||||
let mut connected = false;
|
||||
|
||||
loop {
|
||||
if !first_loop {
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
async_std::task::sleep(wait).await;
|
||||
}
|
||||
|
||||
first_loop = false;
|
||||
|
||||
if connected {
|
||||
println!("Disconnected from MPRIS");
|
||||
let msg = InMsg::Disconnected;
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
connected = false;
|
||||
}
|
||||
|
||||
println!("Connecting to MPRIS...");
|
||||
|
||||
let player = match find_player(&dbus).await {
|
||||
Ok(Some(player)) => player,
|
||||
Ok(None) => {
|
||||
eprintln!("Couldn't find player");
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while finding player: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.path().as_str(),
|
||||
player.destination().as_str()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_panel_json_async(0, &InMsg::Connected).await;
|
||||
|
||||
match player_main(&player, &mut magpie).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while connected to player: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zbus::fdo::DBusProxy;
|
||||
use zbus::zvariant::Value;
|
||||
use zbus::{dbus_proxy, Connection, Result};
|
||||
|
||||
pub type MetadataMap<'a> = HashMap<String, Value<'a>>;
|
||||
|
||||
#[dbus_proxy(
|
||||
interface = "org.mpris.MediaPlayer2.Player",
|
||||
default_path = "/org/mpris/MediaPlayer2"
|
||||
)]
|
||||
trait Player {
|
||||
fn next(&self) -> Result<()>;
|
||||
fn previous(&self) -> Result<()>;
|
||||
fn pause(&self) -> Result<()>;
|
||||
fn play_pause(&self) -> Result<()>;
|
||||
fn stop(&self) -> Result<()>;
|
||||
fn play(&self) -> Result<()>;
|
||||
fn seek(&self, offset: i64) -> Result<()>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn playback_status(&self) -> Result<String>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn position(&self) -> Result<i64>;
|
||||
|
||||
#[dbus_proxy(property)]
|
||||
fn metadata(&self) -> Result<MetadataMap>;
|
||||
}
|
||||
|
||||
pub async fn find_player(connection: &Connection) -> Result<Option<PlayerProxy>> {
|
||||
let dbus = DBusProxy::new(connection).await?;
|
||||
let names = dbus.list_names().await?;
|
||||
|
||||
for name in names {
|
||||
let name = name.as_str().to_string();
|
||||
if name.starts_with("org.mpris.MediaPlayer2") {
|
||||
let player = PlayerProxy::builder(connection)
|
||||
.destination(name)?
|
||||
.build()
|
||||
.await?;
|
||||
return Ok(Some(player));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "canary-notifications"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "canary-notifications"
|
||||
path = "src/main.rs"
|
||||
required-features = ["bin"]
|
||||
|
||||
[dependencies]
|
||||
async-std = { version = "1.12", optional = true, features = ["attributes"] }
|
||||
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
zbus = { version = "3.5", optional = true }
|
||||
|
||||
[features]
|
||||
bin = ["dep:async-std", "dep:canary-magpie", "dep:zbus"]
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use serde;
|
||||
pub use serde_json;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Contents {
|
||||
/// The optional name of the application sending the notification.
|
||||
pub app_name: Option<String>,
|
||||
|
||||
/// The summary text briefly describing the notification.
|
||||
pub summary: String,
|
||||
|
||||
/// The optional detailed body text.
|
||||
pub body: Option<String>,
|
||||
|
||||
/// The timeout time in milliseconds since the display of the notification
|
||||
/// at which the notification should automatically close.
|
||||
pub timeout: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum OutMsg {}
|
|
@ -0,0 +1,143 @@
|
|||
use std::collections::HashMap;
|
||||
use std::future::pending;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use async_std::channel::{unbounded, Sender};
|
||||
use async_std::os::unix::net::UnixStream;
|
||||
use canary_magpie::protocol::*;
|
||||
use canary_notifications::Contents;
|
||||
use zbus::{dbus_interface, zvariant::Value, ConnectionBuilder, SignalContext};
|
||||
|
||||
pub type MagpieClient = ClientMessenger<UnixStream>;
|
||||
|
||||
pub struct Notifications {
|
||||
module_path: PathBuf,
|
||||
magpie_sender: Sender<MagpieServerMsg>,
|
||||
next_id: u32,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.freedesktop.Notifications")]
|
||||
impl Notifications {
|
||||
fn get_capabilities(&self) -> Vec<String> {
|
||||
vec!["body", "body-markup", "actions", "icon-static"]
|
||||
.into_iter()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[dbus_interface(out_args("name", "vendor", "version", "spec_version"))]
|
||||
fn get_server_information(&self) -> zbus::fdo::Result<(String, String, String, String)> {
|
||||
Ok((
|
||||
"canary-notifications".to_string(),
|
||||
"Canary Development Team".to_string(),
|
||||
"0.1.0".to_string(),
|
||||
"1.2".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn notify(
|
||||
&mut self,
|
||||
app_name: String,
|
||||
replaces_id: u32,
|
||||
app_icon: String,
|
||||
summary: String,
|
||||
body: String,
|
||||
actions: Vec<String>,
|
||||
hints: HashMap<String, Value<'_>>,
|
||||
timeout: i32,
|
||||
) -> u32 {
|
||||
let timeout = match timeout {
|
||||
-1 => Some(5000), // default timeout
|
||||
0 => None,
|
||||
t => Some(t),
|
||||
};
|
||||
|
||||
let contents = Contents {
|
||||
app_name: Some(app_name).filter(|s| !s.is_empty()),
|
||||
summary,
|
||||
body: Some(body).filter(|s| !s.is_empty()),
|
||||
timeout,
|
||||
};
|
||||
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
let msg = CreatePanel {
|
||||
id,
|
||||
protocol: "tebibyte-media.desktop.notification".to_string(),
|
||||
script: self.module_path.to_owned(),
|
||||
init_msg: serde_json::to_vec(&contents).unwrap(),
|
||||
};
|
||||
|
||||
if let Some(delay_ms) = contents.timeout.clone() {
|
||||
let delay = std::time::Duration::from_millis(delay_ms as _);
|
||||
let magpie_sender = self.magpie_sender.to_owned();
|
||||
async_std::task::spawn(async move {
|
||||
async_std::task::sleep(delay).await;
|
||||
magpie_sender
|
||||
.send(MagpieServerMsg::ClosePanel(ClosePanel { id }))
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
self.magpie_sender
|
||||
.send(MagpieServerMsg::CreatePanel(msg))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
fn close_notification(&self, id: u32) {}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn notification_closed(ctx: &SignalContext<'_>, id: u32, reason: u32)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn action_invoked(
|
||||
ctx: &SignalContext<'_>,
|
||||
id: u32,
|
||||
action_key: String,
|
||||
) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let module_path = args
|
||||
.get(1)
|
||||
.expect("Please pass a path to a Canary script!")
|
||||
.to_owned()
|
||||
.into();
|
||||
|
||||
let sock_path = find_socket();
|
||||
let socket = UnixStream::connect(sock_path).await.unwrap();
|
||||
let mut magpie = MagpieClient::new(socket);
|
||||
let (magpie_sender, magpie_receiver) = unbounded();
|
||||
|
||||
let notifications = Notifications {
|
||||
magpie_sender,
|
||||
next_id: 0,
|
||||
module_path,
|
||||
};
|
||||
|
||||
let _ = ConnectionBuilder::session()
|
||||
.unwrap()
|
||||
.name("org.freedesktop.Notifications")
|
||||
.unwrap()
|
||||
.serve_at("/org/freedesktop/Notifications", notifications)
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
while let Ok(msg) = magpie_receiver.recv().await {
|
||||
magpie.send_async(&msg).await.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
pending::<()>().await;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
[package]
|
||||
name = "canary_egui_harness"
|
||||
name = "canary-sandbox"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
canary = { path = "../.." }
|
|
@ -0,0 +1,213 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary::{CursorEventKind, Panel, Runtime, Script, PX_PER_MM};
|
||||
use eframe::egui;
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let module_path = args
|
||||
.get(1)
|
||||
.expect("Please pass a path to a Canary script!")
|
||||
.to_owned();
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"Canary Sandbox",
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
||||
Box::new(App::new(&module_path))
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
struct App {
|
||||
script: Script,
|
||||
panels: Vec<PanelWindow>,
|
||||
next_idx: usize,
|
||||
last_update: Instant,
|
||||
protocol_buf: String,
|
||||
bind_message_buf: String,
|
||||
panel_bg: egui::Color32,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(module_path: &str) -> Self {
|
||||
let backend = canary::backend::make_default_backend().unwrap();
|
||||
let runtime = Runtime::new(backend).unwrap();
|
||||
let module = std::fs::read(module_path).unwrap();
|
||||
let script = runtime.load_module(&module).unwrap();
|
||||
|
||||
Self {
|
||||
script,
|
||||
panels: vec![],
|
||||
next_idx: 0,
|
||||
last_update: Instant::now(),
|
||||
protocol_buf: String::new(),
|
||||
bind_message_buf: String::new(),
|
||||
panel_bg: egui::Color32::TRANSPARENT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
ctx.request_repaint();
|
||||
|
||||
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
||||
ui.heading("New Panel");
|
||||
|
||||
ui.label("Protocol name:");
|
||||
ui.text_edit_singleline(&mut self.protocol_buf);
|
||||
|
||||
ui.label("Bind message:");
|
||||
let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor();
|
||||
ui.add(text_edit);
|
||||
|
||||
if ui.button("Bind Panel").clicked() {
|
||||
let msg = self.bind_message_buf.as_bytes().to_vec();
|
||||
let panel = self.script.create_panel(&self.protocol_buf, msg).unwrap();
|
||||
let index = self.next_idx;
|
||||
self.next_idx += 1;
|
||||
|
||||
let panel = PanelWindow {
|
||||
panel,
|
||||
index,
|
||||
msg_buf: String::new(),
|
||||
show_msg: false,
|
||||
current_size: Default::default(),
|
||||
};
|
||||
|
||||
self.panels.push(panel);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Global Settings");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Panel background color: ");
|
||||
ui.color_edit_button_srgba(&mut self.panel_bg);
|
||||
});
|
||||
});
|
||||
|
||||
let dt = self.last_update.elapsed().as_secs_f32();
|
||||
self.last_update = Instant::now();
|
||||
|
||||
for panel in self.panels.iter_mut() {
|
||||
panel.panel.update(dt);
|
||||
panel.show(self.panel_bg, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanelWindow {
|
||||
pub panel: Panel,
|
||||
pub index: usize,
|
||||
pub msg_buf: String,
|
||||
pub show_msg: bool,
|
||||
pub current_size: egui::Vec2,
|
||||
}
|
||||
|
||||
impl PanelWindow {
|
||||
pub fn show(&mut self, bg: egui::Color32, ctx: &egui::Context) {
|
||||
let frame = egui::Frame::window(&ctx.style()).fill(bg);
|
||||
let window_id = egui::Id::new(format!("panel_{}", self.index));
|
||||
egui::Window::new("Panel")
|
||||
.frame(frame)
|
||||
.id(window_id)
|
||||
.show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.checkbox(&mut self.show_msg, "Show Message Editor");
|
||||
});
|
||||
|
||||
let sense = egui::Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: true,
|
||||
};
|
||||
|
||||
let desired_size = ui.available_size();
|
||||
let response = ui.allocate_response(desired_size, sense);
|
||||
let rect = response.rect;
|
||||
|
||||
if rect.size() != self.current_size {
|
||||
let size = rect.size();
|
||||
self.current_size = size;
|
||||
|
||||
let size = canary::Vec2::new(size.x, size.y);
|
||||
self.panel.on_resize(size * PX_PER_MM);
|
||||
}
|
||||
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
let local = (hover_pos - rect.left_top()) * PX_PER_MM;
|
||||
let pos = canary::Vec2::new(local.x, local.y);
|
||||
|
||||
let kind = if response.drag_started() {
|
||||
CursorEventKind::Select
|
||||
} else if response.drag_released() {
|
||||
CursorEventKind::Deselect
|
||||
} else if response.dragged() {
|
||||
CursorEventKind::Drag
|
||||
} else {
|
||||
CursorEventKind::Hover
|
||||
};
|
||||
|
||||
self.panel.on_cursor_event(kind, pos);
|
||||
}
|
||||
|
||||
let texture = egui::TextureId::Managed(0);
|
||||
let uv = egui::pos2(0.0, 0.0);
|
||||
let mut mesh = egui::Mesh::with_texture(texture);
|
||||
|
||||
let commands = self.panel.draw();
|
||||
for command in commands.into_iter() {
|
||||
let voff = mesh.vertices.len() as u32;
|
||||
|
||||
match command {
|
||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||
for v in vertices.iter() {
|
||||
use egui::epaint::Vertex;
|
||||
let pos = v.position / PX_PER_MM;
|
||||
let pos = egui::pos2(pos.x, pos.y);
|
||||
let pos = pos + rect.left_top().to_vec2();
|
||||
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
||||
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let v = Vertex { pos, uv, color };
|
||||
mesh.vertices.push(v);
|
||||
}
|
||||
|
||||
for i in indices.iter() {
|
||||
mesh.indices.push(i + voff);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
let painter = ui.painter_at(rect);
|
||||
let shape = egui::Shape::mesh(mesh);
|
||||
painter.add(shape);
|
||||
|
||||
response
|
||||
});
|
||||
|
||||
let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index));
|
||||
egui::Window::new("Message Editor")
|
||||
.open(&mut self.show_msg)
|
||||
.id(msg_edit_id)
|
||||
.show(ctx, |ui| {
|
||||
let text_edit = egui::TextEdit::multiline(&mut self.msg_buf).code_editor();
|
||||
ui.add(text_edit);
|
||||
|
||||
if ui.button("Send Message").clicked() {
|
||||
let msg = self.msg_buf.as_bytes().to_vec();
|
||||
self.panel.on_message(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# check if we have tomcat(1)
|
||||
if ! command -v tomcat >/dev/null 2>&1; then
|
||||
printf "%s: Missing dependency: tomcat(1)\n"
|
||||
exit 69 # sysexits(3) EX_UNAVAILABLE
|
||||
fi
|
||||
|
||||
dir="$(pwd | sed 's/\//\n/g' | tail -n 1)"
|
||||
|
||||
for toml in $(find "$PWD" -name "Cargo.toml"); do
|
||||
printf "Project: %s\n" "$(tomcat package.name "$toml")"
|
||||
toml_lic="$(tomcat package.license "$toml")"
|
||||
if ! test -n "$toml_lic"; then
|
||||
printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\
|
||||
sed "s/^.\+$dir\///g")"
|
||||
continue 2
|
||||
fi
|
||||
for file in $(find "$(printf "%s\n" "$toml" |\
|
||||
sed 's/Cargo\.toml/src/g')" -name "*.rs")
|
||||
do
|
||||
info="$(head -n 2 "$file")"
|
||||
if ! [ "$toml_lic" = "$(printf "%s\n" "$info" | tail -n 1 |\
|
||||
sed -n 's/\/\/ SPDX-License-Identifier: //p')" ]
|
||||
then
|
||||
printf "%s: Missing or malformed license information\n" \
|
||||
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
|
||||
fi
|
||||
if ! test -n "$(printf "%s\n" "$info" | head -n 1 |\
|
||||
sed -n '/\/\/ Copyright (c) .\+/p')"
|
||||
then
|
||||
printf "%s: Missing or malformed copyright holder information\n" \
|
||||
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
|
||||
fi
|
||||
done
|
||||
done
|
|
@ -1,140 +0,0 @@
|
|||
use canary::{CursorEventKind, ScriptInstance};
|
||||
use eframe::egui;
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let module_path = args
|
||||
.get(1)
|
||||
.expect("Please pass a path to a Canary script!")
|
||||
.to_owned();
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
multisampling: 8,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"Canary egui Harness",
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
||||
Box::new(App::new(&module_path))
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
struct App {
|
||||
script: canary::WasmtimeScript<canary::ScriptAbiImpl>,
|
||||
panels: Vec<canary::PanelId>,
|
||||
last_update: Instant,
|
||||
bind_message_buf: String,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(module_path: &str) -> Self {
|
||||
let runtime = canary::WasmtimeRuntime::new().unwrap();
|
||||
let abi = canary::ScriptAbiImpl::default();
|
||||
let module = std::fs::read(module_path).unwrap();
|
||||
let script = runtime.load_module(abi, &module).unwrap();
|
||||
|
||||
Self {
|
||||
script,
|
||||
panels: vec![],
|
||||
last_update: Instant::now(),
|
||||
bind_message_buf: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
ctx.request_repaint();
|
||||
|
||||
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
||||
let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor();
|
||||
ui.add(text_edit);
|
||||
|
||||
if ui.button("Bind Panel").clicked() {
|
||||
let msg = self.bind_message_buf.as_bytes().to_vec();
|
||||
let panel = self.script.bind_panel(msg);
|
||||
self.panels.push(panel);
|
||||
}
|
||||
});
|
||||
|
||||
let dt = self.last_update.elapsed().as_secs_f32();
|
||||
self.last_update = Instant::now();
|
||||
|
||||
for (idx, panel) in self.panels.iter().enumerate() {
|
||||
self.script.update(*panel, dt);
|
||||
|
||||
let window_id = egui::Id::new(format!("panel_{}", idx));
|
||||
egui::Window::new("Panel").id(window_id).show(ctx, |ui| {
|
||||
let size = egui::vec2(800.0, 800.0);
|
||||
let sense = egui::Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: true,
|
||||
};
|
||||
|
||||
let (rect, response) = ui.allocate_at_least(size, sense);
|
||||
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
let local = (hover_pos - rect.left_top()) / rect.size();
|
||||
let norm = local * 2.0 - egui::vec2(1.0, 1.0);
|
||||
let x = norm.x;
|
||||
let y = -norm.y;
|
||||
let pos = canary::Vec2 { x, y };
|
||||
|
||||
let kind = if response.drag_started() {
|
||||
CursorEventKind::Select
|
||||
} else if response.drag_released() {
|
||||
CursorEventKind::Deselect
|
||||
} else if response.dragged() {
|
||||
CursorEventKind::Drag
|
||||
} else {
|
||||
CursorEventKind::Hover
|
||||
};
|
||||
|
||||
self.script.on_cursor_event(*panel, kind, pos);
|
||||
}
|
||||
|
||||
let texture = egui::TextureId::Managed(0);
|
||||
let uv = egui::pos2(0.0, 0.0);
|
||||
let mut mesh = egui::Mesh::with_texture(texture);
|
||||
|
||||
self.script.draw(*panel, |commands| {
|
||||
for command in commands.iter() {
|
||||
let voff = mesh.vertices.len() as u32;
|
||||
|
||||
match command {
|
||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||
for v in vertices.iter() {
|
||||
use egui::epaint::Vertex;
|
||||
let pos = egui::pos2(v.position.x, -v.position.y);
|
||||
let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5);
|
||||
let pos = rect.left_top() + pos * rect.size();
|
||||
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
||||
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let v = Vertex { pos, uv, color };
|
||||
mesh.vertices.push(v);
|
||||
}
|
||||
|
||||
for i in indices.iter() {
|
||||
mesh.indices.push(i + voff);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let painter = ui.painter_at(rect);
|
||||
let shape = egui::Shape::mesh(mesh);
|
||||
painter.add(shape);
|
||||
|
||||
response
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "sao-ui-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
glam = "^0.21"
|
||||
keyframe = "1"
|
||||
canary_script = { path = "../script" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
wee_alloc = "^0.4"
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
pub mod anim;
|
||||
pub mod draw;
|
||||
pub mod main_menu;
|
||||
pub mod panel;
|
||||
pub mod widgets;
|
||||
|
||||
use canary_script::*;
|
||||
use glam::Vec2;
|
||||
use widgets::Widget;
|
||||
use main_menu::MainMenuPanel;
|
||||
|
||||
export_abi!(ConfirmationDialogPanel);
|
||||
|
||||
pub const ICON_FONT: &str = "Iosevka Nerd Font";
|
||||
pub const DISPLAY_FONT: &str = "Homenaje";
|
||||
pub const CONTENT_FONT: &str = "Ubuntu";
|
||||
|
||||
pub struct ConfirmationDialogPanel {
|
||||
panel: Panel,
|
||||
dialog: widgets::dialog::Dialog,
|
||||
}
|
||||
|
||||
impl BindPanel for ConfirmationDialogPanel {
|
||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
let msg = msg.to_vec();
|
||||
let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
|
||||
|
||||
use widgets::dialog::*;
|
||||
let style = DialogStyle::default();
|
||||
let dialog = Dialog::new(style, &info);
|
||||
Box::new(Self { panel, dialog })
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelImpl for ConfirmationDialogPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
self.dialog.update(dt);
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = canary_script::draw::DrawContext::new(self.panel);
|
||||
self.dialog.draw(&ctx);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) {
|
||||
self.dialog.on_cursor_event(kind, at.into());
|
||||
}
|
||||
|
||||
fn on_message(&mut self, _msg: Message) {}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "canary_script"
|
||||
name = "canary-script"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "^1"
|
||||
canary_types = { path = "../types", features = ["glam"] }
|
||||
glam = "^0.21"
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
glam = { version = "^0.21", features = ["bytemuck"] }
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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
|
||||
|
||||
http://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.
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::*;
|
||||
|
||||
static mut PANEL_IMPLS: Vec<Box<dyn PanelImpl>> = Vec::new();
|
||||
|
||||
pub fn bind_panel(
|
||||
cb: impl Fn(Panel, Message, Message) -> Box<dyn PanelImpl>,
|
||||
panel: u32,
|
||||
protocol: u32,
|
||||
msg: u32,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let panel = Panel(panel);
|
||||
let protocol = Message(protocol);
|
||||
let msg = Message(msg);
|
||||
let panel_impl = cb(panel, protocol, msg);
|
||||
let id = PANEL_IMPLS.len() as u32;
|
||||
PANEL_IMPLS.push(panel_impl);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(panel_data: u32, dt: f32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
panel.update(dt);
|
||||
}
|
||||
|
||||
pub fn draw(panel_data: u32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
panel.draw();
|
||||
}
|
||||
|
||||
pub fn on_resize(panel_data: u32, width: f32, height: f32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
let new_size = Vec2::new(width, height);
|
||||
panel.on_resize(new_size);
|
||||
}
|
||||
|
||||
pub fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
let at = Vec2::new(x, y);
|
||||
let kind = CursorEventKind::from_u32(kind).unwrap();
|
||||
panel.on_cursor_event(kind, at);
|
||||
}
|
||||
|
||||
pub fn on_message(panel_data: u32, msg: u32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
let msg = Message(msg);
|
||||
panel.on_message(msg)
|
||||
}
|
|
@ -1,151 +1,162 @@
|
|||
use super::{Color, Panel, TextLayout};
|
||||
use bitflags::bitflags;
|
||||
use glam::Vec2;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub enum Corner {
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
TopLeft,
|
||||
use super::*;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
pub mod abi;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! export_abi {
|
||||
($bind_panel: ident) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bind_panel(panel: u32, protocol: u32, msg: u32) -> u32 {
|
||||
::canary_script::api::abi::bind_panel($bind_panel, panel, protocol, msg)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn update(panel_data: u32, dt: f32) {
|
||||
::canary_script::api::abi::update(panel_data, dt)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn draw(panel_data: u32) {
|
||||
::canary_script::api::abi::draw(panel_data)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn on_resize(panel_data: u32, width: f32, height: f32) {
|
||||
::canary_script::api::abi::on_resize(panel_data, width, height)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) {
|
||||
::canary_script::api::abi::on_cursor_event(panel_data, kind, x, y)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn on_message(panel_data: u32, msg: u32) {
|
||||
::canary_script::api::abi::on_message(panel_data, msg)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
pub struct CornerFlags: u8 {
|
||||
const TOP_RIGHT = 0x01;
|
||||
const BOTTOM_RIGHT = 0x02;
|
||||
const BOTTOM_LEFT = 0x04;
|
||||
const TOP_LEFT = 0x08;
|
||||
const TOP = 0x09;
|
||||
const RIGHT = 0x03;
|
||||
const BOTTOM = 0x06;
|
||||
const LEFT = 0x0C;
|
||||
const ALL = 0x0F;
|
||||
pub trait PanelImpl {
|
||||
fn update(&mut self, dt: f32);
|
||||
fn draw(&mut self);
|
||||
fn on_resize(&mut self, new_size: Vec2);
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2);
|
||||
fn on_message(&mut self, msg: Message);
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Panel(u32);
|
||||
|
||||
impl Panel {
|
||||
pub unsafe fn bind(id: u32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
unsafe {
|
||||
draw_indexed(
|
||||
vertices.as_ptr() as u32,
|
||||
vertices.len() as u32,
|
||||
indices.as_ptr() as u32,
|
||||
indices.len() as u32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_text_layout(&self, layout: &TextLayout, offset: Vec2, scale: f32, color: Color) {
|
||||
unsafe { draw_text_layout(layout.0, offset.x, offset.y, scale, color.0) }
|
||||
}
|
||||
|
||||
pub fn draw_triangle(&self, v1: Vec2, v2: Vec2, v3: Vec2, color: Color) {
|
||||
let vertices = [
|
||||
MeshVertex {
|
||||
position: v1,
|
||||
color,
|
||||
},
|
||||
MeshVertex {
|
||||
position: v2,
|
||||
color,
|
||||
},
|
||||
MeshVertex {
|
||||
position: v3,
|
||||
color,
|
||||
},
|
||||
];
|
||||
|
||||
let indices = [0, 1, 2];
|
||||
|
||||
self.draw_indexed(&vertices, &indices);
|
||||
}
|
||||
|
||||
pub fn send_message(&self, message: &[u8]) {
|
||||
unsafe { panel_send_message(self.0, message.as_ptr() as u32, message.len() as u32) }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Side {
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
}
|
||||
pub struct Font(u32);
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ColoredTriangle {
|
||||
pub v1: Vec2,
|
||||
pub v2: Vec2,
|
||||
pub v3: Vec2,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Rect {
|
||||
pub bl: Vec2,
|
||||
pub tr: Vec2,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn from_xy_size(xy: Vec2, size: Vec2) -> Self {
|
||||
Self {
|
||||
bl: xy,
|
||||
tr: xy + size,
|
||||
}
|
||||
impl Font {
|
||||
pub fn new(family: &str) -> Self {
|
||||
unsafe { Self(font_load(family.as_ptr() as u32, family.len() as u32)) }
|
||||
}
|
||||
|
||||
pub fn from_circle_bounds(center: Vec2, radius: f32) -> Self {
|
||||
Self {
|
||||
bl: center - radius,
|
||||
tr: center + radius,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_triangle_bounds(tri: &ColoredTriangle) -> Self {
|
||||
Self {
|
||||
bl: tri.v1.min(tri.v2).min(tri.v3),
|
||||
tr: tri.v1.max(tri.v2).max(tri.v3),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inset(&self, d: f32) -> Self {
|
||||
Self {
|
||||
bl: self.bl + d,
|
||||
tr: self.tr - d,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tl(&self) -> Vec2 {
|
||||
Vec2::new(self.bl.x, self.tr.y)
|
||||
}
|
||||
|
||||
pub fn br(&self) -> Vec2 {
|
||||
Vec2::new(self.tr.x, self.bl.y)
|
||||
}
|
||||
|
||||
pub fn offset(&self, offset: Vec2) -> Self {
|
||||
Self {
|
||||
bl: self.bl + offset,
|
||||
tr: self.tr + offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(&self, scale: f32) -> Self {
|
||||
Self {
|
||||
bl: self.bl * scale,
|
||||
tr: self.tr * scale,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.bl.cmplt(self.tr).all()
|
||||
}
|
||||
|
||||
pub fn intersects_rect(&self, other: &Self) -> bool {
|
||||
self.bl.cmple(other.tr).all() && self.tr.cmpge(other.bl).all()
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Self) -> Option<Self> {
|
||||
let clipped = Self {
|
||||
bl: self.bl.max(other.bl),
|
||||
tr: self.tr.min(other.tr),
|
||||
};
|
||||
|
||||
if clipped.is_valid() {
|
||||
Some(clipped)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_rect(&self, other: &Self) -> bool {
|
||||
self.bl.x < other.bl.x
|
||||
&& self.bl.y < other.bl.y
|
||||
&& self.tr.x > other.tr.x
|
||||
&& self.tr.y > other.tr.y
|
||||
}
|
||||
|
||||
pub fn contains_point(&self, xy: Vec2) -> bool {
|
||||
self.bl.x < xy.x && self.bl.y < xy.y && self.tr.x > xy.x && self.tr.y > xy.y
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.tr - self.bl
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f32 {
|
||||
self.tr.x - self.bl.x
|
||||
}
|
||||
|
||||
pub fn height(&self) -> f32 {
|
||||
self.tr.y - self.bl.y
|
||||
/// Retrieves the script-local identifier of this font.
|
||||
pub fn get_id(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<canary_types::Rect> for Rect {
|
||||
fn from(other: canary_types::Rect) -> Self {
|
||||
Self {
|
||||
bl: other.bl.into(),
|
||||
tr: other.tr.into(),
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug)]
|
||||
pub struct TextLayout(u32);
|
||||
|
||||
impl TextLayout {
|
||||
pub fn new(font: &Font, text: &str) -> Self {
|
||||
unsafe {
|
||||
Self(text_layout_new(
|
||||
font.0,
|
||||
text.as_ptr() as u32,
|
||||
text.len() as u32,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bounds(&self) -> Rect {
|
||||
unsafe {
|
||||
let mut bounds = Rect::default();
|
||||
let bounds_ptr: *mut Rect = &mut bounds;
|
||||
text_layout_get_bounds(self.0, bounds_ptr as u32);
|
||||
bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TextLayout {
|
||||
fn drop(&mut self) {
|
||||
unsafe { text_layout_delete(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct Message(u32);
|
||||
|
||||
impl Message {
|
||||
pub fn to_vec(self) -> Vec<u8> {
|
||||
unsafe {
|
||||
let len = message_get_len(self.0) as usize;
|
||||
let mut vec = Vec::with_capacity(len);
|
||||
vec.set_len(len);
|
||||
message_get_data(self.0, vec.as_ptr() as u32);
|
||||
vec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +165,7 @@ pub struct DrawContext {
|
|||
panel: Panel,
|
||||
offset: Option<Vec2>,
|
||||
clip_rect: Option<Rect>,
|
||||
opacity: Option<f32>,
|
||||
opacity: Option<u8>,
|
||||
}
|
||||
|
||||
impl DrawContext {
|
||||
|
@ -181,6 +192,24 @@ impl DrawContext {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
let mut vertices = vertices.to_vec();
|
||||
|
||||
if let Some(offset) = self.offset {
|
||||
for v in vertices.iter_mut() {
|
||||
v.position += offset;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(opacity) = self.opacity {
|
||||
for v in vertices.iter_mut() {
|
||||
v.color = v.color.alpha_multiply(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
self.panel.draw_indexed(&vertices, &indices);
|
||||
}
|
||||
|
||||
pub fn draw_triangle(&self, v1: Vec2, v2: Vec2, v3: Vec2, color: Color) {
|
||||
if let Some(clip_rect) = self.clip_rect.as_ref() {
|
||||
let tri = ColoredTriangle { v1, v2, v3, color };
|
||||
|
@ -202,7 +231,7 @@ impl DrawContext {
|
|||
}
|
||||
|
||||
if let Some(opacity) = self.opacity.as_ref() {
|
||||
color.a *= opacity;
|
||||
color = color.alpha_multiply(*opacity);
|
||||
}
|
||||
|
||||
self.panel
|
||||
|
@ -232,10 +261,10 @@ impl DrawContext {
|
|||
let delta = PI / 4.0 / spoke_num;
|
||||
|
||||
let (mut theta, limit) = match corner {
|
||||
Corner::TopRight => (0.0, FRAC_PI_2),
|
||||
Corner::BottomRight => (FRAC_PI_2 * 3.0, PI * 2.0),
|
||||
Corner::BottomLeft => (PI, FRAC_PI_2 * 3.0),
|
||||
Corner::TopLeft => (FRAC_PI_2, PI),
|
||||
Corner::TopRight => (FRAC_PI_2 * 3.0, PI * 2.0),
|
||||
Corner::BottomRight => (0.0, FRAC_PI_2),
|
||||
Corner::BottomLeft => (FRAC_PI_2, PI),
|
||||
Corner::TopLeft => (PI, FRAC_PI_2 * 3.0),
|
||||
};
|
||||
|
||||
let mut last_spoke = Vec2::from_angle(theta) * radius + center;
|
||||
|
@ -282,10 +311,10 @@ impl DrawContext {
|
|||
rect
|
||||
};
|
||||
|
||||
let v1 = rect.bl;
|
||||
let v2 = Vec2::new(rect.bl.x, rect.tr.y);
|
||||
let v3 = Vec2::new(rect.tr.x, rect.bl.y);
|
||||
let v4 = rect.tr;
|
||||
let v1 = rect.tl;
|
||||
let v2 = Vec2::new(rect.tl.x, rect.br.y);
|
||||
let v3 = Vec2::new(rect.br.x, rect.tl.y);
|
||||
let v4 = rect.br;
|
||||
|
||||
self.draw_triangle_noclip(v1, v2, v3, color);
|
||||
self.draw_triangle_noclip(v2, v3, v4, color);
|
||||
|
@ -310,48 +339,48 @@ impl DrawContext {
|
|||
let mut inner_rect = rect;
|
||||
let inset = rect.inset(radius);
|
||||
|
||||
if corners.intersects(CornerFlags::BOTTOM) {
|
||||
inner_rect.bl.y += radius;
|
||||
|
||||
let mut bottom_edge = Rect {
|
||||
bl: rect.bl,
|
||||
tr: Vec2::new(rect.tr.x, rect.bl.y + radius),
|
||||
};
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_LEFT) {
|
||||
bottom_edge.bl.x += radius;
|
||||
self.draw_quarter_circle(Corner::BottomLeft, inset.bl, radius, color);
|
||||
}
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_RIGHT) {
|
||||
bottom_edge.tr.x -= radius;
|
||||
self.draw_quarter_circle(Corner::BottomRight, inset.br(), radius, color);
|
||||
}
|
||||
|
||||
self.draw_rect(bottom_edge, color);
|
||||
}
|
||||
|
||||
if corners.intersects(CornerFlags::TOP) {
|
||||
inner_rect.tr.y -= radius;
|
||||
inner_rect.tl.y += radius;
|
||||
|
||||
let mut top_edge = Rect {
|
||||
bl: Vec2::new(rect.bl.x, rect.tr.y - radius),
|
||||
tr: rect.tr,
|
||||
tl: rect.tl,
|
||||
br: Vec2::new(rect.br.x, rect.tl.y + radius),
|
||||
};
|
||||
|
||||
if corners.contains(CornerFlags::TOP_LEFT) {
|
||||
top_edge.bl.x += radius;
|
||||
self.draw_quarter_circle(Corner::TopLeft, inset.tl(), radius, color);
|
||||
top_edge.tl.x += radius;
|
||||
self.draw_quarter_circle(Corner::TopLeft, inset.tl, radius, color);
|
||||
}
|
||||
|
||||
if corners.contains(CornerFlags::TOP_RIGHT) {
|
||||
top_edge.tr.x -= radius;
|
||||
self.draw_quarter_circle(Corner::TopRight, inset.tr, radius, color);
|
||||
top_edge.br.x -= radius;
|
||||
self.draw_quarter_circle(Corner::TopRight, inset.tr(), radius, color);
|
||||
}
|
||||
|
||||
self.draw_rect(top_edge, color);
|
||||
}
|
||||
|
||||
if corners.intersects(CornerFlags::BOTTOM) {
|
||||
inner_rect.br.y -= radius;
|
||||
|
||||
let mut bottom_edge = Rect {
|
||||
tl: Vec2::new(rect.tl.x, rect.br.y - radius),
|
||||
br: rect.br,
|
||||
};
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_LEFT) {
|
||||
bottom_edge.tl.x += radius;
|
||||
self.draw_quarter_circle(Corner::BottomLeft, inset.bl(), radius, color);
|
||||
}
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_RIGHT) {
|
||||
bottom_edge.br.x -= radius;
|
||||
self.draw_quarter_circle(Corner::BottomRight, inset.br, radius, color);
|
||||
}
|
||||
|
||||
self.draw_rect(bottom_edge, color);
|
||||
}
|
||||
|
||||
self.draw_rect(inner_rect, color);
|
||||
}
|
||||
|
||||
|
@ -367,7 +396,7 @@ impl DrawContext {
|
|||
}
|
||||
|
||||
if let Some(opacity) = self.opacity {
|
||||
color.a *= opacity;
|
||||
color = color.alpha_multiply(opacity);
|
||||
}
|
||||
|
||||
self.panel
|
||||
|
@ -401,9 +430,9 @@ impl DrawContext {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn with_opacity(&self, mut opacity: f32) -> Self {
|
||||
pub fn with_opacity(&self, mut opacity: u8) -> Self {
|
||||
if let Some(old) = self.opacity {
|
||||
opacity *= old;
|
||||
opacity = (((opacity as u16) * (old as u16)) >> 8) as u8;
|
||||
}
|
||||
|
||||
Self {
|
||||
|
@ -412,3 +441,18 @@ impl DrawContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn draw_indexed(vertices_ptr: u32, vertices_num: u32, indices_ptr: u32, indices_num: u32);
|
||||
fn draw_text_layout(id: u32, xoff: f32, yoff: f32, scale: f32, color: u32);
|
||||
fn font_load(family_ptr: u32, family_len: u32) -> u32;
|
||||
|
||||
fn text_layout_new(font_id: u32, text_ptr: u32, text_len: u32) -> u32;
|
||||
fn text_layout_delete(id: u32);
|
||||
fn text_layout_get_bounds(id: u32, rect_ptr: u32);
|
||||
|
||||
fn message_get_len(id: u32) -> u32;
|
||||
fn message_get_data(id: u32, ptr: u32);
|
||||
|
||||
fn panel_send_message(id: u32, message_ptr: u32, message_len: u32);
|
||||
}
|
|
@ -1,203 +1,246 @@
|
|||
pub use canary_types::*;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod draw;
|
||||
use bitflags::bitflags;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
pub use glam::Vec2;
|
||||
use num_derive::{FromPrimitive, ToPrimitive};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! export_abi {
|
||||
($panel_impl: ident) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bind_panel(panel: u32, msg: u32) -> u32 {
|
||||
::canary_script::abi::bind_panel::<$panel_impl>(panel, msg)
|
||||
}
|
||||
pub mod api;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn update(panel_data: u32, dt: f32) {
|
||||
::canary_script::abi::update(panel_data, dt)
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct Rect {
|
||||
pub tl: Vec2,
|
||||
pub br: Vec2,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn draw(panel_data: u32) {
|
||||
::canary_script::abi::draw(panel_data)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) {
|
||||
::canary_script::abi::on_cursor_event(panel_data, kind, x, y)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn on_message(panel_data: u32, msg: u32) {
|
||||
::canary_script::abi::on_message(panel_data, msg)
|
||||
}
|
||||
impl Rect {
|
||||
pub const NEG_INFINITY: Self = Self {
|
||||
tl: Vec2::splat(f32::INFINITY),
|
||||
br: Vec2::splat(f32::NEG_INFINITY),
|
||||
};
|
||||
}
|
||||
|
||||
pub mod abi {
|
||||
use super::*;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
static mut PANEL_IMPLS: Vec<Box<dyn PanelImpl>> = Vec::new();
|
||||
|
||||
pub fn bind_panel<T: BindPanel>(panel: u32, msg: u32) -> u32 {
|
||||
unsafe {
|
||||
let panel = Panel(panel);
|
||||
let msg = Message(msg);
|
||||
let panel_impl = T::bind(panel, msg);
|
||||
let id = PANEL_IMPLS.len() as u32;
|
||||
PANEL_IMPLS.push(panel_impl);
|
||||
id
|
||||
pub fn from_xy_size(xy: Vec2, size: Vec2) -> Self {
|
||||
Self {
|
||||
tl: xy,
|
||||
br: xy + size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(panel_data: u32, dt: f32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
panel.update(dt);
|
||||
pub fn from_circle_bounds(center: Vec2, radius: f32) -> Self {
|
||||
Self {
|
||||
tl: center - radius,
|
||||
br: center + radius,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(panel_data: u32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
panel.draw();
|
||||
pub fn from_triangle_bounds(tri: &ColoredTriangle) -> Self {
|
||||
Self {
|
||||
tl: tri.v1.min(tri.v2).min(tri.v3),
|
||||
br: tri.v1.max(tri.v2).max(tri.v3),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
let at = canary_types::Vec2 { x, y };
|
||||
let kind = CursorEventKind::from_u32(kind).unwrap();
|
||||
panel.on_cursor_event(kind, at);
|
||||
pub fn inset(&self, d: f32) -> Self {
|
||||
Self {
|
||||
tl: self.tl + d,
|
||||
br: self.br - d,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bl(&self) -> Vec2 {
|
||||
Vec2::new(self.tl.x, self.br.y)
|
||||
}
|
||||
|
||||
pub fn on_message(panel_data: u32, msg: u32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
let msg = Message(msg);
|
||||
panel.on_message(msg)
|
||||
pub fn tr(&self) -> Vec2 {
|
||||
Vec2::new(self.br.x, self.tl.y)
|
||||
}
|
||||
|
||||
pub fn offset(&self, offset: Vec2) -> Self {
|
||||
Self {
|
||||
tl: self.tl + offset,
|
||||
br: self.br + offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(&self, scale: f32) -> Self {
|
||||
Self {
|
||||
tl: self.tl * scale,
|
||||
br: self.br * scale,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.tl.cmplt(self.br).all()
|
||||
}
|
||||
|
||||
pub fn intersects_rect(&self, other: &Self) -> bool {
|
||||
self.tl.cmple(other.br).all() && self.br.cmpge(other.tl).all()
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Self) -> Option<Self> {
|
||||
let clipped = Self {
|
||||
tl: self.tl.max(other.tl),
|
||||
br: self.br.min(other.br),
|
||||
};
|
||||
|
||||
if clipped.is_valid() {
|
||||
Some(clipped)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(&self, other: &Self) -> Self {
|
||||
Self {
|
||||
tl: self.tl.min(other.tl),
|
||||
br: self.br.max(other.br),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union_point(&self, point: Vec2) -> Self {
|
||||
Self {
|
||||
tl: self.tl.min(point),
|
||||
br: self.br.max(point),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_rect(&self, other: &Self) -> bool {
|
||||
self.tl.x < other.tl.x
|
||||
&& self.tl.y < other.tl.y
|
||||
&& self.br.x > other.br.x
|
||||
&& self.br.y > other.br.y
|
||||
}
|
||||
|
||||
pub fn contains_point(&self, xy: Vec2) -> bool {
|
||||
self.tl.x < xy.x && self.tl.y < xy.y && self.br.x > xy.x && self.br.y > xy.y
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.br - self.tl
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f32 {
|
||||
self.br.x - self.tl.x
|
||||
}
|
||||
|
||||
pub fn height(&self) -> f32 {
|
||||
self.br.y - self.tl.y
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BindPanel {
|
||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl>;
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct Color(pub u32);
|
||||
|
||||
impl From<glam::Vec4> for Color {
|
||||
fn from(other: glam::Vec4) -> Self {
|
||||
let map = |v: f32| (v * 255.0).floor() as u8;
|
||||
Self::new(map(other.x), map(other.y), map(other.z), map(other.w))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PanelImpl {
|
||||
fn update(&mut self, dt: f32);
|
||||
fn draw(&mut self);
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2);
|
||||
fn on_message(&mut self, msg: Message);
|
||||
impl From<Color> for glam::Vec4 {
|
||||
fn from(other: Color) -> Self {
|
||||
let (r, g, b, a) = other.to_rgba_unmultiplied();
|
||||
let map = |v: u8| (v as f32) / 255.0;
|
||||
Self::new(map(r), map(g), map(b), map(a))
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub const WHITE: Self = Self(0xffffffff);
|
||||
pub const BLACK: Self = Self(0x000000ff);
|
||||
pub const TRANSPARENT: Self = Self(0);
|
||||
pub const RED: Self = Self(0xff0000ff);
|
||||
pub const GREEN: Self = Self(0x00ff00ff);
|
||||
pub const BLUE: Self = Self(0x0000ffff);
|
||||
pub const YELLOW: Self = Self(0xffff00ff);
|
||||
pub const MAGENTA: Self = Self(0xff00ffff);
|
||||
pub const CYAN: Self = Self(0x00ffffff);
|
||||
|
||||
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Color(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
|
||||
}
|
||||
|
||||
pub fn to_rgba_unmultiplied(&self) -> (u8, u8, u8, u8) {
|
||||
(
|
||||
(self.0 >> 24) as u8,
|
||||
(self.0 >> 16) as u8,
|
||||
(self.0 >> 8) as u8,
|
||||
self.0 as u8,
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn alpha_multiply(&self, mul: u8) -> Self {
|
||||
let a = self.0 as u8 as u16;
|
||||
let multiplied = ((a * (mul as u16)) >> 8) as u8;
|
||||
self.with_alpha(multiplied)
|
||||
}
|
||||
|
||||
pub const fn with_alpha(&self, alpha: u8) -> Self {
|
||||
Self(self.0 & 0xffffff00 | alpha as u32)
|
||||
}
|
||||
|
||||
pub fn lerp(self, target: Self, blend: f32) -> Self {
|
||||
let s: glam::Vec4 = self.into();
|
||||
let o: glam::Vec4 = target.into();
|
||||
(o * blend + s * (1.0 - blend)).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct MeshVertex {
|
||||
pub position: Vec2,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub type MeshIndex = u32;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
pub enum CursorEventKind {
|
||||
Hover = 0,
|
||||
Select = 1,
|
||||
Drag = 2,
|
||||
Deselect = 3,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
pub enum Corner {
|
||||
TopRight = 0,
|
||||
BottomRight = 1,
|
||||
BottomLeft = 2,
|
||||
TopLeft = 3,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
pub struct CornerFlags: u8 {
|
||||
const TOP_RIGHT = 0x01;
|
||||
const BOTTOM_RIGHT = 0x02;
|
||||
const BOTTOM_LEFT = 0x04;
|
||||
const TOP_LEFT = 0x08;
|
||||
const TOP = 0x09;
|
||||
const RIGHT = 0x03;
|
||||
const BOTTOM = 0x06;
|
||||
const LEFT = 0x0C;
|
||||
const ALL = 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Side {
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Panel(u32);
|
||||
|
||||
impl Panel {
|
||||
pub unsafe fn bind(id: u32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
unsafe {
|
||||
draw_indexed(
|
||||
vertices.as_ptr() as u32,
|
||||
vertices.len() as u32,
|
||||
indices.as_ptr() as u32,
|
||||
indices.len() as u32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_text_layout(&self, layout: &TextLayout, offset: Vec2, scale: f32, color: Color) {
|
||||
unsafe {
|
||||
draw_text_layout(
|
||||
layout.0, offset.x, offset.y, scale, color.r, color.g, color.b, color.a,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_triangle(&self, v1: Vec2, v2: Vec2, v3: Vec2, color: Color) {
|
||||
let vertices = [
|
||||
MeshVertex {
|
||||
position: v1,
|
||||
color,
|
||||
},
|
||||
MeshVertex {
|
||||
position: v2,
|
||||
color,
|
||||
},
|
||||
MeshVertex {
|
||||
position: v3,
|
||||
color,
|
||||
},
|
||||
];
|
||||
|
||||
let indices = [0, 1, 2];
|
||||
|
||||
self.draw_indexed(&vertices, &indices);
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Font(u32);
|
||||
|
||||
impl Font {
|
||||
pub fn new(family: &str) -> Self {
|
||||
unsafe { Self(font_load(family.as_ptr() as u32, family.len() as u32)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct TextLayout(u32);
|
||||
|
||||
impl TextLayout {
|
||||
pub fn new(font: &Font, text: &str) -> Self {
|
||||
unsafe {
|
||||
Self(text_layout_new(
|
||||
font.0,
|
||||
text.as_ptr() as u32,
|
||||
text.len() as u32,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bounds(&self) -> Rect {
|
||||
unsafe {
|
||||
let mut bounds = Rect::default();
|
||||
let bounds_ptr: *mut Rect = &mut bounds;
|
||||
text_layout_get_bounds(self.0, bounds_ptr as u32);
|
||||
bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TextLayout {
|
||||
fn drop(&mut self) {
|
||||
unsafe { text_layout_delete(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct Message(u32);
|
||||
|
||||
impl Message {
|
||||
pub fn to_vec(self) -> Vec<u8> {
|
||||
unsafe {
|
||||
let len = message_get_len(self.0) as usize;
|
||||
let mut vec = Vec::with_capacity(len);
|
||||
vec.set_len(len);
|
||||
message_get_data(self.0, vec.as_ptr() as u32);
|
||||
vec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn draw_indexed(vertices_ptr: u32, vertices_num: u32, indices_ptr: u32, indices_num: u32);
|
||||
fn draw_text_layout(id: u32, xoff: f32, yoff: f32, scale: f32, r: f32, g: f32, b: f32, a: f32);
|
||||
fn font_load(family_ptr: u32, family_len: u32) -> u32;
|
||||
|
||||
fn text_layout_new(font_id: u32, text_ptr: u32, text_len: u32) -> u32;
|
||||
fn text_layout_delete(id: u32);
|
||||
fn text_layout_get_bounds(id: u32, rect_ptr: u32);
|
||||
|
||||
fn message_get_len(id: u32) -> u32;
|
||||
fn message_get_data(id: u32, ptr: u32);
|
||||
pub struct ColoredTriangle {
|
||||
pub v1: Vec2,
|
||||
pub v2: Vec2,
|
||||
pub v3: Vec2,
|
||||
pub color: Color,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "canary-textwrap"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
canary-script = { path = "../script" }
|
||||
textwrap = "0.16"
|
|
@ -0,0 +1,248 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use canary_script::api::{DrawContext, Font, TextLayout};
|
||||
use canary_script::{Color, Vec2};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextCache {
|
||||
layouts: Vec<OwnedText>,
|
||||
fonts: Vec<HashMap<String, usize>>,
|
||||
}
|
||||
|
||||
impl TextCache {
|
||||
pub fn insert(&mut self, font: Font, text: &str) -> (usize, f32) {
|
||||
let font_idx = font.get_id() as usize;
|
||||
if let Some(font_cache) = self.fonts.get(font_idx) {
|
||||
if let Some(layout_idx) = font_cache.get(text) {
|
||||
return (*layout_idx, self.get(*layout_idx).width);
|
||||
}
|
||||
} else {
|
||||
let new_size = font_idx + 1;
|
||||
self.fonts.resize_with(new_size, Default::default);
|
||||
}
|
||||
|
||||
let index = self.layouts.len();
|
||||
let layout = TextLayout::new(&font, text);
|
||||
let width = layout.get_bounds().width().max(0.0);
|
||||
|
||||
self.layouts.push(OwnedText {
|
||||
font,
|
||||
layout,
|
||||
width,
|
||||
});
|
||||
|
||||
self.fonts[font_idx].insert(text.to_string(), index);
|
||||
|
||||
(index, width)
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Text<'_> {
|
||||
self.layouts[index].borrow()
|
||||
}
|
||||
}
|
||||
|
||||
/// Processed text that can be laid out.
|
||||
pub struct Content {
|
||||
pub words: Vec<Word>,
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn from_plain(cache: &mut TextCache, font: Font, text: &str) -> Self {
|
||||
use textwrap::word_splitters::{split_words, WordSplitter};
|
||||
use textwrap::WordSeparator;
|
||||
|
||||
let separator = WordSeparator::new();
|
||||
let words = separator.find_words(text);
|
||||
|
||||
// TODO: crate feature to enable hyphenation support?
|
||||
let splitter = WordSplitter::NoHyphenation;
|
||||
let split_words = split_words(words, &splitter);
|
||||
|
||||
let mut words = Vec::new();
|
||||
for split_word in split_words {
|
||||
let (word, word_width) = cache.insert(font, split_word.word);
|
||||
let (whitespace, whitespace_width) = cache.insert(font, "_");
|
||||
let (penalty, penalty_width) = cache.insert(font, split_word.penalty);
|
||||
|
||||
words.push(Word {
|
||||
word,
|
||||
word_width,
|
||||
whitespace,
|
||||
whitespace_width,
|
||||
penalty,
|
||||
penalty_width,
|
||||
});
|
||||
}
|
||||
|
||||
Self { words }
|
||||
}
|
||||
|
||||
pub fn layout(&self, cache: &TextCache, width: f32) -> Layout {
|
||||
use textwrap::wrap_algorithms::wrap_optimal_fit;
|
||||
let fragments = self.words.as_slice();
|
||||
let line_widths = &[width as f64];
|
||||
let penalties = Default::default();
|
||||
|
||||
// Should never fail with reasonable input. Check [wrap_optimal_fit] docs for more info.
|
||||
let wrapped_lines = wrap_optimal_fit(fragments, line_widths, &penalties).unwrap();
|
||||
|
||||
let mut lines = Vec::new();
|
||||
for line in wrapped_lines {
|
||||
lines.push(Line::from_word_line(cache, line));
|
||||
}
|
||||
|
||||
Layout { lines }
|
||||
}
|
||||
}
|
||||
|
||||
/// An atomic fragment of processed text that is ready to be laid out.
|
||||
///
|
||||
/// May or may not correspond to a single English "word".
|
||||
///
|
||||
/// Please see [textwrap::core::Word] and [textwrap::core::Fragment] for more information.
|
||||
#[derive(Debug)]
|
||||
pub struct Word {
|
||||
pub word: usize,
|
||||
pub word_width: f32,
|
||||
pub whitespace: usize,
|
||||
pub whitespace_width: f32,
|
||||
pub penalty: usize,
|
||||
pub penalty_width: f32,
|
||||
}
|
||||
|
||||
impl textwrap::core::Fragment for Word {
|
||||
fn width(&self) -> f64 {
|
||||
self.word_width as f64
|
||||
}
|
||||
|
||||
fn whitespace_width(&self) -> f64 {
|
||||
self.whitespace_width as f64
|
||||
}
|
||||
|
||||
fn penalty_width(&self) -> f64 {
|
||||
self.penalty_width as f64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OwnedText {
|
||||
pub font: Font,
|
||||
pub layout: TextLayout,
|
||||
pub width: f32,
|
||||
}
|
||||
|
||||
impl OwnedText {
|
||||
pub fn borrow(&self) -> Text<'_> {
|
||||
Text {
|
||||
font: self.font,
|
||||
layout: &self.layout,
|
||||
width: self.width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single piece of renderable text.
|
||||
#[derive(Debug)]
|
||||
pub struct Text<'a> {
|
||||
/// The font that this fragment has been laid out with.
|
||||
pub font: Font,
|
||||
|
||||
/// The draw-ready [TextLayout] of this fragment.
|
||||
pub layout: &'a TextLayout,
|
||||
|
||||
/// The width of this text.
|
||||
pub width: f32,
|
||||
}
|
||||
|
||||
/// A finished, wrapped text layout.
|
||||
pub struct Layout {
|
||||
pub lines: Vec<Line>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn draw(
|
||||
&self,
|
||||
cache: &TextCache,
|
||||
ctx: &DrawContext,
|
||||
scale: f32,
|
||||
line_height: f32,
|
||||
color: Color,
|
||||
) {
|
||||
let mut cursor = Vec2::ZERO;
|
||||
for line in self.lines.iter() {
|
||||
let ctx = ctx.with_offset(cursor);
|
||||
line.draw(cache, &ctx, scale, color);
|
||||
cursor.y += line_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A finished line of a layout.
|
||||
pub struct Line {
|
||||
pub fragments: Vec<Fragment>,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
pub fn from_word_line(cache: &TextCache, words: &[Word]) -> Self {
|
||||
let last_idx = words.len() - 1;
|
||||
let mut fragments = Vec::new();
|
||||
|
||||
let mut add_word = |index: usize, hidden: bool| {
|
||||
let text = cache.get(index);
|
||||
|
||||
fragments.push(Fragment {
|
||||
font: text.font,
|
||||
text: index,
|
||||
offset: Vec2::ZERO,
|
||||
advance: text.width,
|
||||
hidden,
|
||||
});
|
||||
};
|
||||
|
||||
for (idx, word) in words.iter().enumerate() {
|
||||
add_word(word.word, false);
|
||||
|
||||
if idx == last_idx {
|
||||
add_word(word.penalty, false);
|
||||
} else {
|
||||
add_word(word.whitespace, true);
|
||||
}
|
||||
}
|
||||
|
||||
Self { fragments }
|
||||
}
|
||||
|
||||
pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32, color: Color) {
|
||||
let mut cursor = Vec2::ZERO;
|
||||
for fragment in self.fragments.iter() {
|
||||
if !fragment.hidden {
|
||||
let text = cache.get(fragment.text);
|
||||
let offset = cursor + fragment.offset;
|
||||
ctx.draw_text_layout(text.layout, offset, scale, color);
|
||||
}
|
||||
|
||||
cursor.x += fragment.advance * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A finished fragment in a layout.
|
||||
pub struct Fragment {
|
||||
/// The font of this fragment.
|
||||
pub font: Font,
|
||||
|
||||
/// The index into the [TextCache] of the content of this fragment.
|
||||
pub text: usize,
|
||||
|
||||
/// The offset for drawing the text layout.
|
||||
pub offset: Vec2,
|
||||
|
||||
/// The horizontal advance to draw the next fragment.
|
||||
pub advance: f32,
|
||||
|
||||
/// Whether this fragment should be skipped while drawing.
|
||||
pub hidden: bool,
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
[package]
|
||||
name = "canary_types"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
glam = { version = "^0.21", optional = true }
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
|
@ -1,118 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
pub use num_traits;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct Vec2 {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub const INFINITY: Self = Self {
|
||||
x: f32::INFINITY,
|
||||
y: f32::INFINITY,
|
||||
};
|
||||
|
||||
pub const NEG_INFINITY: Self = Self {
|
||||
x: f32::NEG_INFINITY,
|
||||
y: f32::NEG_INFINITY,
|
||||
};
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct Color {
|
||||
pub r: f32,
|
||||
pub g: f32,
|
||||
pub b: f32,
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub const WHITE: Self = Self::new(1., 1., 1., 1.);
|
||||
pub const BLACK: Self = Self::new(0., 0., 0., 1.);
|
||||
pub const TRANSPARENT: Self = Self::new(0., 0., 0., 0.);
|
||||
pub const RED: Self = Self::new(1., 0., 0., 1.);
|
||||
pub const GREEN: Self = Self::new(0., 1., 0., 1.);
|
||||
pub const BLUE: Self = Self::new(0., 0., 1., 1.);
|
||||
pub const YELLOW: Self = Self::new(1., 1., 0., 1.);
|
||||
pub const MAGENTA: Self = Self::new(1., 0., 1., 1.);
|
||||
pub const CYAN: Self = Self::new(0., 1., 1., 1.);
|
||||
|
||||
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
|
||||
pub fn to_rgba_unmultiplied(&self) -> (u8, u8, u8, u8) {
|
||||
let map = |c: f32| (c * 255.0).floor() as u8;
|
||||
(map(self.r), map(self.g), map(self.b), map(self.a))
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct MeshVertex {
|
||||
pub position: Vec2,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub type MeshIndex = u32;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
pub enum CursorEventKind {
|
||||
Hover = 0,
|
||||
Select = 1,
|
||||
Drag = 2,
|
||||
Deselect = 3,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct Rect {
|
||||
pub bl: Vec2,
|
||||
pub tr: Vec2,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub const NEG_INFINITY: Self = Self {
|
||||
bl: Vec2::INFINITY,
|
||||
tr: Vec2::NEG_INFINITY,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "glam")]
|
||||
mod glam_interop {
|
||||
use super::*;
|
||||
|
||||
impl From<glam::Vec2> for Vec2 {
|
||||
fn from(other: glam::Vec2) -> Self {
|
||||
Self {
|
||||
x: other.x,
|
||||
y: other.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec2> for glam::Vec2 {
|
||||
fn from(other: Vec2) -> Self {
|
||||
Self::new(other.x, other.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<glam::Vec4> for Color {
|
||||
fn from(other: glam::Vec4) -> Self {
|
||||
Self::new(other.x, other.y, other.z, other.w)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for glam::Vec4 {
|
||||
fn from(other: Color) -> Self {
|
||||
let Color { r, g, b, a } = other;
|
||||
Self::new(r, g, b, a)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "canary-music-player-script"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
canary-script = { path = "../../crates/script" }
|
||||
canary-music-player = { path = "../../apps/music-player" }
|
||||
wee_alloc = "^0.4"
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
use api::*;
|
||||
use canary_script::*;
|
||||
|
||||
canary_script::export_abi!(bind_panel_impl);
|
||||
|
||||
pub fn bind_panel_impl(panel: Panel, _protocol: Message, message: Message) -> Box<dyn PanelImpl> {
|
||||
MusicPlayerPanel::bind(panel, message)
|
||||
}
|
||||
|
||||
const DISPLAY_FONT: &str = "Liberation Sans";
|
||||
|
||||
pub struct MusicPlayerPanel {
|
||||
panel: Panel,
|
||||
display_font: Font,
|
||||
label: Label,
|
||||
}
|
||||
|
||||
impl PanelImpl for MusicPlayerPanel {
|
||||
fn update(&mut self, dt: f32) {}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = DrawContext::new(self.panel);
|
||||
|
||||
let offset = Vec2 { x: 5.0, y: 12.0 };
|
||||
let size = 8.0;
|
||||
let color = Color::WHITE;
|
||||
self.label.draw(&ctx, offset, size, color);
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, new_size: Vec2) {}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {
|
||||
use canary_music_player::{serde_json, InMsg};
|
||||
let msg = msg.to_vec();
|
||||
let msg = serde_json::from_slice::<InMsg>(&msg);
|
||||
self.label.set_text(format!("{:#?}", msg));
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicPlayerPanel {
|
||||
pub fn bind(panel: Panel, _message: Message) -> Box<dyn PanelImpl> {
|
||||
let display_font = Font::new(DISPLAY_FONT);
|
||||
let label = Label::new(display_font, "Hello, world!".into(), 1.2);
|
||||
let panel = Self {
|
||||
panel,
|
||||
display_font,
|
||||
label,
|
||||
};
|
||||
Box::new(panel)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Label {
|
||||
font: Font,
|
||||
text: String,
|
||||
line_height: f32,
|
||||
layout: Option<Vec<TextLayout>>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(font: Font, text: String, line_height: f32) -> Self {
|
||||
Self {
|
||||
font,
|
||||
text,
|
||||
line_height,
|
||||
layout: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, ctx: &DrawContext, offset: Vec2, size: f32, color: Color) {
|
||||
let layout = self.layout.get_or_insert_with(|| {
|
||||
self.text
|
||||
.lines()
|
||||
.map(|line| TextLayout::new(&self.font, line))
|
||||
.collect()
|
||||
});
|
||||
|
||||
for (line, layout) in layout.iter().enumerate() {
|
||||
let offset = Vec2 {
|
||||
x: offset.x,
|
||||
y: self.line_height * size * line as f32 + offset.y,
|
||||
};
|
||||
|
||||
ctx.draw_text_layout(layout, offset.into(), size, color);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: String) {
|
||||
self.text = text;
|
||||
self.layout = None;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "canary-sao-ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
glam = "^0.21"
|
||||
keyframe = "1"
|
||||
canary-music-player = { path = "../../apps/music-player" }
|
||||
canary-notifications = { path = "../../apps/notifications" }
|
||||
canary-script = { path = "../../crates/script" }
|
||||
canary-textwrap = { path = "../../crates/textwrap" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
wee_alloc = "^0.4"
|
|
@ -1,5 +1,8 @@
|
|||
use keyframe::EasingFunction;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::Color;
|
||||
use keyframe::EasingFunction;
|
||||
|
||||
pub trait AnimationLerp<T> {
|
||||
fn lerp(&self, x: f32) -> T;
|
||||
|
@ -105,6 +108,15 @@ impl<F: EasingFunction> AnimationLerp<Color> for Animation<F, Color> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<F: EasingFunction> AnimationLerp<u8> for Animation<F, u8> {
|
||||
fn lerp(&self, x: f32) -> u8 {
|
||||
let from = self.from as f32;
|
||||
let to = self.to as f32;
|
||||
let lerp = (1.0 - x) * from + x * to;
|
||||
lerp.round() as _
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T> Animation<F, T>
|
||||
where
|
||||
F: EasingFunction,
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
pub mod anim;
|
||||
pub mod main_menu;
|
||||
pub mod music_player;
|
||||
pub mod notifications;
|
||||
pub mod style;
|
||||
pub mod widgets;
|
||||
|
||||
use api::*;
|
||||
use canary_script::*;
|
||||
use main_menu::MainMenuPanel;
|
||||
use music_player::MusicPlayerPanel;
|
||||
use notifications::NotificationPanel;
|
||||
use widgets::Widget;
|
||||
|
||||
export_abi!(bind_panel_impl);
|
||||
|
||||
fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn PanelImpl> {
|
||||
let protocol = protocol.to_vec();
|
||||
let protocol = String::from_utf8(protocol).unwrap();
|
||||
|
||||
match protocol.as_str() {
|
||||
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
|
||||
"tebibyte-media.desktop.notification" => NotificationPanel::bind(panel, msg),
|
||||
"wip-dialog" => ConfirmationDialogPanel::bind(panel, msg),
|
||||
_ => MainMenuPanel::bind(panel, msg),
|
||||
}
|
||||
}
|
||||
|
||||
pub const ICON_FONT: &str = "Iosevka Nerd Font";
|
||||
pub const DISPLAY_FONT: &str = "Homenaje";
|
||||
pub const CONTENT_FONT: &str = "Ubuntu";
|
||||
|
||||
pub struct ConfirmationDialogPanel {
|
||||
panel: Panel,
|
||||
dialog: widgets::dialog::Dialog,
|
||||
}
|
||||
|
||||
impl PanelImpl for ConfirmationDialogPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
self.dialog.update(dt);
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = DrawContext::new(self.panel);
|
||||
self.dialog.draw(&ctx);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||
self.dialog.on_cursor_event(kind, at.into());
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, size: Vec2) {
|
||||
self.dialog.resize(size);
|
||||
}
|
||||
|
||||
fn on_message(&mut self, _msg: Message) {}
|
||||
}
|
||||
|
||||
impl ConfirmationDialogPanel {
|
||||
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
// let msg = msg.to_vec();
|
||||
// let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
|
||||
|
||||
let info = DialogInfo {
|
||||
title: "Hello world!".to_string(),
|
||||
content: "Testing, testing...".to_string(),
|
||||
responses: vec![DialogResponse::Yes, DialogResponse::No],
|
||||
};
|
||||
|
||||
use widgets::dialog::*;
|
||||
let style = DialogStyle::default();
|
||||
let dialog = Dialog::new(style, &info);
|
||||
Box::new(Self { panel, dialog })
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
use crate::widgets::*;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::widgets::prelude::*;
|
||||
use crate::{DrawContext, Rect};
|
||||
|
||||
use button::{RectButton, RoundButton, RoundButtonStyle};
|
||||
use canary_script::draw::{DrawContext, Rect};
|
||||
use canary_script::{BindPanel, Color, CursorEventKind, Font, Message, Panel, PanelImpl};
|
||||
use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle};
|
||||
use glam::Vec2;
|
||||
use menu::{SlotMenu, SlotMenuEvent, TabMenu};
|
||||
use palette::Palette;
|
||||
use shell::{Offset, OffsetAlignment, Popup, Reveal};
|
||||
use text::LabelText;
|
||||
|
||||
|
@ -13,8 +16,27 @@ pub struct MainMenuPanel {
|
|||
menu: MainMenu,
|
||||
}
|
||||
|
||||
impl BindPanel for MainMenuPanel {
|
||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
impl PanelImpl for MainMenuPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
Widget::update(&mut self.menu, dt);
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = DrawContext::new(self.panel);
|
||||
Widget::draw(&mut self.menu, &ctx);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||
Widget::on_cursor_event(&mut self.menu, kind, at.into());
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, _size: Vec2) {}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {}
|
||||
}
|
||||
|
||||
impl MainMenuPanel {
|
||||
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
Box::new(Self {
|
||||
panel,
|
||||
menu: MainMenu::default(),
|
||||
|
@ -22,33 +44,17 @@ impl BindPanel for MainMenuPanel {
|
|||
}
|
||||
}
|
||||
|
||||
impl PanelImpl for MainMenuPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
Widget::update(&mut self.menu, dt);
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = canary_script::draw::DrawContext::new(self.panel);
|
||||
Widget::draw(&mut self.menu, &ctx);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) {
|
||||
Widget::on_cursor_event(&mut self.menu, kind, at.into());
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {}
|
||||
}
|
||||
|
||||
pub struct MainMenu {
|
||||
pub menu: Offset<SlotMenu<RoundButton>>,
|
||||
pub player_info: Reveal<Offset<PlayerInfo>>,
|
||||
pub inventory: Reveal<Offset<TabMenu>>,
|
||||
pub palette: Reveal<Offset<Palette>>,
|
||||
pub settings: Reveal<Offset<SettingsMenu>>,
|
||||
}
|
||||
|
||||
impl MainMenu {
|
||||
pub const POSITION_X: f32 = -0.40;
|
||||
pub const SUBMENU_SPACING: f32 = 0.1;
|
||||
pub const ANCHOR: Vec2 = Vec2::new(100.0, 100.0);
|
||||
pub const SUBMENU_SPACING: f32 = 15.0;
|
||||
}
|
||||
|
||||
impl Default for MainMenu {
|
||||
|
@ -57,11 +63,12 @@ impl Default for MainMenu {
|
|||
let icons = ["", "", "", "", "", ""];
|
||||
|
||||
let button_style = RoundButtonStyle {
|
||||
radius: 0.05,
|
||||
spacing: 0.01,
|
||||
thickness: 0.002,
|
||||
color: Color::WHITE,
|
||||
icon_color: Color::BLACK,
|
||||
radius: 7.5,
|
||||
spacing: 1.5,
|
||||
thickness: 0.4,
|
||||
body_color: THEME.palette.surface,
|
||||
ring_color: THEME.palette.surface,
|
||||
icon_color: THEME.palette.text,
|
||||
};
|
||||
|
||||
let mut buttons = Vec::new();
|
||||
|
@ -75,12 +82,13 @@ impl Default for MainMenu {
|
|||
buttons.push(button);
|
||||
}
|
||||
|
||||
let menu = SlotMenu::new(buttons, 0.18);
|
||||
let menu = Offset::new(menu, Vec2::new(Self::POSITION_X, 0.0));
|
||||
let menu = SlotMenu::new(buttons, 30.0);
|
||||
let menu = Offset::new(menu, Self::ANCHOR);
|
||||
|
||||
let submenu_spacing_left = Vec2::new(Self::POSITION_X - Self::SUBMENU_SPACING, 0.0);
|
||||
let submenu_spacing_right = Vec2::new(Self::POSITION_X + Self::SUBMENU_SPACING, 0.0);
|
||||
let reveal_slide = -0.02;
|
||||
let submenu_spacing = Vec2::new(Self::SUBMENU_SPACING, 0.0);
|
||||
let submenu_spacing_left = Self::ANCHOR - submenu_spacing;
|
||||
let submenu_spacing_right = Self::ANCHOR + submenu_spacing;
|
||||
let reveal_slide = -5.0;
|
||||
let reveal_duration = 0.1;
|
||||
|
||||
let player_info = PlayerInfo::new();
|
||||
|
@ -96,6 +104,15 @@ impl Default for MainMenu {
|
|||
let inventory = Offset::new(inventory, submenu_spacing_right);
|
||||
let inventory = Reveal::new(inventory, reveal_slide, reveal_duration);
|
||||
|
||||
let palette = Palette::new(Default::default());
|
||||
let palette = Offset::new_aligned(
|
||||
palette,
|
||||
submenu_spacing_left,
|
||||
OffsetAlignment::End,
|
||||
OffsetAlignment::Center,
|
||||
);
|
||||
let palette = Reveal::new(palette, -reveal_slide, reveal_duration);
|
||||
|
||||
let settings = SettingsMenu::new();
|
||||
let settings = Offset::new(settings, submenu_spacing_right);
|
||||
let settings = Reveal::new(settings, reveal_slide, reveal_duration);
|
||||
|
@ -104,6 +121,7 @@ impl Default for MainMenu {
|
|||
menu,
|
||||
player_info,
|
||||
inventory,
|
||||
palette,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +132,7 @@ impl Container for MainMenu {
|
|||
f(&mut self.menu);
|
||||
f(&mut self.player_info);
|
||||
f(&mut self.inventory);
|
||||
f(&mut self.palette);
|
||||
f(&mut self.settings);
|
||||
}
|
||||
|
||||
|
@ -133,8 +152,14 @@ impl Container for MainMenu {
|
|||
self.player_info.hide();
|
||||
self.inventory.hide();
|
||||
}
|
||||
SlotMenuEvent::SubmenuOpen(4) => self.settings.show(),
|
||||
SlotMenuEvent::SubmenuClose(4) => self.settings.hide(),
|
||||
SlotMenuEvent::SubmenuOpen(4) => {
|
||||
self.palette.show();
|
||||
self.settings.show();
|
||||
}
|
||||
SlotMenuEvent::SubmenuClose(4) => {
|
||||
self.palette.hide();
|
||||
self.settings.hide();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
@ -144,14 +169,16 @@ pub struct PlayerInfo {
|
|||
width: f32,
|
||||
height: f32,
|
||||
rounding: f32,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl PlayerInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
width: 0.5,
|
||||
height: 0.9,
|
||||
rounding: 0.02,
|
||||
width: 70.0,
|
||||
height: 120.0,
|
||||
rounding: 5.0,
|
||||
color: THEME.palette.surface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +191,7 @@ impl RectBounds for PlayerInfo {
|
|||
|
||||
impl Widget for PlayerInfo {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, Color::WHITE);
|
||||
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, self.color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,7 +202,7 @@ pub struct Inventory {
|
|||
|
||||
impl Inventory {
|
||||
pub fn new(available_width: f32) -> (Self, f32) {
|
||||
let height = 1.28;
|
||||
let height = 1024.0;
|
||||
|
||||
(
|
||||
Self {
|
||||
|
@ -189,8 +216,8 @@ impl Inventory {
|
|||
|
||||
impl Widget for Inventory {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let box_size = 0.06;
|
||||
let box_margin = 0.02;
|
||||
let box_size = 12.0;
|
||||
let box_margin = 4.0;
|
||||
let box_stride = box_size + box_margin;
|
||||
|
||||
let grid_width = (self.width / box_stride).floor() as usize;
|
||||
|
@ -227,7 +254,7 @@ impl SettingsMenu {
|
|||
("Log Out", ""),
|
||||
];
|
||||
|
||||
let button_size = Vec2::new(0.4, 0.1);
|
||||
let button_size = Vec2::new(90.0, 20.0);
|
||||
let button_rect = Rect::from_xy_size(Vec2::new(0.0, -button_size.y / 2.0), button_size);
|
||||
|
||||
let mut buttons = Vec::new();
|
||||
|
@ -248,7 +275,7 @@ impl SettingsMenu {
|
|||
buttons.push(button);
|
||||
}
|
||||
|
||||
let menu = SlotMenu::new(buttons, 0.12);
|
||||
let menu = SlotMenu::new(buttons, 25.0);
|
||||
|
||||
Self {
|
||||
menu,
|
|
@ -0,0 +1,430 @@
|
|||
// Copyright (c) 2022 Marceline Crmaer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use api::*;
|
||||
use canary_script::*;
|
||||
|
||||
use canary_music_player::{AlbumInfo, OutMsg, PlaybackStatus, ProgressChanged, TrackInfo};
|
||||
|
||||
use crate::widgets::prelude::*;
|
||||
use button::{RoundButton, RoundButtonStyle};
|
||||
use dialog::{DialogBodyStyle, DialogFooterStyle};
|
||||
use shell::Offset;
|
||||
use slider::Slider;
|
||||
use text::{HorizontalAlignment, Label, LabelText};
|
||||
|
||||
pub struct MusicPlayerPanel {
|
||||
panel: Panel,
|
||||
widget: Option<MusicPlayerWidget>,
|
||||
disconnected: Offset<Label>,
|
||||
}
|
||||
|
||||
impl PanelImpl for MusicPlayerPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
Widget::update(widget, dt);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = DrawContext::new(self.panel);
|
||||
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
Widget::draw(widget, &ctx);
|
||||
} else {
|
||||
self.disconnected.draw(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, new_size: Vec2) {
|
||||
self.disconnected.set_offset(new_size / 2.0);
|
||||
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
widget.resize(new_size);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
Widget::on_cursor_event(widget, kind, at);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {
|
||||
use canary_music_player::{serde_json::from_slice, InMsg};
|
||||
let msg = msg.to_vec();
|
||||
let msg: InMsg = match from_slice(&msg) {
|
||||
Ok(msg) => msg,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
use InMsg::*;
|
||||
match (self.widget.as_mut(), msg) {
|
||||
(Some(_), Disconnected) => self.widget = None,
|
||||
(None, Connected) => self.widget = Some(MusicPlayerWidget::new(self.panel)),
|
||||
(Some(widget), AlbumChanged(info)) => widget.update_album(info),
|
||||
(Some(widget), TrackChanged(info)) => widget.update_track(info),
|
||||
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
|
||||
(Some(widget), ProgressChanged(progress)) => widget.update_progress(progress),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicPlayerPanel {
|
||||
pub fn bind(panel: Panel, _msg: Message) -> Box<dyn PanelImpl> {
|
||||
let dc_text = LabelText {
|
||||
font: Font::new(crate::DISPLAY_FONT),
|
||||
text: "Disconnected".to_string(),
|
||||
};
|
||||
|
||||
let disconnected = Label::new_centered(dc_text, 10.0, Color::WHITE);
|
||||
let disconnected = Offset::new(disconnected, Vec2::ZERO);
|
||||
|
||||
Box::new(Self {
|
||||
panel,
|
||||
widget: None,
|
||||
disconnected,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MusicPlayerStyle {
|
||||
pub body: DialogBodyStyle,
|
||||
pub footer: DialogFooterStyle,
|
||||
pub rounding: f32,
|
||||
pub art_margin: f32,
|
||||
pub button_spacing: f32,
|
||||
pub slider_height: f32,
|
||||
}
|
||||
|
||||
impl Default for MusicPlayerStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
body: Default::default(),
|
||||
footer: Default::default(),
|
||||
rounding: 5.0,
|
||||
art_margin: 5.0,
|
||||
button_spacing: 15.0,
|
||||
slider_height: 7.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MusicPlayerWidget {
|
||||
panel: Panel,
|
||||
artist: Offset<Label>,
|
||||
album: Offset<Label>,
|
||||
track: Offset<Label>,
|
||||
previous: Offset<RoundButton>,
|
||||
play: Offset<RoundButton>,
|
||||
next: Offset<RoundButton>,
|
||||
position: Offset<Label>,
|
||||
duration: Offset<Label>,
|
||||
slider: Slider,
|
||||
style: MusicPlayerStyle,
|
||||
art_rect: Rect,
|
||||
body_rect: Rect,
|
||||
footer_rect: Rect,
|
||||
position_secs: f32,
|
||||
duration_secs: f32,
|
||||
position_dirty: bool,
|
||||
position_updating: bool,
|
||||
status: PlaybackStatus,
|
||||
}
|
||||
|
||||
impl Container for MusicPlayerWidget {
|
||||
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
|
||||
f(&mut self.artist);
|
||||
f(&mut self.album);
|
||||
f(&mut self.track);
|
||||
f(&mut self.previous);
|
||||
f(&mut self.play);
|
||||
f(&mut self.next);
|
||||
f(&mut self.position);
|
||||
f(&mut self.duration);
|
||||
f(&mut self.slider);
|
||||
}
|
||||
|
||||
fn update(&mut self, dt: f32) {
|
||||
let position_display = if let Some(position) = self.slider.has_update() {
|
||||
self.position_updating = true;
|
||||
Some(position * self.duration_secs)
|
||||
} else if self.position_updating {
|
||||
let position = self.slider.get_position() * self.duration_secs;
|
||||
let offset = position - self.position_secs;
|
||||
let msg = OutMsg::Seek { offset };
|
||||
self.send_message(&msg);
|
||||
self.position_secs = position;
|
||||
self.position_updating = false;
|
||||
Some(position)
|
||||
} else if let PlaybackStatus::Playing = self.status {
|
||||
self.position_secs += dt;
|
||||
Some(self.position_secs)
|
||||
} else if self.position_dirty {
|
||||
self.position_dirty = false;
|
||||
Some(self.position_secs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(position) = position_display {
|
||||
self.position_dirty = false;
|
||||
self.position.set_text(&Self::format_time(position));
|
||||
self.slider
|
||||
.set_position(self.position_secs / self.duration_secs);
|
||||
}
|
||||
|
||||
if self.previous.was_clicked() {
|
||||
self.send_message(&OutMsg::Previous);
|
||||
}
|
||||
|
||||
if self.play.was_clicked() {
|
||||
self.send_message(&OutMsg::PlayPause);
|
||||
}
|
||||
|
||||
if self.next.was_clicked() {
|
||||
self.send_message(&OutMsg::Next);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::TOP,
|
||||
self.body_rect,
|
||||
self.style.rounding,
|
||||
self.style.body.color,
|
||||
);
|
||||
|
||||
let placeholder_art_color = THEME.palette.overlay;
|
||||
ctx.draw_rounded_rect(self.art_rect, self.style.rounding, placeholder_art_color);
|
||||
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::BOTTOM,
|
||||
self.footer_rect,
|
||||
self.style.rounding,
|
||||
self.style.footer.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicPlayerWidget {
|
||||
pub fn new(panel: Panel) -> Self {
|
||||
let style = MusicPlayerStyle::default();
|
||||
let display_font = Font::new(crate::DISPLAY_FONT);
|
||||
let content_font = Font::new(crate::CONTENT_FONT);
|
||||
|
||||
let make_body_label = |content: &str| {
|
||||
let text = LabelText {
|
||||
font: display_font,
|
||||
text: content.to_string(),
|
||||
};
|
||||
|
||||
let color = style.body.text_color;
|
||||
let label = Label::new_centered(text, 10.0, color);
|
||||
Offset::new(label, Vec2::ZERO)
|
||||
};
|
||||
|
||||
let make_footer_label = |content: &str| {
|
||||
let text = LabelText {
|
||||
font: content_font,
|
||||
text: content.to_string(),
|
||||
};
|
||||
|
||||
let size = style.footer.height;
|
||||
let scale = size * 0.4;
|
||||
let baseline = size * 0.125;
|
||||
let label = Label::new(
|
||||
text,
|
||||
HorizontalAlignment::Center,
|
||||
scale,
|
||||
THEME.palette.text,
|
||||
0.0,
|
||||
0.0,
|
||||
baseline,
|
||||
);
|
||||
Offset::new(label, Vec2::ZERO)
|
||||
};
|
||||
|
||||
let icon_font = Font::new(crate::ICON_FONT);
|
||||
|
||||
let prev_text = LabelText {
|
||||
font: icon_font,
|
||||
text: "玲".to_string(),
|
||||
};
|
||||
|
||||
let play_text = LabelText {
|
||||
font: icon_font,
|
||||
text: "契".to_string(),
|
||||
};
|
||||
|
||||
let next_text = LabelText {
|
||||
font: icon_font,
|
||||
text: "怜".to_string(),
|
||||
};
|
||||
|
||||
let primary_button = RoundButtonStyle {
|
||||
radius: style.footer.height * 0.3,
|
||||
spacing: style.footer.height * 0.1,
|
||||
thickness: style.footer.height * 0.025,
|
||||
body_color: THEME.palette.yellow,
|
||||
ring_color: THEME.palette.yellow,
|
||||
icon_color: THEME.palette.black,
|
||||
};
|
||||
|
||||
let secondary_button = RoundButtonStyle {
|
||||
radius: style.footer.height * 0.25,
|
||||
spacing: style.footer.height * 0.05,
|
||||
thickness: style.footer.height * 0.025,
|
||||
body_color: style.footer.color,
|
||||
ring_color: THEME.palette.black,
|
||||
icon_color: THEME.palette.black,
|
||||
};
|
||||
|
||||
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
|
||||
let play = RoundButton::new(primary_button, Some(play_text));
|
||||
let next = RoundButton::new(secondary_button, Some(next_text));
|
||||
|
||||
let slider = Slider::new(
|
||||
Default::default(),
|
||||
Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||
);
|
||||
|
||||
Self {
|
||||
panel,
|
||||
artist: make_body_label("Artist"),
|
||||
album: make_body_label("Album"),
|
||||
track: make_body_label("Track"),
|
||||
previous: Offset::new(prev, Vec2::ZERO),
|
||||
play: Offset::new(play, Vec2::ZERO),
|
||||
next: Offset::new(next, Vec2::ZERO),
|
||||
position: make_footer_label("--:--"),
|
||||
duration: make_footer_label("--:--"),
|
||||
slider,
|
||||
style,
|
||||
art_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||
body_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||
footer_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||
position_secs: 0.0,
|
||||
duration_secs: 0.0,
|
||||
position_dirty: false,
|
||||
position_updating: false,
|
||||
status: PlaybackStatus::Paused,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message(&self, msg: &OutMsg) {
|
||||
let msg = serde_json::to_vec(msg).unwrap();
|
||||
self.panel.send_message(&msg);
|
||||
}
|
||||
|
||||
pub fn format_time(secs: f32) -> String {
|
||||
let duration = secs.floor() as usize;
|
||||
let seconds = duration % 60;
|
||||
let minutes = (duration / 60) % 60;
|
||||
let hours = (duration / 60) / 60;
|
||||
|
||||
if hours > 0 {
|
||||
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||
} else {
|
||||
format!("{:02}:{:02}", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, new_size: Vec2) {
|
||||
let style = &self.style;
|
||||
let width = new_size.x;
|
||||
let body_height = new_size.y - style.footer.height;
|
||||
let body_height = body_height.max(0.0);
|
||||
let body_size = Vec2::new(width, body_height);
|
||||
let footer_size = Vec2::new(width, style.footer.height);
|
||||
|
||||
let art_size = body_height - style.art_margin * 2.0;
|
||||
self.art_rect = Rect::from_xy_size(Vec2::splat(style.art_margin), Vec2::splat(art_size));
|
||||
|
||||
let label_x = (width + self.art_rect.br.x) / 2.0;
|
||||
let artist_baseline = body_height * 0.25;
|
||||
let album_baseline = body_height * 0.55;
|
||||
let track_baseline = body_height * 0.85;
|
||||
|
||||
let button_y = body_height + style.footer.height / 2.0;
|
||||
let previous_x = style.button_spacing * 0.5;
|
||||
let play_x = style.button_spacing * 1.5;
|
||||
let next_x = style.button_spacing * 2.5;
|
||||
let position_x = style.button_spacing * 3.5;
|
||||
let slider_left = style.button_spacing * 4.25;
|
||||
let slider_right = width - style.button_spacing * 1.5;
|
||||
let slider_top = button_y - style.slider_height / 2.0;
|
||||
let slider_bottom = button_y + style.slider_height / 2.0;
|
||||
let duration_x = width - style.button_spacing * 0.75;
|
||||
|
||||
let slider_rect = Rect {
|
||||
tl: Vec2::new(slider_left, slider_top),
|
||||
br: Vec2::new(slider_right, slider_bottom),
|
||||
};
|
||||
|
||||
self.artist.set_offset(Vec2::new(label_x, artist_baseline));
|
||||
self.album.set_offset(Vec2::new(label_x, album_baseline));
|
||||
self.track.set_offset(Vec2::new(label_x, track_baseline));
|
||||
self.position.set_offset(Vec2::new(position_x, button_y));
|
||||
self.duration.set_offset(Vec2::new(duration_x, button_y));
|
||||
|
||||
self.body_rect = Rect::from_xy_size(Vec2::ZERO, body_size);
|
||||
self.footer_rect = Rect::from_xy_size(self.body_rect.bl(), footer_size);
|
||||
|
||||
self.previous.set_offset(Vec2::new(previous_x, button_y));
|
||||
self.play.set_offset(Vec2::new(play_x, button_y));
|
||||
self.next.set_offset(Vec2::new(next_x, button_y));
|
||||
|
||||
self.slider.set_rect(slider_rect);
|
||||
}
|
||||
|
||||
pub fn update_album(&mut self, info: AlbumInfo) {
|
||||
self.album.set_text(
|
||||
info.title
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("<album here>"),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_track(&mut self, info: TrackInfo) {
|
||||
self.artist.set_text(
|
||||
info.artists
|
||||
.first()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("<artist here>"),
|
||||
);
|
||||
|
||||
self.track.set_text(
|
||||
info.title
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("<album here>"),
|
||||
);
|
||||
|
||||
if let Some(length) = info.length {
|
||||
self.duration.set_text(&Self::format_time(length));
|
||||
self.duration_secs = length;
|
||||
} else {
|
||||
self.duration.set_text("--:--");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_progress(&mut self, progress: ProgressChanged) {
|
||||
self.position_secs = progress.position;
|
||||
self.position_dirty = true;
|
||||
}
|
||||
|
||||
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
|
||||
self.status = status;
|
||||
|
||||
let icon = match status {
|
||||
PlaybackStatus::Playing => "契",
|
||||
PlaybackStatus::Paused => "",
|
||||
PlaybackStatus::Stopped => "栗",
|
||||
};
|
||||
|
||||
self.play.set_text(icon);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) 2022 Marceline Crmaer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::widgets::prelude::*;
|
||||
use api::*;
|
||||
use canary_script::*;
|
||||
use canary_textwrap::{Content, Layout, TextCache};
|
||||
|
||||
use dialog::{DialogBodyStyle, DialogHeaderStyle};
|
||||
use shell::Offset;
|
||||
use text::{Label, LabelText};
|
||||
|
||||
pub struct NotificationStyle {
|
||||
pub header: DialogHeaderStyle,
|
||||
pub body: DialogBodyStyle,
|
||||
pub rounding: f32,
|
||||
}
|
||||
|
||||
impl Default for NotificationStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
header: DialogHeaderStyle {
|
||||
height: 12.0,
|
||||
..Default::default()
|
||||
},
|
||||
body: Default::default(),
|
||||
rounding: THEME.metrics.surface_rounding,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationPanel {
|
||||
panel: Panel,
|
||||
style: NotificationStyle,
|
||||
summary: Label,
|
||||
text_cache: TextCache,
|
||||
body: Content,
|
||||
body_layout: Layout,
|
||||
header_rect: Rect,
|
||||
body_rect: Rect,
|
||||
}
|
||||
|
||||
impl PanelImpl for NotificationPanel {
|
||||
fn update(&mut self, dt: f32) {}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = DrawContext::new(self.panel);
|
||||
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::TOP,
|
||||
self.header_rect,
|
||||
self.style.rounding,
|
||||
self.style.header.color,
|
||||
);
|
||||
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::BOTTOM,
|
||||
self.body_rect,
|
||||
self.style.rounding,
|
||||
self.style.body.color,
|
||||
);
|
||||
|
||||
self.summary.draw(&ctx);
|
||||
|
||||
let ctx = ctx.with_offset(Vec2::new(5.0, 20.0));
|
||||
self.body_layout
|
||||
.draw(&self.text_cache, &ctx, 5.0, 8.0, THEME.palette.text);
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, new_size: Vec2) {
|
||||
let style = &self.style;
|
||||
let width = new_size.x;
|
||||
let body_height = new_size.y - style.header.height;
|
||||
let body_height = body_height.max(0.0);
|
||||
let header_size = Vec2::new(width, style.header.height);
|
||||
let body_size = Vec2::new(width, body_height);
|
||||
|
||||
self.header_rect = Rect::from_xy_size(Vec2::ZERO, header_size);
|
||||
self.body_rect = Rect::from_xy_size(self.header_rect.bl(), body_size);
|
||||
|
||||
let width = (new_size.x - 10.0) / 5.0;
|
||||
self.body_layout = self.body.layout(&mut self.text_cache, width);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {}
|
||||
}
|
||||
|
||||
impl NotificationPanel {
|
||||
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
let msg = msg.to_vec();
|
||||
let msg: canary_notifications::Contents = serde_json::from_slice(&msg).unwrap();
|
||||
|
||||
let style = NotificationStyle::default();
|
||||
let font = style.header.text_font;
|
||||
let text = msg.summary;
|
||||
let text = LabelText { font, text };
|
||||
let scale = style.header.height * style.header.text_scale_factor;
|
||||
let summary = Label::new(
|
||||
text,
|
||||
text::HorizontalAlignment::Left,
|
||||
scale,
|
||||
style.header.text_color,
|
||||
5.0,
|
||||
5.0,
|
||||
style.header.height * (1.0 - style.header.text_baseline),
|
||||
);
|
||||
|
||||
let font = Font::new(crate::CONTENT_FONT);
|
||||
let text = msg.body.unwrap_or(String::new());
|
||||
let mut text_cache = TextCache::default();
|
||||
let body = Content::from_plain(&mut text_cache, font, &text);
|
||||
let body_layout = body.layout(&text_cache, 0.0);
|
||||
|
||||
let header_rect = Default::default();
|
||||
let body_rect = Default::default();
|
||||
|
||||
Box::new(Self {
|
||||
style,
|
||||
panel,
|
||||
summary,
|
||||
text_cache,
|
||||
body,
|
||||
body_layout,
|
||||
header_rect,
|
||||
body_rect,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
// Copyright (c) 2022 Marceline Crmaer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary_script::Color;
|
||||
|
||||
/// A reusable set of colors. Used by default widget styles.
|
||||
pub struct Palette {
|
||||
pub base: Color,
|
||||
pub base_hover: Color,
|
||||
pub base_active: Color,
|
||||
pub surface: Color,
|
||||
pub overlay: Color,
|
||||
pub text: Color,
|
||||
pub black: Color,
|
||||
pub red: Color,
|
||||
pub green: Color,
|
||||
pub yellow: Color,
|
||||
pub blue: Color,
|
||||
pub magenta: Color,
|
||||
pub cyan: Color,
|
||||
pub white: Color,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
pub fn make_label_pairs(&self) -> Vec<(&'static str, Color)> {
|
||||
vec![
|
||||
("Base", self.base),
|
||||
("Base Hover", self.base_hover),
|
||||
("Base Active", self.base_active),
|
||||
("Surface", self.surface),
|
||||
("Overlay", self.overlay),
|
||||
("Text", self.text),
|
||||
("Black", self.black),
|
||||
("Red", self.red),
|
||||
("Green", self.green),
|
||||
("Yellow", self.yellow),
|
||||
("Blue", self.blue),
|
||||
("Magenta", self.magenta),
|
||||
("Cyan", self.cyan),
|
||||
("White", self.white),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// The common base color alpha shared between all themes.
|
||||
pub const BASE_ALPHA: u8 = 230;
|
||||
|
||||
/// The common base_hover color alpha shared between all themes.
|
||||
pub const BASE_HOVER_ALPHA: u8 = 242;
|
||||
|
||||
/// Converts 0xrrggbb hex to an opaque [Color].
|
||||
pub const fn hex(rgb: u32) -> Color {
|
||||
Color((rgb << 8) | 0xff)
|
||||
}
|
||||
|
||||
/// Sword Art Online color palette.
|
||||
pub const SAO_PALETTE: Palette = Palette {
|
||||
base: Color::WHITE.with_alpha(BASE_ALPHA),
|
||||
base_hover: Color::WHITE.with_alpha(BASE_HOVER_ALPHA),
|
||||
base_active: Color::YELLOW,
|
||||
surface: Color::WHITE,
|
||||
overlay: Color::WHITE,
|
||||
text: Color::BLACK,
|
||||
black: Color::BLACK,
|
||||
red: Color::RED,
|
||||
green: Color::GREEN,
|
||||
yellow: Color::YELLOW,
|
||||
blue: Color::BLUE,
|
||||
magenta: Color::MAGENTA,
|
||||
cyan: Color::CYAN,
|
||||
white: Color::WHITE,
|
||||
};
|
||||
|
||||
/// Rose Pine color palette.
|
||||
pub const ROSE_PINE_PALETTE: Palette = Palette {
|
||||
base: hex(0x191724).with_alpha(BASE_ALPHA),
|
||||
base_hover: hex(0x21202ee0).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
|
||||
base_active: hex(0x403d52), // Highlight Med
|
||||
surface: hex(0x1f1d2e),
|
||||
overlay: hex(0x26233a),
|
||||
text: hex(0xe0def4),
|
||||
black: hex(0x6e6a86), // Muted
|
||||
red: hex(0xeb6f92), // Love
|
||||
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
|
||||
yellow: hex(0xf6c177), // Gold
|
||||
blue: hex(0x31748f), // Pine
|
||||
magenta: hex(0xc4a7e7), // Iris
|
||||
cyan: hex(0x9ccfd8), // Foam
|
||||
white: hex(0xe0def4), // Text
|
||||
};
|
||||
|
||||
/// Rose Pine Moon color palette.
|
||||
pub const ROSE_PINE_MOON_PALETTE: Palette = Palette {
|
||||
base: hex(0x232136).with_alpha(BASE_ALPHA),
|
||||
base_hover: hex(0x2a283e).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
|
||||
base_active: hex(0x44415a), // Highlight Med
|
||||
surface: hex(0x2a273f),
|
||||
overlay: hex(0x393552),
|
||||
text: hex(0xe0def4),
|
||||
black: hex(0x6e6a86), // Muted
|
||||
red: hex(0xeb6f92), // Love
|
||||
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
|
||||
yellow: hex(0xf6c177), // Gold
|
||||
blue: hex(0x3e8fb0), // Pine
|
||||
magenta: hex(0xc4a7e7), // Iris
|
||||
cyan: hex(0x9ccfd8), // Foam
|
||||
white: hex(0xe0def4), // Text
|
||||
};
|
||||
|
||||
/// [Arctica](https://github.com/sashakoshka/arctica) indexable color theme.
|
||||
pub const ARCTICA: [Color; 24] = [
|
||||
hex(0x242933),
|
||||
hex(0x2e3440),
|
||||
hex(0x3b4252),
|
||||
hex(0x4c566a),
|
||||
hex(0xeceff4),
|
||||
hex(0xd8dee9),
|
||||
hex(0xc2c9d6),
|
||||
hex(0xaeb7c6),
|
||||
hex(0xa8555d),
|
||||
hex(0xb77763),
|
||||
hex(0xcdb179),
|
||||
hex(0x8ba277),
|
||||
hex(0x769b9b),
|
||||
hex(0x72a1ae),
|
||||
hex(0x5e81ac),
|
||||
hex(0x92738c),
|
||||
hex(0xbf616a),
|
||||
hex(0xd08770),
|
||||
hex(0xebcb8b),
|
||||
hex(0xa3be8c),
|
||||
hex(0x8fbcbb),
|
||||
hex(0x88c0d0),
|
||||
hex(0x81a1c1),
|
||||
hex(0xb48ead),
|
||||
];
|
||||
|
||||
/// [Arctica](https://github.com/sashakoshka/arctica) color palette.
|
||||
pub const ARCTICA_PALETTE: Palette = Palette {
|
||||
base: ARCTICA[0].with_alpha(BASE_ALPHA),
|
||||
base_hover: ARCTICA[1].with_alpha(BASE_HOVER_ALPHA),
|
||||
base_active: ARCTICA[13],
|
||||
surface: ARCTICA[2],
|
||||
overlay: ARCTICA[3],
|
||||
text: ARCTICA[5],
|
||||
black: ARCTICA[3],
|
||||
red: ARCTICA[8],
|
||||
green: ARCTICA[11],
|
||||
yellow: ARCTICA[10],
|
||||
blue: ARCTICA[14],
|
||||
magenta: ARCTICA[15],
|
||||
cyan: ARCTICA[13],
|
||||
white: ARCTICA[7],
|
||||
};
|
||||
|
||||
/// Common measurements for widget shapes.
|
||||
pub struct Metrics {
|
||||
pub surface_rounding: f32,
|
||||
}
|
||||
|
||||
/// Common default parameters for widget styles.
|
||||
pub struct Theme {
|
||||
pub palette: Palette,
|
||||
pub metrics: Metrics,
|
||||
}
|
||||
|
||||
/// The global theme.
|
||||
pub const THEME: Theme = Theme {
|
||||
palette: ROSE_PINE_MOON_PALETTE,
|
||||
metrics: Metrics {
|
||||
surface_rounding: 5.0,
|
||||
},
|
||||
};
|
|
@ -1,5 +1,8 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
use text::{LabelText, Label, Icon, HorizontalAlignment};
|
||||
use text::{HorizontalAlignment, Icon, Label, LabelText};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ButtonState {
|
||||
|
@ -14,7 +17,8 @@ pub struct RoundButtonStyle {
|
|||
pub radius: f32,
|
||||
pub spacing: f32,
|
||||
pub thickness: f32,
|
||||
pub color: Color,
|
||||
pub body_color: Color,
|
||||
pub ring_color: Color,
|
||||
pub icon_color: Color,
|
||||
}
|
||||
|
||||
|
@ -44,6 +48,12 @@ impl RoundButton {
|
|||
icon,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if let Some(icon) = self.icon.as_mut() {
|
||||
icon.set_text(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Button for RoundButton {
|
||||
|
@ -59,12 +69,20 @@ impl Widget for RoundButton {
|
|||
}
|
||||
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let radius = self.style.radius;
|
||||
let color = self.style.color;
|
||||
let RoundButtonStyle {
|
||||
body_color,
|
||||
ring_color,
|
||||
thickness,
|
||||
radius,
|
||||
spacing,
|
||||
..
|
||||
} = self.style;
|
||||
|
||||
let center = Vec2::ZERO;
|
||||
let spacing = self.shrink_anim.get() * self.style.spacing;
|
||||
ctx.draw_circle(center, radius, color);
|
||||
ctx.draw_ring(center, radius + spacing, self.style.thickness, color);
|
||||
let spacing = self.shrink_anim.get() * spacing;
|
||||
|
||||
ctx.draw_circle(center, radius, body_color);
|
||||
ctx.draw_ring(center, radius + spacing, thickness, ring_color);
|
||||
|
||||
self.icon.draw(ctx);
|
||||
}
|
||||
|
@ -100,6 +118,8 @@ pub struct RectButtonStyle {
|
|||
pub inactive_color: Color,
|
||||
pub hover_color: Color,
|
||||
pub selected_color: Color,
|
||||
pub icon_color: Color,
|
||||
pub label_color: Color,
|
||||
}
|
||||
|
||||
impl Default for RectButtonStyle {
|
||||
|
@ -111,9 +131,11 @@ impl Default for RectButtonStyle {
|
|||
label_baseline: 0.25,
|
||||
icon_scale_factor: 0.8,
|
||||
icon_margin_factor: 1.1,
|
||||
inactive_color: Color::new(1., 1., 1., 0.2),
|
||||
hover_color: Color::new(1., 1., 1., 0.8),
|
||||
selected_color: Color::new(1., 1., 0., 1.),
|
||||
inactive_color: THEME.palette.base,
|
||||
hover_color: THEME.palette.base_hover,
|
||||
selected_color: THEME.palette.base_active,
|
||||
icon_color: THEME.palette.black,
|
||||
label_color: THEME.palette.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +164,7 @@ impl RectButton {
|
|||
label: Option<LabelText>,
|
||||
icon: Option<LabelText>,
|
||||
) -> Self {
|
||||
let mut label_left = rect.bl.x;
|
||||
let mut label_left = rect.tl.x;
|
||||
let mut alignment = HorizontalAlignment::Center;
|
||||
|
||||
let icon = icon.map(|text| {
|
||||
|
@ -150,9 +172,9 @@ impl RectButton {
|
|||
label_left += margin;
|
||||
alignment = HorizontalAlignment::Left;
|
||||
let scale = rect.height() * style.icon_scale_factor;
|
||||
let color = Color::BLACK;
|
||||
let cx = rect.bl.x + margin / 2.0;
|
||||
let cy = rect.bl.y + rect.height() / 2.0;
|
||||
let color = style.icon_color;
|
||||
let cx = rect.tl.x + margin / 2.0;
|
||||
let cy = rect.tl.y + rect.height() / 2.0;
|
||||
let center = Vec2::new(cx, cy);
|
||||
|
||||
Icon::new(text, scale, color, center)
|
||||
|
@ -161,10 +183,10 @@ impl RectButton {
|
|||
let label = label.map(|text| {
|
||||
let scale = rect.height() * style.label_scale_factor;
|
||||
let left = label_left;
|
||||
let right = rect.tr.x;
|
||||
let baseline = rect.bl.y;
|
||||
let baseline = (rect.height() * style.label_baseline) + baseline;
|
||||
let color = Color::BLACK;
|
||||
let right = rect.br.x;
|
||||
let baseline = rect.tl.y;
|
||||
let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline;
|
||||
let color = style.label_color;
|
||||
|
||||
Label::new(text, alignment, scale, color, left, right, baseline)
|
||||
});
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
use button::{RoundButton, RoundButtonStyle};
|
||||
use serde::Deserialize;
|
||||
|
@ -20,15 +23,14 @@ impl DialogResponse {
|
|||
|
||||
pub fn get_color(&self) -> Color {
|
||||
match self {
|
||||
DialogResponse::Yes => Color::BLUE,
|
||||
DialogResponse::No => Color::RED,
|
||||
DialogResponse::Yes => THEME.palette.blue,
|
||||
DialogResponse::No => THEME.palette.red,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DialogStyle {
|
||||
pub width: f32,
|
||||
pub rounding: f32,
|
||||
pub header: DialogHeaderStyle,
|
||||
pub body: DialogBodyStyle,
|
||||
|
@ -38,8 +40,7 @@ pub struct DialogStyle {
|
|||
impl Default for DialogStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: 1.2,
|
||||
rounding: 0.02,
|
||||
rounding: THEME.metrics.surface_rounding,
|
||||
header: Default::default(),
|
||||
body: Default::default(),
|
||||
footer: Default::default(),
|
||||
|
@ -60,10 +61,10 @@ pub struct DialogHeaderStyle {
|
|||
impl Default for DialogHeaderStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::WHITE,
|
||||
height: 0.3,
|
||||
color: THEME.palette.surface,
|
||||
height: 20.0,
|
||||
text_font: Font::new(crate::DISPLAY_FONT),
|
||||
text_color: Color::BLACK,
|
||||
text_color: THEME.palette.text,
|
||||
text_scale_factor: 0.65,
|
||||
text_baseline: 0.25,
|
||||
}
|
||||
|
@ -73,20 +74,18 @@ impl Default for DialogHeaderStyle {
|
|||
#[derive(Clone)]
|
||||
pub struct DialogBodyStyle {
|
||||
pub color: Color,
|
||||
pub height: f32,
|
||||
pub text_font: Font,
|
||||
pub text_color: Color,
|
||||
pub text_scale_factor: f32,
|
||||
pub text_size: f32,
|
||||
}
|
||||
|
||||
impl Default for DialogBodyStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::new(1., 1., 1., 0.8),
|
||||
height: 0.6,
|
||||
color: THEME.palette.base,
|
||||
text_font: Font::new(crate::CONTENT_FONT),
|
||||
text_color: Color::BLACK,
|
||||
text_scale_factor: 0.15,
|
||||
text_color: THEME.palette.text,
|
||||
text_size: 5.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +95,7 @@ pub struct DialogFooterStyle {
|
|||
pub icon_font: Font,
|
||||
pub button_radius: f32,
|
||||
pub color: Color,
|
||||
pub button_fg: Color,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
|
@ -103,9 +103,10 @@ impl Default for DialogFooterStyle {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
icon_font: Font::new(crate::ICON_FONT),
|
||||
button_radius: 0.1,
|
||||
color: Color::WHITE,
|
||||
height: 0.25,
|
||||
button_radius: 7.5,
|
||||
color: THEME.palette.surface,
|
||||
button_fg: THEME.palette.white,
|
||||
height: 15.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,60 +120,25 @@ pub struct DialogInfo {
|
|||
|
||||
pub struct Dialog {
|
||||
style: DialogStyle,
|
||||
title: Label,
|
||||
content: Label,
|
||||
title: Offset<Label>,
|
||||
content: Offset<Label>,
|
||||
content_size: Vec2,
|
||||
buttons: Vec<DialogButton>,
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
pub fn new(style: DialogStyle, info: &DialogInfo) -> Self {
|
||||
let width2 = style.width / 2.0;
|
||||
|
||||
let button_y = -(style.body.height + style.footer.height) / 2.0;
|
||||
let button_spacing = style.width / info.responses.len() as f32;
|
||||
let button_spacing2 = button_spacing / 2.0;
|
||||
|
||||
let title_scale = style.header.height * style.header.text_scale_factor;
|
||||
let title_baseline =
|
||||
style.header.height * style.header.text_baseline + style.body.height / 2.0;
|
||||
let title = Label::new(
|
||||
LabelText {
|
||||
font: style.header.text_font,
|
||||
text: info.title.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
title_scale,
|
||||
style.header.text_color,
|
||||
-width2,
|
||||
width2,
|
||||
title_baseline,
|
||||
);
|
||||
|
||||
let content_scale = style.body.height * style.body.text_scale_factor;
|
||||
let content = Label::new(
|
||||
LabelText {
|
||||
font: style.body.text_font,
|
||||
text: info.content.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
content_scale,
|
||||
style.body.text_color,
|
||||
-width2,
|
||||
width2,
|
||||
0.0,
|
||||
);
|
||||
|
||||
let mut buttons = Vec::new();
|
||||
for (index, response) in info.responses.iter().enumerate() {
|
||||
let button_x = button_spacing * index as f32 + button_spacing2 - style.width / 2.0;
|
||||
|
||||
for response in info.responses.iter() {
|
||||
let color = response.get_color();
|
||||
let radius = style.footer.button_radius;
|
||||
let button_style = RoundButtonStyle {
|
||||
radius: radius * 0.8,
|
||||
spacing: radius * 0.15,
|
||||
thickness: radius * 0.05,
|
||||
color: response.get_color(),
|
||||
icon_color: Color::WHITE,
|
||||
body_color: color,
|
||||
ring_color: color,
|
||||
icon_color: style.footer.button_fg,
|
||||
};
|
||||
|
||||
let text = LabelText {
|
||||
|
@ -181,7 +147,7 @@ impl Dialog {
|
|||
};
|
||||
|
||||
let button = RoundButton::new(button_style, Some(text));
|
||||
let button = Offset::new(button, Vec2::new(button_x, button_y));
|
||||
let button = Offset::new(button, Vec2::ZERO);
|
||||
|
||||
buttons.push(DialogButton {
|
||||
response: *response,
|
||||
|
@ -189,11 +155,67 @@ impl Dialog {
|
|||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
let title_scale = style.header.height * style.header.text_scale_factor;
|
||||
let title = Label::new(
|
||||
LabelText {
|
||||
font: style.header.text_font,
|
||||
text: info.title.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
title_scale,
|
||||
style.header.text_color,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
|
||||
let content = Label::new(
|
||||
LabelText {
|
||||
font: style.body.text_font,
|
||||
text: info.content.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
style.body.text_size,
|
||||
style.body.text_color,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
|
||||
let mut dialog = Self {
|
||||
style,
|
||||
title,
|
||||
content,
|
||||
title: Offset::new(title, Vec2::ZERO),
|
||||
content: Offset::new(content, Vec2::ZERO),
|
||||
content_size: Vec2::ONE,
|
||||
buttons,
|
||||
};
|
||||
|
||||
dialog.resize(Vec2::splat(100.0));
|
||||
|
||||
dialog
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: Vec2) {
|
||||
let style = &self.style;
|
||||
let width = size.x;
|
||||
let width2 = width / 2.0;
|
||||
let body_height = size.y - style.header.height - style.footer.height;
|
||||
let body_height = body_height.max(0.0);
|
||||
let title_baseline = style.header.height * (1.0 - style.header.text_baseline);
|
||||
let content_baseline = style.header.height + body_height / 2.0;
|
||||
let button_baseline = style.header.height + body_height + style.footer.height / 2.0;
|
||||
let button_spacing = width / self.buttons.len() as f32;
|
||||
let button_spacing2 = button_spacing / 2.0;
|
||||
|
||||
self.content_size = Vec2::new(width, body_height);
|
||||
self.title.set_offset(Vec2::new(width2, title_baseline));
|
||||
self.content.set_offset(Vec2::new(width2, content_baseline));
|
||||
|
||||
for (index, button) in self.buttons.iter_mut().enumerate() {
|
||||
let button_x = button_spacing * index as f32 + button_spacing2;
|
||||
button
|
||||
.button
|
||||
.set_offset(Vec2::new(button_x, button_baseline));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,24 +232,15 @@ impl Container for Dialog {
|
|||
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let style = &self.style;
|
||||
let width = style.width;
|
||||
let width2 = width / 2.0;
|
||||
let width = self.content_size.x;
|
||||
let rounding = style.rounding;
|
||||
|
||||
let header_xy = Vec2::new(-width2, style.body.height / 2.0);
|
||||
let header_size = Vec2::new(width, style.header.height);
|
||||
let header_rect = Rect::from_xy_size(header_xy, header_size);
|
||||
let header_rect = Rect::from_xy_size(Vec2::ZERO, header_size);
|
||||
let header_corners = CornerFlags::TOP;
|
||||
|
||||
let body_tr = Vec2::new(width2, style.body.height / 2.0);
|
||||
let body_rect = Rect {
|
||||
bl: -body_tr,
|
||||
tr: body_tr,
|
||||
};
|
||||
|
||||
let footer_xy = Vec2::new(-width2, -style.body.height / 2.0 - style.footer.height);
|
||||
let body_rect = Rect::from_xy_size(header_rect.bl(), self.content_size);
|
||||
let footer_size = Vec2::new(width, style.footer.height);
|
||||
let footer_rect = Rect::from_xy_size(footer_xy, footer_size);
|
||||
let footer_rect = Rect::from_xy_size(body_rect.bl(), footer_size);
|
||||
let footer_corners = CornerFlags::BOTTOM;
|
||||
|
||||
ctx.draw_rect(body_rect, style.body.color);
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Unit {
|
||||
Fixed(f32),
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
use crate::main_menu::Inventory;
|
||||
use button::{RectButton, RectButtonStyle};
|
||||
use button::{RectButton, RectButtonStyle, RoundButton, RoundButtonStyle};
|
||||
use scroll::{ScrollBar, ScrollView};
|
||||
use shell::Offset;
|
||||
|
||||
|
@ -23,7 +26,7 @@ pub enum SlotMenuEvent {
|
|||
pub struct SlotMenuButton<T> {
|
||||
pub widget: T,
|
||||
pub slide_anim: Animation<EaseOut>,
|
||||
pub opacity_anim: Animation<EaseOut>,
|
||||
pub opacity_anim: Animation<EaseOut, u8>,
|
||||
}
|
||||
|
||||
pub struct SlotMenu<T> {
|
||||
|
@ -49,12 +52,12 @@ impl<T> SlotMenu<T> {
|
|||
let out_delay = i as f32 * inter_button_delay;
|
||||
let in_delay = max_delay - out_delay;
|
||||
|
||||
let mut slide_anim = Animation::new(EaseOut, duration, 0.25, 0.0);
|
||||
let mut slide_anim = Animation::new(EaseOut, duration, -50.0, 0.0);
|
||||
slide_anim.set_in_delay(in_delay);
|
||||
slide_anim.set_out_delay(out_delay);
|
||||
slide_anim.ease_in();
|
||||
|
||||
let mut opacity_anim = Animation::new(EaseOut, duration, 0.0, 1.0);
|
||||
let mut opacity_anim = Animation::new(EaseOut, duration, 0, 0xff);
|
||||
opacity_anim.set_in_delay(in_delay);
|
||||
opacity_anim.set_out_delay(out_delay);
|
||||
opacity_anim.ease_in();
|
||||
|
@ -105,7 +108,7 @@ impl<T> SlotMenu<T> {
|
|||
|
||||
pub fn for_buttons(&mut self, mut cb: impl FnMut(&mut SlotMenuButton<T>, usize, f32)) {
|
||||
for (i, button) in self.buttons.iter_mut().enumerate() {
|
||||
let y = -(i as f32 - self.scroll_anim.get()) * self.spacing + button.slide_anim.get();
|
||||
let y = (i as f32 - self.scroll_anim.get()) * self.spacing + button.slide_anim.get();
|
||||
cb(button, i, y);
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +178,7 @@ impl<T: Widget + Button> Widget for SlotMenu<T> {
|
|||
let ctx = ctx.with_offset(Vec2::new(0.0, y));
|
||||
|
||||
let opacity = button.opacity_anim.get();
|
||||
let ctx = if opacity != 1.0 {
|
||||
let ctx = if opacity != u8::MAX {
|
||||
ctx.with_opacity(opacity)
|
||||
} else {
|
||||
ctx
|
||||
|
@ -210,6 +213,8 @@ impl<T: Widget + Button> Widget for SlotMenu<T> {
|
|||
}
|
||||
|
||||
pub struct TabMenu {
|
||||
pop_out: Offset<RoundButton>,
|
||||
search: Offset<RoundButton>,
|
||||
tabs: Vec<RectButton>,
|
||||
view: Offset<ScrollView<Inventory>>,
|
||||
head_rect: Rect,
|
||||
|
@ -217,22 +222,35 @@ pub struct TabMenu {
|
|||
}
|
||||
|
||||
impl TabMenu {
|
||||
const HEAD_RADIUS: f32 = 0.05;
|
||||
const HEAD_HEIGHT: f32 = 0.1;
|
||||
const TAB_WIDTH: f32 = 0.1;
|
||||
const TAB_HEIGHT: f32 = 0.15;
|
||||
const HEAD_RADIUS: f32 = 5.0;
|
||||
const HEAD_HEIGHT: f32 = 15.0;
|
||||
const HEAD_COLOR: Color = THEME.palette.surface;
|
||||
const TAB_WIDTH: f32 = 15.0;
|
||||
const TAB_HEIGHT: f32 = 25.0;
|
||||
const TAB_NUM: usize = 6;
|
||||
const SEPARATOR_WIDTH: f32 = 0.02;
|
||||
const INNER_RADIUS: f32 = 0.01;
|
||||
const CONTENT_WIDTH: f32 = 0.64;
|
||||
const SEPARATOR_WIDTH: f32 = 5.0;
|
||||
const INNER_RADIUS: f32 = 5.0;
|
||||
const CONTENT_WIDTH: f32 = 100.0;
|
||||
|
||||
const HEAD_BUTTON_STYLE: RoundButtonStyle = RoundButtonStyle {
|
||||
radius: Self::HEAD_HEIGHT * 0.25,
|
||||
spacing: Self::HEAD_HEIGHT * 0.1,
|
||||
thickness: Self::HEAD_HEIGHT * 0.05,
|
||||
body_color: Self::HEAD_COLOR,
|
||||
ring_color: THEME.palette.black,
|
||||
icon_color: THEME.palette.black,
|
||||
};
|
||||
|
||||
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
|
||||
const HEAD_BUTTON_SPACING: f32 = Self::HEAD_HEIGHT;
|
||||
|
||||
pub fn new() -> Self {
|
||||
let tab_size = Vec2::new(Self::TAB_WIDTH, Self::TAB_HEIGHT);
|
||||
|
||||
let mut tabs = Vec::new();
|
||||
for i in 0..Self::TAB_NUM {
|
||||
let y = (i + 1) as f32 * Self::TAB_HEIGHT;
|
||||
let pos = Vec2::new(0.0, -y);
|
||||
let y = i as f32 * Self::TAB_HEIGHT;
|
||||
let pos = Vec2::new(0.0, y);
|
||||
|
||||
let mut style = RectButtonStyle::default();
|
||||
style.radius = Self::HEAD_RADIUS;
|
||||
|
@ -245,6 +263,32 @@ impl TabMenu {
|
|||
tabs.push(RectButton::new(style, rect, None, None));
|
||||
}
|
||||
|
||||
let icon_font = Font::new(crate::ICON_FONT);
|
||||
|
||||
let pop_out = RoundButton::new(
|
||||
Self::HEAD_BUTTON_STYLE,
|
||||
Some(text::LabelText {
|
||||
font: icon_font,
|
||||
text: "ﬕ".to_string(),
|
||||
}),
|
||||
);
|
||||
|
||||
let search = RoundButton::new(
|
||||
Self::HEAD_BUTTON_STYLE,
|
||||
Some(text::LabelText {
|
||||
font: icon_font,
|
||||
text: "".to_string(),
|
||||
}),
|
||||
);
|
||||
|
||||
let head_button_y = -Self::HEAD_HEIGHT / 2.0;
|
||||
|
||||
let pop_out_x = Self::HEAD_BUTTON_MARGIN;
|
||||
let pop_out = Offset::new(pop_out, Vec2::new(pop_out_x, head_button_y));
|
||||
|
||||
let search_x = pop_out_x + Self::HEAD_BUTTON_SPACING;
|
||||
let search = Offset::new(search, Vec2::new(search_x, head_button_y));
|
||||
|
||||
let tab_list_height = Self::TAB_NUM as f32 * Self::TAB_HEIGHT;
|
||||
|
||||
let scroll_bar = ScrollBar::new(tab_list_height, tab_list_height * 3.0, Default::default());
|
||||
|
@ -252,8 +296,8 @@ impl TabMenu {
|
|||
let scroll_bar = Offset::new(scroll_bar, Vec2::new(scroll_x, -tab_list_height));
|
||||
|
||||
let separator_rect = Rect {
|
||||
bl: Vec2::new(Self::TAB_WIDTH, -tab_list_height),
|
||||
tr: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
|
||||
tl: Vec2::new(Self::TAB_WIDTH, 0.0),
|
||||
br: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, tab_list_height),
|
||||
};
|
||||
|
||||
let head_width = Self::TAB_WIDTH
|
||||
|
@ -263,13 +307,13 @@ impl TabMenu {
|
|||
+ scroll_bar.style.margin.x * 2.0;
|
||||
|
||||
let head_rect = Rect {
|
||||
bl: Vec2::ZERO,
|
||||
tr: Vec2::new(head_width, Self::HEAD_HEIGHT),
|
||||
tl: Vec2::new(0.0, -Self::HEAD_HEIGHT),
|
||||
br: Vec2::new(head_width, 0.0),
|
||||
};
|
||||
|
||||
let view_rect = Rect {
|
||||
bl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, -tab_list_height),
|
||||
tr: Vec2::new(head_rect.tr.x, 0.0),
|
||||
tl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
|
||||
br: Vec2::new(head_rect.br.x, tab_list_height),
|
||||
};
|
||||
|
||||
let view = ScrollView::new(
|
||||
|
@ -279,9 +323,11 @@ impl TabMenu {
|
|||
|available_width: f32| Inventory::new(available_width),
|
||||
);
|
||||
|
||||
let view = Offset::new(view, view_rect.bl);
|
||||
let view = Offset::new(view, view_rect.tl);
|
||||
|
||||
Self {
|
||||
pop_out,
|
||||
search,
|
||||
tabs,
|
||||
view,
|
||||
separator_rect,
|
||||
|
@ -292,6 +338,9 @@ impl TabMenu {
|
|||
|
||||
impl Container for TabMenu {
|
||||
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
|
||||
f(&mut self.pop_out);
|
||||
f(&mut self.search);
|
||||
|
||||
for tab in self.tabs.iter_mut() {
|
||||
f(tab);
|
||||
}
|
||||
|
@ -300,25 +349,18 @@ impl Container for TabMenu {
|
|||
}
|
||||
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let head_color = Color {
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 1.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::BOTTOM_RIGHT,
|
||||
self.separator_rect,
|
||||
Self::INNER_RADIUS,
|
||||
head_color,
|
||||
Self::HEAD_COLOR,
|
||||
);
|
||||
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::TOP_LEFT | CornerFlags::TOP_RIGHT,
|
||||
CornerFlags::TOP,
|
||||
self.head_rect,
|
||||
Self::HEAD_RADIUS,
|
||||
head_color,
|
||||
Self::HEAD_COLOR,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{CursorEventKind, Vec2};
|
||||
use canary_script::draw::{DrawContext, Rect};
|
||||
use canary_script::{api::DrawContext, Rect};
|
||||
|
||||
pub mod button;
|
||||
pub mod dialog;
|
||||
pub mod flex;
|
||||
pub mod menu;
|
||||
pub mod palette;
|
||||
pub mod scroll;
|
||||
pub mod slider;
|
||||
pub mod shell;
|
||||
pub mod text;
|
||||
|
||||
|
@ -71,8 +76,8 @@ impl<T: Container> Widget for T {
|
|||
|
||||
pub mod prelude {
|
||||
pub use super::*;
|
||||
pub use canary_script::{CursorEventKind, Color, Font, TextLayout};
|
||||
pub use canary_script::draw::{CornerFlags, Rect, DrawContext};
|
||||
pub use crate::anim::Animation;
|
||||
pub use crate::style::{self, THEME};
|
||||
pub use canary_script::{*, api::*};
|
||||
pub use keyframe::functions::*;
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright (c) 2022 Marceline Crmaer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
use shell::Offset;
|
||||
use text::{HorizontalAlignment, Label, LabelText};
|
||||
|
||||
pub struct PaletteStyle {
|
||||
pub bg: Color,
|
||||
pub text: Color,
|
||||
pub rounding: f32,
|
||||
pub text_size: f32,
|
||||
pub line_spacing: f32,
|
||||
pub color_radius: f32,
|
||||
pub margin: Rect,
|
||||
}
|
||||
|
||||
impl Default for PaletteStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bg: THEME.palette.surface,
|
||||
text: THEME.palette.text,
|
||||
rounding: THEME.metrics.surface_rounding,
|
||||
text_size: 5.0,
|
||||
line_spacing: 8.0,
|
||||
color_radius: 3.0,
|
||||
margin: Rect::from_xy_size(Vec2::splat(10.0), Vec2::ZERO),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that displays all the colors in the global palette.
|
||||
pub struct Palette {
|
||||
body: Rect,
|
||||
style: PaletteStyle,
|
||||
labels: Vec<Offset<Label>>,
|
||||
colors: Vec<(Vec2, Color)>,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
pub fn new(style: PaletteStyle) -> Self {
|
||||
let width = 70.0;
|
||||
let pairs = THEME.palette.make_label_pairs();
|
||||
let label_font = Font::new(crate::CONTENT_FONT);
|
||||
|
||||
let mut label_cursor = Vec2::new(0.0, style.line_spacing) + style.margin.tl;
|
||||
let mut color_cursor = Vec2::new(
|
||||
width - style.margin.br.x,
|
||||
style.line_spacing / 2.0 + style.margin.tl.y,
|
||||
);
|
||||
let mut labels = Vec::new();
|
||||
let mut colors = Vec::new();
|
||||
|
||||
for (text, color) in pairs {
|
||||
let text = LabelText {
|
||||
font: label_font,
|
||||
text: text.to_string(),
|
||||
};
|
||||
|
||||
let label = Label::new(
|
||||
text,
|
||||
HorizontalAlignment::Left,
|
||||
style.text_size,
|
||||
style.text,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
|
||||
let label = Offset::new(label, label_cursor);
|
||||
|
||||
labels.push(label);
|
||||
|
||||
colors.push((color_cursor, color));
|
||||
|
||||
label_cursor.y += style.line_spacing;
|
||||
color_cursor.y += style.line_spacing;
|
||||
}
|
||||
|
||||
let height = label_cursor.y + style.margin.br.y;
|
||||
|
||||
Self {
|
||||
body: Rect::from_xy_size(Vec2::ZERO, Vec2::new(width, height)),
|
||||
style,
|
||||
labels,
|
||||
colors,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RectBounds for Palette {
|
||||
fn get_bounds(&self) -> Rect {
|
||||
self.body
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Palette {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
ctx.draw_rounded_rect(self.body, self.style.rounding, self.style.bg);
|
||||
|
||||
for label in self.labels.iter_mut() {
|
||||
label.draw(ctx);
|
||||
}
|
||||
|
||||
for (center, color) in self.colors.iter() {
|
||||
ctx.draw_circle(*center, self.style.color_radius, *color);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
use shell::Offset;
|
||||
|
||||
|
@ -15,14 +18,14 @@ pub struct ScrollBarStyle {
|
|||
impl Default for ScrollBarStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
margin: Vec2::new(0.01, 0.01),
|
||||
body_radius: 0.005,
|
||||
body_width: 0.015,
|
||||
body_idle_color: Color::new(0.5, 0.5, 0.5, 1.0),
|
||||
body_hover_color: Color::new(0.8, 0.8, 0.8, 1.0),
|
||||
body_selected_color: Color::new(1.0, 1.0, 0.0, 1.0),
|
||||
rail_width: 0.005,
|
||||
rail_color: Color::new(0.7, 0.7, 0.7, 0.5),
|
||||
margin: Vec2::splat(2.0),
|
||||
body_radius: 1.0,
|
||||
body_width: 3.0,
|
||||
body_idle_color: THEME.palette.base,
|
||||
body_hover_color: THEME.palette.base_hover,
|
||||
body_selected_color: THEME.palette.base_active,
|
||||
rail_width: 1.0,
|
||||
rail_color: THEME.palette.base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,8 +50,8 @@ impl ScrollBar {
|
|||
pub fn new(height: f32, content_height: f32, style: ScrollBarStyle) -> Self {
|
||||
let center_x = style.body_width / 2.0 + style.margin.x;
|
||||
let rail_rect = Rect {
|
||||
bl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y),
|
||||
tr: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y),
|
||||
tl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y),
|
||||
br: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y),
|
||||
};
|
||||
|
||||
let body_color_anim = Animation::new(
|
||||
|
@ -77,7 +80,7 @@ impl ScrollBar {
|
|||
let style = &self.style;
|
||||
let rail_height = self.rail_rect.height();
|
||||
let body_height = (self.height / self.content_height) * rail_height;
|
||||
let body_y = rail_height - (self.scroll / self.content_height) * rail_height - body_height;
|
||||
let body_y = (self.scroll / self.content_height) * rail_height;
|
||||
let body_xy = Vec2::new(style.margin.x, body_y + style.margin.y);
|
||||
let body_size = Vec2::new(style.body_width, body_height);
|
||||
Rect::from_xy_size(body_xy, body_size)
|
||||
|
@ -125,7 +128,7 @@ impl Widget for ScrollBar {
|
|||
}
|
||||
|
||||
if kind == CursorEventKind::Drag && self.is_selected {
|
||||
self.scroll = ((self.grab_coord - at.y) / self.rail_rect.height())
|
||||
self.scroll = ((at.y - self.grab_coord) / self.rail_rect.height())
|
||||
* self.content_height
|
||||
+ self.grab_scroll;
|
||||
|
||||
|
@ -200,7 +203,7 @@ impl<T: Widget> ScrollView<T> {
|
|||
impl<T: Widget> Widget for ScrollView<T> {
|
||||
fn update(&mut self, dt: f32) {
|
||||
if self.scroll_bar.is_dirty() {
|
||||
let yoff = self.scroll_bar.get_scroll() - self.content_height + self.height;
|
||||
let yoff = -self.scroll_bar.get_scroll();
|
||||
self.inner.set_offset(Vec2::new(0.0, yoff));
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
|
@ -22,7 +25,7 @@ macro_rules! impl_shell_inner {
|
|||
pub struct Reveal<T> {
|
||||
inner: T,
|
||||
slide_anim: Animation<EaseIn>,
|
||||
opacity_anim: Animation<Linear>,
|
||||
opacity_anim: Animation<Linear, u8>,
|
||||
state: bool,
|
||||
}
|
||||
|
||||
|
@ -33,7 +36,7 @@ impl<T: Widget> Reveal<T> {
|
|||
Self {
|
||||
inner,
|
||||
slide_anim: Animation::new(EaseIn, duration, slide, 0.0),
|
||||
opacity_anim: Animation::new(Linear, duration, 0.0, 1.0),
|
||||
opacity_anim: Animation::new(Linear, duration, 0, 0xff),
|
||||
state: false,
|
||||
}
|
||||
}
|
||||
|
@ -129,8 +132,8 @@ impl<T: RectBounds> Offset<T> {
|
|||
vert_align: OffsetAlignment,
|
||||
) -> Self {
|
||||
let bounds = inner.get_bounds();
|
||||
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
|
||||
let y = vert_align.align(bounds.tr.y, bounds.bl.y);
|
||||
let x = hori_align.align(bounds.tl.x, bounds.br.x);
|
||||
let y = vert_align.align(bounds.br.y, bounds.tl.y);
|
||||
let offset = anchor - Vec2::new(x, y);
|
||||
Self { inner, offset }
|
||||
}
|
||||
|
@ -177,8 +180,8 @@ impl<T: RectBounds> Popup<T> {
|
|||
vert_align: OffsetAlignment,
|
||||
) -> Self {
|
||||
let bounds = inner.get_bounds();
|
||||
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
|
||||
let y = vert_align.align(bounds.tr.y, bounds.bl.y);
|
||||
let x = hori_align.align(bounds.tl.x, bounds.br.x);
|
||||
let y = vert_align.align(bounds.br.y, bounds.tl.y);
|
||||
let offset = anchor - Vec2::new(x, y);
|
||||
Self { inner, offset }
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (c) 2022 Marceline Crmaer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
|
||||
pub struct SliderStyle {
|
||||
pub bg_color: Color,
|
||||
pub bg_padding: f32,
|
||||
pub bg_rounding: f32,
|
||||
pub fg_color: Color,
|
||||
}
|
||||
|
||||
impl Default for SliderStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bg_color: THEME.palette.overlay,
|
||||
bg_padding: 2.5,
|
||||
bg_rounding: 2.5,
|
||||
fg_color: THEME.palette.blue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Slider {
|
||||
style: SliderStyle,
|
||||
bg_rect: Rect,
|
||||
fg_rect: Rect,
|
||||
position: f32,
|
||||
dirty: bool,
|
||||
updating: bool,
|
||||
}
|
||||
|
||||
impl Slider {
|
||||
pub fn new(style: SliderStyle, rect: Rect) -> Self {
|
||||
Self {
|
||||
style,
|
||||
bg_rect: rect,
|
||||
fg_rect: rect,
|
||||
position: 0.5,
|
||||
dirty: true,
|
||||
updating: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, position: f32) {
|
||||
if !self.updating {
|
||||
self.position = position;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_update(&mut self) -> Option<f32> {
|
||||
if self.updating {
|
||||
Some(self.position)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_rect(&mut self, rect: Rect) {
|
||||
self.bg_rect = rect;
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
fn undirty(&mut self) {
|
||||
if !self.dirty {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut fg_space = self.bg_rect.inset(self.style.bg_padding);
|
||||
fg_space.br.x = (fg_space.width() * self.position) + fg_space.tl.x;
|
||||
self.fg_rect = fg_space;
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> f32 {
|
||||
self.position
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Slider {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
self.undirty();
|
||||
ctx.draw_rounded_rect(self.bg_rect, self.style.bg_rounding, self.style.bg_color);
|
||||
ctx.draw_rect(self.fg_rect, self.style.fg_color);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||
if let CursorEventKind::Select = kind {
|
||||
if self.bg_rect.contains_point(at) {
|
||||
self.updating = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.updating {
|
||||
return;
|
||||
}
|
||||
|
||||
match kind {
|
||||
CursorEventKind::Hover => {}
|
||||
CursorEventKind::Select | CursorEventKind::Drag => {
|
||||
let offset = at.x - self.fg_rect.tl.x;
|
||||
let range = self.bg_rect.inset(self.style.bg_padding).width();
|
||||
self.position = (offset / range).clamp(0.0, 1.0);
|
||||
self.dirty = true;
|
||||
}
|
||||
CursorEventKind::Deselect => {
|
||||
self.updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::prelude::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
|
@ -50,6 +53,26 @@ impl Label {
|
|||
offset: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_centered(text: LabelText, scale: f32, color: Color) -> Self {
|
||||
Self::new(
|
||||
text,
|
||||
HorizontalAlignment::Center,
|
||||
scale,
|
||||
color,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if self.text.text != text {
|
||||
self.text.text = text.to_string();
|
||||
self.layout = None;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Label {
|
||||
|
@ -59,14 +82,14 @@ impl Widget for Label {
|
|||
let bounds = Rect::from(layout.get_bounds()).scale(self.scale);
|
||||
self.bounds = bounds;
|
||||
let xoff = match self.alignment {
|
||||
HorizontalAlignment::Left => self.left - bounds.bl.x,
|
||||
HorizontalAlignment::Right => self.right - bounds.tr.x,
|
||||
HorizontalAlignment::Left => self.left - bounds.tl.x,
|
||||
HorizontalAlignment::Right => self.right - bounds.br.x,
|
||||
HorizontalAlignment::Center => {
|
||||
let available = self.right - self.left;
|
||||
let halfway = available / 2.0 + self.left;
|
||||
let width = bounds.tr.x - bounds.bl.x;
|
||||
let width = bounds.br.x - bounds.tl.x;
|
||||
let left = halfway - width / 2.0;
|
||||
left - bounds.bl.x
|
||||
left - bounds.tl.x
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -105,6 +128,14 @@ impl Icon {
|
|||
offset: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if self.text.text != text {
|
||||
self.text.text = text.to_string();
|
||||
self.layout = None;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Icon {
|
||||
|
@ -114,7 +145,7 @@ impl Widget for Icon {
|
|||
let bounds = Rect::from(layout.get_bounds()).scale(self.scale);
|
||||
self.bounds = bounds;
|
||||
self.offset = self.center - bounds.size() / 2.0;
|
||||
self.offset.y -= bounds.bl.y;
|
||||
self.offset.y -= bounds.tl.y;
|
||||
self.dirty = false;
|
||||
self.layout = Some(layout);
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
//! This module defines backends for WebAssembly execution.
|
||||
//!
|
||||
//! Canary is designed to support multiple WebAssembly runtimes for different
|
||||
//! purposes. Currently, [wasmtime](https://wasmtime.dev) is the only one
|
||||
//! implemented, but in the future, [wasm3](https://github.com/wasm3/wasm3)
|
||||
//! will also be provided.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub mod wasmtime;
|
||||
|
||||
/// Creates the default WebAssembly backend.
|
||||
///
|
||||
/// Currently, only ever creates [wasmtime::WasmtimeBackend].
|
||||
pub fn make_default_backend() -> anyhow::Result<Box<dyn Backend>> {
|
||||
let backend = wasmtime::WasmtimeBackend::new()?;
|
||||
log::info!("Created default ({}) backend", backend.name());
|
||||
Ok(Box::new(backend))
|
||||
}
|
||||
|
||||
/// A WebAssembly runtime backend.
|
||||
pub trait Backend {
|
||||
fn name(&self) -> &'static str;
|
||||
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>>;
|
||||
}
|
||||
|
||||
/// An instance of a WebAssembly module.
|
||||
///
|
||||
/// All self parameters to this trait's functions are immutable, so the
|
||||
/// implementation must provide interior mutability. This allows instances
|
||||
/// to intelligently optimize the execution of their scripts, for example, by
|
||||
/// allowing the execution of multiple ABI calls from multiple threads when
|
||||
/// a script supports the WebAssembly multithreading extension.
|
||||
pub trait Instance {
|
||||
/// Binds script data to a Canary panel.
|
||||
///
|
||||
/// To "bind" a Canary panel to a Canary script, this function must be
|
||||
/// called. It passes the ID of a panel to the script, the name of the
|
||||
/// protocol that this panel will be using, plus an initialization
|
||||
/// message, and the script returns an integer as userdata. All panel
|
||||
/// events will be identified to the script with this userdata as the first
|
||||
/// argument.
|
||||
///
|
||||
/// The intended usecase for this userdata is to contain a pointer. A
|
||||
/// Canary script can allocate some high-level object in memory, and when
|
||||
/// a panel is bound, the script will return a pointer to that object as the
|
||||
/// userdata. Then, when the runtime calls back into the script, the
|
||||
/// userdata will be reinterpreted as a pointer and a method can be called
|
||||
/// on that object in memory.
|
||||
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32;
|
||||
|
||||
fn update(&self, panel_ud: u32, dt: f32);
|
||||
|
||||
fn draw(&self, panel_ud: u32) -> Vec<DrawCommand>;
|
||||
|
||||
fn on_resize(&self, panel_ud: u32, new_size: Vec2);
|
||||
|
||||
fn on_cursor_event(&self, panel_ud: u32, kind: CursorEventKind, at: Vec2);
|
||||
|
||||
fn on_message(&self, panel_ud: u32, msg: Vec<u8>);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScriptAbi {
|
||||
draw_cmds: Mutex<Vec<DrawCommand>>,
|
||||
font_store: Arc<text::FontStore>,
|
||||
font_families: Mutex<HashMap<String, u32>>,
|
||||
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
|
||||
text_layouts: RwLock<Slab<text::TextLayout>>,
|
||||
message_store: RwLock<Slab<Vec<u8>>>,
|
||||
panels: RwLock<Slab<PanelAbi>>,
|
||||
}
|
||||
|
||||
impl ScriptAbi {
|
||||
pub fn new(font_store: Arc<text::FontStore>) -> Self {
|
||||
Self {
|
||||
font_store,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates a new ID and host-side storage for a panel.
|
||||
pub fn create_panel(&self) -> PanelId {
|
||||
let abi = PanelAbi::default();
|
||||
let id = self.panels.write().insert(abi);
|
||||
PanelId(id)
|
||||
}
|
||||
|
||||
pub fn start_draw(&self) {
|
||||
let mut lock = self.draw_cmds.lock();
|
||||
lock.clear();
|
||||
}
|
||||
|
||||
pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
self.draw_cmds.lock().push(DrawCommand::Mesh {
|
||||
vertices: vertices.to_vec(),
|
||||
indices: indices.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color) {
|
||||
// TODO multiple fonts per layout
|
||||
let layouts = self.text_layouts.read();
|
||||
let layout = layouts.get(id as usize).unwrap();
|
||||
let glyphs = layout.glyphs.as_slice();
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(layout.font_id as usize).unwrap();
|
||||
let cmds = font.draw(glyphs, offset, scale, color);
|
||||
self.draw_cmds.lock().extend(cmds.into_iter());
|
||||
}
|
||||
|
||||
pub fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) {
|
||||
f(self.draw_cmds.lock().as_slice());
|
||||
}
|
||||
|
||||
pub fn font_load(&self, family: &str) -> u32 {
|
||||
let mut family_cache = self.font_families.lock();
|
||||
|
||||
if let Some(cached) = family_cache.get(family) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
let font = self.font_store.load_font(family);
|
||||
let mut loaded = self.loaded_fonts.write();
|
||||
let id = loaded.len() as u32;
|
||||
family_cache.insert(family.to_string(), id);
|
||||
loaded.push(font);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn text_layout_new(&self, font_id: u32, text: &str) -> u32 {
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(font_id as usize).unwrap();
|
||||
let layout = font.shape(text);
|
||||
self.text_layouts.write().insert(layout) as u32
|
||||
}
|
||||
|
||||
pub fn text_layout_delete(&self, id: u32) {
|
||||
self.text_layouts.write().remove(id as usize);
|
||||
}
|
||||
|
||||
pub fn text_layout_get_bounds(&self, id: u32, dst: &mut Rect) {
|
||||
let src = self.text_layouts.read().get(id as usize).unwrap().bounds;
|
||||
let _ = std::mem::replace(dst, src);
|
||||
}
|
||||
|
||||
pub fn message_new(&self, data: Vec<u8>) -> u32 {
|
||||
let mut store = self.message_store.write();
|
||||
let id = store.insert(data) as u32;
|
||||
id
|
||||
}
|
||||
|
||||
pub fn message_free(&self, id: u32) {
|
||||
let mut store = self.message_store.write();
|
||||
store.remove(id as usize);
|
||||
}
|
||||
|
||||
pub fn message_get_len(&self, id: u32) -> u32 {
|
||||
self.message_store.read().get(id as usize).unwrap().len() as u32
|
||||
}
|
||||
|
||||
pub fn message_get_data(&self, id: u32, dst: &mut [u8]) {
|
||||
let store = self.message_store.read();
|
||||
let src = store.get(id as usize).unwrap();
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
|
||||
pub fn panel_send_message(&self, id: u32, message: Vec<u8>) {
|
||||
if let Some(panel) = self.panels.read().get(id as usize) {
|
||||
panel.outgoing_messages.write().push_back(message);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_panel_messages(&self, id: PanelId) -> Vec<Vec<u8>> {
|
||||
if let Some(panel) = self.panels.read().get(id.0) {
|
||||
panel.outgoing_messages.write().drain(..).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PanelAbi {
|
||||
outgoing_messages: RwLock<VecDeque<Vec<u8>>>,
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use std::collections::{hash_map::DefaultHasher, HashMap};
|
||||
use std::hash::{Hasher, BuildHasherDefault};
|
||||
use std::ops::DerefMut;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{Arc, Backend, Instance, PanelId, ScriptAbi};
|
||||
use crate::DrawCommand;
|
||||
|
||||
use canary_script::{Color, CursorEventKind, Rect, Vec2};
|
||||
use parking_lot::Mutex;
|
||||
use prehash::Passthru;
|
||||
|
||||
type Caller<'a> = wasmtime::Caller<'a, Arc<ScriptAbi>>;
|
||||
type Store = wasmtime::Store<Arc<ScriptAbi>>;
|
||||
type Linker = wasmtime::Linker<Arc<ScriptAbi>>;
|
||||
type ModuleCache = Mutex<HashMap<u64, wasmtime::Module, BuildHasherDefault<Passthru>>>;
|
||||
|
||||
pub struct WasmtimeBackend {
|
||||
engine: wasmtime::Engine,
|
||||
module_cache: ModuleCache,
|
||||
}
|
||||
|
||||
impl WasmtimeBackend {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
log::info!("Creating wasmtime backend");
|
||||
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_simd(true);
|
||||
config.wasm_bulk_memory(true);
|
||||
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
|
||||
config.cache_config_load_default()?;
|
||||
|
||||
let engine = wasmtime::Engine::new(&config)?;
|
||||
let module_cache = Default::default();
|
||||
Ok(Self {
|
||||
engine,
|
||||
module_cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for WasmtimeBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
"wasmtime"
|
||||
}
|
||||
|
||||
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
|
||||
let start = Instant::now();
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(module);
|
||||
let hash = hasher.finish();
|
||||
let fmt_hash = format!("{:x}", hash);
|
||||
|
||||
log::debug!("Loading module (hash: {})", fmt_hash);
|
||||
let mut cache = self.module_cache.lock();
|
||||
|
||||
let module = if let Some(module) = cache.get(&hash) {
|
||||
log::debug!("Module load cache hit (hash: {})", fmt_hash);
|
||||
module
|
||||
} else {
|
||||
log::debug!("Module load cache miss; building (hash: {})", fmt_hash);
|
||||
let start = Instant::now();
|
||||
let module = wasmtime::Module::new(&self.engine, module)?;
|
||||
cache.insert(hash, module);
|
||||
log::debug!("Built module in {:?} (hash: {})", start.elapsed(), fmt_hash);
|
||||
cache.get(&hash).unwrap()
|
||||
};
|
||||
|
||||
let mut store = wasmtime::Store::new(&self.engine, abi);
|
||||
let mut linker = Linker::new(&self.engine);
|
||||
WasmtimeInstance::link(&mut linker)?;
|
||||
let instance = linker.instantiate(&mut store, module)?;
|
||||
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
|
||||
let update = instance.get_typed_func(&mut store, "update")?;
|
||||
let draw = instance.get_typed_func(&mut store, "draw")?;
|
||||
let on_resize = instance.get_typed_func(&mut store, "on_resize")?;
|
||||
let on_cursor_event = instance.get_typed_func(&mut store, "on_cursor_event")?;
|
||||
let on_message = instance.get_typed_func(&mut store, "on_message")?;
|
||||
|
||||
let instance = WasmtimeInstance {
|
||||
store: Mutex::new(store),
|
||||
bind_panel,
|
||||
update,
|
||||
draw,
|
||||
on_resize,
|
||||
on_cursor_event,
|
||||
on_message,
|
||||
};
|
||||
|
||||
let instance = Arc::new(instance);
|
||||
|
||||
log::debug!(
|
||||
"Loaded module in {:?} (hash: {})",
|
||||
start.elapsed(),
|
||||
fmt_hash
|
||||
);
|
||||
|
||||
Ok(instance)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WasmtimeInstance {
|
||||
store: Mutex<Store>,
|
||||
bind_panel: wasmtime::TypedFunc<(u32, u32, u32), u32>,
|
||||
update: wasmtime::TypedFunc<(u32, f32), ()>,
|
||||
draw: wasmtime::TypedFunc<u32, ()>,
|
||||
on_resize: wasmtime::TypedFunc<(u32, f32, f32), ()>,
|
||||
on_cursor_event: wasmtime::TypedFunc<(u32, u32, f32, f32), ()>,
|
||||
on_message: wasmtime::TypedFunc<(u32, u32), ()>,
|
||||
}
|
||||
|
||||
impl WasmtimeInstance {
|
||||
pub fn link(linker: &mut Linker) -> anyhow::Result<()> {
|
||||
let module = "env";
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"draw_indexed",
|
||||
|mut caller: Caller<'_>,
|
||||
vertices_ptr: u32,
|
||||
vertices_num: u32,
|
||||
indices_ptr: u32,
|
||||
indices_num: u32| {
|
||||
let vertices = Self::get_memory_slice(&mut caller, vertices_ptr, vertices_num);
|
||||
let indices = Self::get_memory_slice(&mut caller, indices_ptr, indices_num);
|
||||
caller.data().draw_indexed(vertices, indices);
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"draw_text_layout",
|
||||
|caller: Caller<'_>, id: u32, xoff: f32, yoff: f32, scale: f32, color: u32| {
|
||||
let offset = Vec2 { x: xoff, y: yoff };
|
||||
let color = Color(color);
|
||||
caller.data().draw_text_layout(id, offset, scale, color);
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"font_load",
|
||||
|mut caller: Caller<'_>, family_ptr: u32, family_len: u32| {
|
||||
let family = Self::get_memory_slice_str(&mut caller, family_ptr, family_len);
|
||||
caller.data().font_load(family)
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"text_layout_new",
|
||||
|mut caller: Caller<'_>, font_id: u32, text_ptr: u32, text_len: u32| {
|
||||
let text = Self::get_memory_slice_str(&mut caller, text_ptr, text_len);
|
||||
caller.data().text_layout_new(font_id, text)
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"text_layout_delete",
|
||||
|caller: Caller<'_>, id: u32| caller.data().text_layout_delete(id),
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"text_layout_get_bounds",
|
||||
|mut caller: Caller<'_>, id: u32, rect_ptr: u32| {
|
||||
let rect: &mut Rect = Self::get_memory_ref(&mut caller, rect_ptr);
|
||||
caller.data().text_layout_get_bounds(id, rect);
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"message_get_len",
|
||||
|caller: Caller<'_>, id: u32| -> u32 { caller.data().message_get_len(id) },
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"message_get_data",
|
||||
|mut caller: Caller<'_>, id: u32, ptr: u32| {
|
||||
let ptr = ptr as usize;
|
||||
let len = caller.data().message_get_len(id) as usize;
|
||||
let dst = Self::get_memory_slice_bytes(&mut caller, ptr, len);
|
||||
caller.data().message_get_data(id, dst)
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"panel_send_message",
|
||||
|mut caller: Caller<'_>, id: u32, ptr: u32, len: u32| {
|
||||
let message = Self::get_memory_slice_bytes(&mut caller, ptr as usize, len as usize);
|
||||
caller.data().panel_send_message(id, message.to_vec())
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_memory_ref<D: bytemuck::Pod>(caller: &mut Caller<'_>, ptr: u32) -> &'static mut D {
|
||||
let len = std::mem::size_of::<D>();
|
||||
let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len);
|
||||
bytemuck::from_bytes_mut(bytes)
|
||||
}
|
||||
|
||||
fn get_memory_slice<D: bytemuck::Pod>(
|
||||
caller: &mut Caller<'_>,
|
||||
ptr: u32,
|
||||
num: u32,
|
||||
) -> &'static mut [D] {
|
||||
let len = num as usize * std::mem::size_of::<D>();
|
||||
let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len);
|
||||
bytemuck::cast_slice_mut(bytes)
|
||||
}
|
||||
|
||||
fn get_memory_slice_str(caller: &mut Caller<'_>, ptr: u32, len: u32) -> &'static mut str {
|
||||
let memory = Self::get_memory_slice_bytes(caller, ptr as usize, len as usize);
|
||||
std::str::from_utf8_mut(memory).unwrap()
|
||||
}
|
||||
|
||||
fn get_memory_slice_bytes(
|
||||
caller: &mut Caller<'_>,
|
||||
ptr: usize,
|
||||
len: usize,
|
||||
) -> &'static mut [u8] {
|
||||
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
|
||||
if ptr + len > memory.data_size(&caller) {
|
||||
panic!("Attempted wasm memory read is out-of-bounds!");
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ptr = memory.data_ptr(caller).add(ptr);
|
||||
std::slice::from_raw_parts_mut(ptr, len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Instance for WasmtimeInstance {
|
||||
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32 {
|
||||
let mut store = self.store.lock();
|
||||
let protocol = store.data().message_new(protocol.as_bytes().to_vec());
|
||||
let msg = store.data().message_new(msg);
|
||||
let args = (panel.0 as u32, protocol, msg);
|
||||
let data = self.bind_panel.call(store.deref_mut(), args).unwrap();
|
||||
store.data().message_free(protocol);
|
||||
store.data().message_free(msg);
|
||||
data
|
||||
}
|
||||
|
||||
fn update(&self, panel_ud: u32, dt: f32) {
|
||||
let mut store = self.store.lock();
|
||||
self.update.call(store.deref_mut(), (panel_ud, dt)).unwrap();
|
||||
}
|
||||
|
||||
fn draw(&self, panel_ud: u32) -> Vec<DrawCommand> {
|
||||
let mut store = self.store.lock();
|
||||
store.data().start_draw();
|
||||
self.draw.call(store.deref_mut(), panel_ud).unwrap();
|
||||
let mut cmds = Vec::new();
|
||||
store
|
||||
.data()
|
||||
.with_draw_commands(|slice| cmds.extend_from_slice(slice));
|
||||
cmds
|
||||
}
|
||||
|
||||
fn on_resize(&self, panel_ud: u32, new_size: Vec2) {
|
||||
let mut store = self.store.lock();
|
||||
self.on_resize
|
||||
.call(store.deref_mut(), (panel_ud, new_size.x, new_size.y))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn on_cursor_event(&self, panel_ud: u32, kind: CursorEventKind, at: Vec2) {
|
||||
let mut store = self.store.lock();
|
||||
self.on_cursor_event
|
||||
.call(store.deref_mut(), (panel_ud, kind as u32, at.x, at.y))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn on_message(&self, panel_ud: u32, msg: Vec<u8>) {
|
||||
let mut store = self.store.lock();
|
||||
let msg = store.data().message_new(msg);
|
||||
self.on_message
|
||||
.call(store.deref_mut(), (panel_ud, msg))
|
||||
.unwrap();
|
||||
store.data().message_free(msg);
|
||||
}
|
||||
}
|
431
src/lib.rs
431
src/lib.rs
|
@ -1,361 +1,110 @@
|
|||
pub use canary_types::*;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
pub use canary_script::*;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use slab::Slab;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod backend;
|
||||
pub mod text;
|
||||
|
||||
/// Low-level script API callbacks.
|
||||
///
|
||||
/// If you're a casual user of Canary the struct you're looking for is
|
||||
/// [ScriptAbiImpl]. This trait exists to help with making mocks for testing.
|
||||
pub trait ScriptAbi {
|
||||
fn start_draw(&self);
|
||||
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]);
|
||||
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color);
|
||||
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand]));
|
||||
use backend::{Backend, Instance, ScriptAbi};
|
||||
use text::FontStore;
|
||||
|
||||
fn font_load(&self, family: &str) -> u32;
|
||||
|
||||
fn text_layout_new(&self, font_id: u32, text: &str) -> u32;
|
||||
fn text_layout_delete(&self, id: u32);
|
||||
fn text_layout_get_bounds(&self, id: u32, rect: &mut Rect);
|
||||
|
||||
fn message_new(&self, data: Vec<u8>) -> u32;
|
||||
fn message_free(&self, id: u32);
|
||||
fn message_get_len(&self, id: u32) -> u32;
|
||||
fn message_get_data(&self, id: u32, dst: &mut [u8]);
|
||||
/// The main interface to Canary.
|
||||
pub struct Runtime {
|
||||
backend: Box<dyn Backend>,
|
||||
font_store: Arc<FontStore>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
pub fn new(backend: Box<dyn Backend>) -> anyhow::Result<Self> {
|
||||
log::info!("Initializing runtime with {} backend", backend.name());
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
font_store: Arc::new(FontStore::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_module(&self, module: &[u8]) -> anyhow::Result<Script> {
|
||||
let abi = ScriptAbi::new(self.font_store.to_owned());
|
||||
let abi = Arc::new(abi);
|
||||
let instance = self.backend.load_module(abi.to_owned(), module)?;
|
||||
|
||||
Ok(Script {
|
||||
instance,
|
||||
abi,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A loaded instance of a Canary script.
|
||||
pub struct Script {
|
||||
instance: Arc<dyn Instance>,
|
||||
abi: Arc<ScriptAbi>,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn create_panel(&mut self, protocol: &str, msg: Vec<u8>) -> anyhow::Result<Panel> {
|
||||
let id = self.abi.create_panel();
|
||||
let userdata = self.instance.bind_panel(id, protocol, msg);
|
||||
Ok(Panel {
|
||||
instance: self.instance.clone(),
|
||||
abi: self.abi.clone(),
|
||||
id,
|
||||
userdata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A Canary panel.
|
||||
pub struct Panel {
|
||||
instance: Arc<dyn Instance>,
|
||||
abi: Arc<ScriptAbi>,
|
||||
id: PanelId,
|
||||
userdata: u32,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
pub fn update(&self, dt: f32) {
|
||||
self.instance.update(self.userdata, dt);
|
||||
}
|
||||
|
||||
pub fn draw(&self) -> Vec<DrawCommand> {
|
||||
self.instance.draw(self.userdata)
|
||||
}
|
||||
|
||||
pub fn on_resize(&self, new_size: Vec2) {
|
||||
self.instance.on_resize(self.userdata, new_size);
|
||||
}
|
||||
|
||||
pub fn on_cursor_event(&self, kind: CursorEventKind, at: Vec2) {
|
||||
self.instance.on_cursor_event(self.userdata, kind, at);
|
||||
}
|
||||
|
||||
pub fn on_message(&self, msg: Vec<u8>) {
|
||||
self.instance.on_message(self.userdata, msg);
|
||||
}
|
||||
|
||||
pub fn recv_messages(&self) -> Vec<Vec<u8>> {
|
||||
self.abi.recv_panel_messages(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Proportion constant between pixels (at 96dpi) to millimeters (Canary's unit measurement).
|
||||
pub const PX_PER_MM: f32 = 25.4 / 96.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PanelId(pub(crate) usize);
|
||||
|
||||
pub trait ScriptInstance {
|
||||
fn bind_panel(&mut self, msg: Vec<u8>) -> PanelId;
|
||||
fn update(&mut self, panel: PanelId, dt: f32);
|
||||
fn draw(&mut self, panel: PanelId, f: impl FnOnce(&[DrawCommand]));
|
||||
fn on_cursor_event(&mut self, panel: PanelId, kind: CursorEventKind, at: Vec2);
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DrawCommand {
|
||||
Mesh {
|
||||
vertices: Vec<MeshVertex>,
|
||||
indices: Vec<MeshIndex>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct WasmtimeRuntime {
|
||||
engine: wasmtime::Engine,
|
||||
}
|
||||
|
||||
impl WasmtimeRuntime {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_simd(true);
|
||||
config.wasm_bulk_memory(true);
|
||||
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
|
||||
|
||||
let engine = wasmtime::Engine::new(&config)?;
|
||||
|
||||
Ok(Self { engine })
|
||||
}
|
||||
|
||||
pub fn load_module<T: ScriptAbi>(
|
||||
&self,
|
||||
abi: T,
|
||||
module: &[u8],
|
||||
) -> anyhow::Result<WasmtimeScript<T>> {
|
||||
let module = wasmtime::Module::new(&self.engine, module)?;
|
||||
let mut store = wasmtime::Store::new(&self.engine, abi);
|
||||
let panel_datas = Default::default();
|
||||
let mut linker = wasmtime::Linker::new(&self.engine);
|
||||
WasmtimeScript::link(&mut linker)?;
|
||||
let instance = linker.instantiate(&mut store, &module)?;
|
||||
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
|
||||
let update = instance.get_typed_func(&mut store, "update")?;
|
||||
let draw = instance.get_typed_func(&mut store, "draw")?;
|
||||
let on_cursor_event = instance.get_typed_func(&mut store, "on_cursor_event")?;
|
||||
|
||||
Ok(WasmtimeScript {
|
||||
store,
|
||||
panel_datas,
|
||||
bind_panel,
|
||||
update,
|
||||
draw,
|
||||
on_cursor_event,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WasmtimeScript<T> {
|
||||
store: wasmtime::Store<T>,
|
||||
panel_datas: Slab<u32>,
|
||||
bind_panel: wasmtime::TypedFunc<(u32, u32), u32>,
|
||||
update: wasmtime::TypedFunc<(u32, f32), ()>,
|
||||
draw: wasmtime::TypedFunc<u32, ()>,
|
||||
on_cursor_event: wasmtime::TypedFunc<(u32, u32, f32, f32), ()>,
|
||||
}
|
||||
|
||||
impl<T: ScriptAbi> WasmtimeScript<T> {
|
||||
pub fn link(linker: &mut wasmtime::Linker<T>) -> anyhow::Result<()> {
|
||||
let module = "env";
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"draw_indexed",
|
||||
|mut caller: wasmtime::Caller<'_, T>,
|
||||
vertices_ptr: u32,
|
||||
vertices_num: u32,
|
||||
indices_ptr: u32,
|
||||
indices_num: u32| {
|
||||
let vertices = Self::get_memory_slice(&mut caller, vertices_ptr, vertices_num);
|
||||
let indices = Self::get_memory_slice(&mut caller, indices_ptr, indices_num);
|
||||
caller.data().draw_indexed(vertices, indices);
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"draw_text_layout",
|
||||
|caller: wasmtime::Caller<'_, T>,
|
||||
id: u32,
|
||||
xoff: f32,
|
||||
yoff: f32,
|
||||
scale: f32,
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
a: f32| {
|
||||
let offset = Vec2 { x: xoff, y: yoff };
|
||||
let color = Color { r, g, b, a };
|
||||
caller.data().draw_text_layout(id, offset, scale, color);
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"font_load",
|
||||
|mut caller: wasmtime::Caller<'_, T>, family_ptr: u32, family_len: u32| {
|
||||
let family = Self::get_memory_slice_str(&mut caller, family_ptr, family_len);
|
||||
caller.data().font_load(family)
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"text_layout_new",
|
||||
|mut caller: wasmtime::Caller<'_, T>, font_id: u32, text_ptr: u32, text_len: u32| {
|
||||
let text = Self::get_memory_slice_str(&mut caller, text_ptr, text_len);
|
||||
caller.data().text_layout_new(font_id, text)
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"text_layout_delete",
|
||||
|caller: wasmtime::Caller<'_, T>, id: u32| caller.data().text_layout_delete(id),
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"text_layout_get_bounds",
|
||||
|mut caller: wasmtime::Caller<'_, T>, id: u32, rect_ptr: u32| {
|
||||
let rect: &mut Rect = Self::get_memory_ref(&mut caller, rect_ptr);
|
||||
caller.data().text_layout_get_bounds(id, rect);
|
||||
},
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"message_get_len",
|
||||
|caller: wasmtime::Caller<'_, T>, id: u32| -> u32 {
|
||||
caller.data().message_get_len(id)
|
||||
}
|
||||
)?;
|
||||
|
||||
linker.func_wrap(
|
||||
module,
|
||||
"message_get_data",
|
||||
|mut caller: wasmtime::Caller<'_, T>, id: u32, ptr: u32| {
|
||||
let ptr = ptr as usize;
|
||||
let len = caller.data().message_get_len(id) as usize;
|
||||
let dst = Self::get_memory_slice_bytes(&mut caller, ptr, len);
|
||||
caller.data().message_get_data(id, dst)
|
||||
}
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_panel_data(&self, panel: PanelId) -> u32 {
|
||||
*self.panel_datas.get(panel.0).unwrap()
|
||||
}
|
||||
|
||||
fn get_memory_ref<D: bytemuck::Pod>(
|
||||
caller: &mut wasmtime::Caller<'_, T>,
|
||||
ptr: u32,
|
||||
) -> &'static mut D {
|
||||
let len = std::mem::size_of::<D>();
|
||||
let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len);
|
||||
bytemuck::from_bytes_mut(bytes)
|
||||
}
|
||||
|
||||
fn get_memory_slice<D: bytemuck::Pod>(
|
||||
caller: &mut wasmtime::Caller<'_, T>,
|
||||
ptr: u32,
|
||||
num: u32,
|
||||
) -> &'static mut [D] {
|
||||
let len = num as usize * std::mem::size_of::<D>();
|
||||
let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len);
|
||||
bytemuck::cast_slice_mut(bytes)
|
||||
}
|
||||
|
||||
fn get_memory_slice_str(
|
||||
caller: &mut wasmtime::Caller<'_, T>,
|
||||
ptr: u32,
|
||||
len: u32,
|
||||
) -> &'static mut str {
|
||||
let memory = Self::get_memory_slice_bytes(caller, ptr as usize, len as usize);
|
||||
std::str::from_utf8_mut(memory).unwrap()
|
||||
}
|
||||
|
||||
fn get_memory_slice_bytes(
|
||||
caller: &mut wasmtime::Caller<'_, T>,
|
||||
ptr: usize,
|
||||
len: usize,
|
||||
) -> &'static mut [u8] {
|
||||
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
|
||||
if ptr + len > memory.data_size(&caller) {
|
||||
panic!("Attempted wasm memory read is out-of-bounds!");
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ptr = memory.data_ptr(caller).add(ptr);
|
||||
std::slice::from_raw_parts_mut(ptr, len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ScriptAbi> ScriptInstance for WasmtimeScript<T> {
|
||||
fn bind_panel(&mut self, msg: Vec<u8>) -> PanelId {
|
||||
let id = self.panel_datas.insert(0);
|
||||
let msg = self.store.data().message_new(msg);
|
||||
let args = (id as u32, msg);
|
||||
let data = self.bind_panel.call(&mut self.store, args).unwrap();
|
||||
*self.panel_datas.get_mut(id).unwrap() = data;
|
||||
PanelId(id)
|
||||
}
|
||||
|
||||
fn update(&mut self, panel: PanelId, dt: f32) {
|
||||
let data = self.get_panel_data(panel);
|
||||
self.update.call(&mut self.store, (data, dt)).unwrap();
|
||||
}
|
||||
|
||||
fn draw(&mut self, panel: PanelId, f: impl FnOnce(&[DrawCommand])) {
|
||||
let data = self.get_panel_data(panel);
|
||||
self.store.data().start_draw();
|
||||
self.draw.call(&mut self.store, data).unwrap();
|
||||
self.store.data().with_draw_commands(f);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, panel: PanelId, kind: CursorEventKind, at: Vec2) {
|
||||
let data = self.get_panel_data(panel);
|
||||
self.on_cursor_event
|
||||
.call(&mut self.store, (data, kind as u32, at.x, at.y))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// The standard [ScriptAbi] implementation to use.
|
||||
#[derive(Default)]
|
||||
pub struct ScriptAbiImpl {
|
||||
draw_cmds: Mutex<Vec<DrawCommand>>,
|
||||
font_store: text::FontStore,
|
||||
font_families: Mutex<HashMap<String, u32>>,
|
||||
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
|
||||
text_layouts: RwLock<Slab<text::TextLayout>>,
|
||||
message_store: RwLock<Slab<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl ScriptAbi for ScriptAbiImpl {
|
||||
fn start_draw(&self) {
|
||||
let mut lock = self.draw_cmds.lock();
|
||||
lock.clear();
|
||||
}
|
||||
|
||||
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
self.draw_cmds.lock().push(DrawCommand::Mesh {
|
||||
vertices: vertices.to_vec(),
|
||||
indices: indices.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color) {
|
||||
// TODO multiple fonts per layout
|
||||
let layouts = self.text_layouts.read();
|
||||
let layout = layouts.get(id as usize).unwrap();
|
||||
let glyphs = layout.glyphs.as_slice();
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(layout.font_id as usize).unwrap();
|
||||
let cmds = font.draw(glyphs, offset, scale, color);
|
||||
self.draw_cmds.lock().extend(cmds.into_iter());
|
||||
}
|
||||
|
||||
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) {
|
||||
f(self.draw_cmds.lock().as_slice());
|
||||
}
|
||||
|
||||
fn font_load(&self, family: &str) -> u32 {
|
||||
let mut family_cache = self.font_families.lock();
|
||||
|
||||
if let Some(cached) = family_cache.get(family) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
let font = self.font_store.load_font(family);
|
||||
let mut loaded = self.loaded_fonts.write();
|
||||
let id = loaded.len() as u32;
|
||||
family_cache.insert(family.to_string(), id);
|
||||
loaded.push(font);
|
||||
id
|
||||
}
|
||||
|
||||
fn text_layout_new(&self, font_id: u32, text: &str) -> u32 {
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(font_id as usize).unwrap();
|
||||
let layout = font.shape(text);
|
||||
self.text_layouts.write().insert(layout) as u32
|
||||
}
|
||||
|
||||
fn text_layout_delete(&self, id: u32) {
|
||||
self.text_layouts.write().remove(id as usize);
|
||||
}
|
||||
|
||||
fn text_layout_get_bounds(&self, id: u32, dst: &mut Rect) {
|
||||
let src = self.text_layouts.read().get(id as usize).unwrap().bounds;
|
||||
let _ = std::mem::replace(dst, src);
|
||||
}
|
||||
|
||||
fn message_new(&self, data: Vec<u8>) -> u32 {
|
||||
let mut store = self.message_store.write();
|
||||
let id = store.insert(data) as u32;
|
||||
id
|
||||
}
|
||||
|
||||
fn message_free(&self, id: u32) {
|
||||
let mut store = self.message_store.write();
|
||||
store.remove(id as usize);
|
||||
}
|
||||
|
||||
fn message_get_len(&self, id: u32) -> u32 {
|
||||
self.message_store.read().get(id as usize).unwrap().len() as u32
|
||||
}
|
||||
|
||||
fn message_get_data(&self, id: u32, dst: &mut [u8]) {
|
||||
let store = self.message_store.read();
|
||||
let src = store.get(id as usize).unwrap();
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use super::AllsortsFont;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use super::{AllsortsFont, Rect, Vec2};
|
||||
|
||||
use allsorts::binary::read::ReadScope;
|
||||
use allsorts::cff::CFF;
|
||||
use allsorts::font::GlyphTableFlags;
|
||||
|
@ -7,7 +11,6 @@ use allsorts::outline::OutlineBuilder;
|
|||
use allsorts::pathfinder_geometry::{line_segment::LineSegment2F, vector::Vector2F};
|
||||
use allsorts::tables::{glyf::GlyfTable, loca::LocaTable, FontTableProvider, SfntVersion};
|
||||
use allsorts::tag;
|
||||
use canary_types::{Rect, Vec2};
|
||||
use lyon::path::Path;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
|
@ -90,10 +93,7 @@ impl CachedGlyph {
|
|||
&fill_options,
|
||||
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
|
||||
let position = vertex.position();
|
||||
Vec2 {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
}
|
||||
Vec2::new(position.x, position.y)
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -132,27 +132,9 @@ impl OutlineSink {
|
|||
}
|
||||
|
||||
fn pf_vector_to_lyon(&mut self, v: &Vector2F) -> lyon::geom::Point<f32> {
|
||||
let point = lyon::geom::Point::<f32>::new(v.x(), v.y()) / self.units_per_em;
|
||||
|
||||
// TODO clean this up with helper math methods?
|
||||
let bb = &mut self.bounding_box;
|
||||
|
||||
if point.x < bb.bl.x {
|
||||
bb.bl.x = point.x;
|
||||
}
|
||||
|
||||
if point.x > bb.tr.x {
|
||||
bb.tr.x = point.x;
|
||||
}
|
||||
|
||||
if point.y < bb.bl.y {
|
||||
bb.bl.y = point.y;
|
||||
}
|
||||
|
||||
if point.y > bb.tr.y {
|
||||
bb.tr.y = point.y;
|
||||
}
|
||||
|
||||
let point = lyon::geom::Point::<f32>::new(v.x(), -v.y()) / self.units_per_em;
|
||||
let glam_point = Vec2::new(point.x, point.y);
|
||||
self.bounding_box = self.bounding_box.union_point(glam_point);
|
||||
point
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
use super::{DrawCommand, MeshIndex, MeshVertex};
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use super::{Color, DrawCommand, MeshIndex, MeshVertex, Rect, Vec2};
|
||||
|
||||
use allsorts::binary::read::ReadScope;
|
||||
use allsorts::font::MatchingPresentation;
|
||||
use allsorts::font_data::{DynamicFontTableProvider, FontData as AllsortsFontData};
|
||||
use allsorts::glyph_position::{GlyphLayout, TextDirection};
|
||||
use allsorts::{tag, Font as AllsortsFont};
|
||||
use canary_types::{Color, Rect, Vec2};
|
||||
use ouroboros::self_referencing;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
|
@ -71,31 +73,9 @@ impl Font {
|
|||
xcur += position.hori_advance;
|
||||
ycur += position.vert_advance;
|
||||
|
||||
let xpos = xpos as f32 / units_per_em;
|
||||
let ypos = ypos as f32 / units_per_em;
|
||||
|
||||
let mut bb = glyphs.get(position.index as usize).unwrap().bounding_box;
|
||||
bb.bl.x = bb.bl.x + xpos;
|
||||
bb.bl.y = bb.bl.y + ypos;
|
||||
bb.tr.x = bb.tr.x + xpos;
|
||||
bb.tr.y = bb.tr.y + ypos;
|
||||
|
||||
// TODO use euclid instead
|
||||
if bounds.bl.x > bb.bl.x {
|
||||
bounds.bl.x = bb.bl.x;
|
||||
}
|
||||
|
||||
if bounds.bl.y > bb.bl.y {
|
||||
bounds.bl.y = bb.bl.y;
|
||||
}
|
||||
|
||||
if bounds.tr.x < bb.tr.x {
|
||||
bounds.tr.x = bb.tr.x;
|
||||
}
|
||||
|
||||
if bounds.tr.y < bb.tr.y {
|
||||
bounds.tr.y = bb.tr.y;
|
||||
}
|
||||
let pos = Vec2::new(xpos as f32, ypos as f32) / units_per_em;
|
||||
let bb = glyphs.get(position.index as usize).unwrap().bounding_box;
|
||||
bounds = bounds.union(&bb.offset(pos));
|
||||
}
|
||||
|
||||
TextLayout {
|
||||
|
@ -130,14 +110,14 @@ impl Font {
|
|||
ycur += position.vert_advance;
|
||||
|
||||
let voff = vertices.len() as MeshIndex;
|
||||
let xpos = xpos as f32 * pos_scale + offset.x;
|
||||
let ypos = ypos as f32 * pos_scale + offset.y;
|
||||
let xpos = offset.x + xpos as f32 * pos_scale;
|
||||
let ypos = offset.y - ypos as f32 * pos_scale;
|
||||
|
||||
for v in glyph.vertices.iter() {
|
||||
let x = v.x * scale + xpos;
|
||||
let y = v.y * scale + ypos;
|
||||
vertices.push(MeshVertex {
|
||||
position: canary_types::Vec2 { x, y },
|
||||
position: Vec2::new(x, y),
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
@ -240,6 +220,7 @@ impl Default for FontStore {
|
|||
|
||||
impl FontStore {
|
||||
pub fn new() -> Self {
|
||||
log::info!("Initializing FontStore");
|
||||
let source = font_kit::source::SystemSource::new();
|
||||
let source = Box::new(source);
|
||||
|
||||
|
@ -261,14 +242,14 @@ impl FontStore {
|
|||
use font_kit::handle::Handle;
|
||||
use font_kit::properties::Properties;
|
||||
|
||||
println!("loading font family {}", title);
|
||||
log::info!("Finding font by family: {}", title);
|
||||
|
||||
let font_handle = self
|
||||
.source
|
||||
.select_best_match(&[FamilyName::Title(title.to_string())], &Properties::new())
|
||||
.unwrap();
|
||||
|
||||
println!("loading font file: {:?}", font_handle);
|
||||
log::info!("Loading font file: {:?}", font_handle);
|
||||
|
||||
let font_data = if let Handle::Path {
|
||||
path,
|
||||
|
|
Loading…
Reference in New Issue