diff --git a/LICENSE b/LICENSE
index 02bbb60..f288702 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,165 +1,674 @@
- GNU LESSER GENERAL PUBLIC LICENSE
+ GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
- Copyright (C) 2007 Free Software Foundation, Inc.
+ Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
+ Preamble
- 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.
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
- 0. Additional Definitions.
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
- 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.
+ 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.
- "The Library" refers to a covered work governed by this License,
-other than an Application or a Combined Work as defined below.
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
- 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.
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
- 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".
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
- 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.
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
- 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.
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
- 1. Exception to Section 3 of the GNU GPL.
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
- You may convey a covered work under sections 3 and 4 of this License
-without being bound by section 3 of the GNU GPL.
+ The precise terms and conditions for copying, distribution and
+modification follow.
- 2. Conveying Modified Versions.
+ TERMS AND CONDITIONS
- 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:
+ 0. Definitions.
- 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
+ "This License" refers to version 3 of the GNU General Public License.
- b) under the GNU GPL, with none of the additional permissions of
- this License applicable to that copy.
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
- 3. Object Code Incorporating Material from Library Header Files.
+ "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.
- 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:
+ 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) 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.
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
- b) Accompany the object code with a copy of the GNU GPL and this license
- document.
+ 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.
- 4. Combined Works.
+ 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.
- 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:
+ 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.
- 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.
+ 1. Source Code.
- b) Accompany the Combined Work with a copy of the GNU GPL and this license
- document.
+ 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.
- 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.
+ 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.
- d) Do one of the following:
+ 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.
- 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.
+ 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.
- 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.
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
- 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.)
+ The Corresponding Source for a work in source code form is that
+same work.
- 5. Combined Libraries.
+ 2. Basic Permissions.
- 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:
+ 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.
- 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.
+ 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.
- 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.
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
- 6. Revised Versions of the GNU Lesser General Public License.
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
- 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.
+ 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.
- 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.
+ 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.
- 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.
\ No newline at end of file
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.md b/README.md
index 77e1766..d63e401 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[]()
-[](https://www.gnu.org/licenses/lgpl-3.0)
+[](https://www.gnu.org/licenses/gpl-3.0)
[](https://github.com/kunkundi/crossdesk/commits/web-client)
[](https://github.com/kunkundi/crossdesk/actions)
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
@@ -82,34 +82,77 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/)
-Linux环境下需安装以下包:
+### Linux
-```
-sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxfixes-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
+Linux 构建目前支持 Ubuntu 22.04 及以上版本的 amd64 和 arm64。先安装基础编译依赖:
+
+```bash
+sudo apt-get update
+sudo apt-get install -y \
+ git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
+ libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
+ libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
+ libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
+ libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
+ libasound2-dev libsndio-dev libpulse-dev
```
-编译
-```
-git clone https://github.com/kunkundi/crossdesk.git
+需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
+```bash
+sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
+```
+
+下载子模块并编译 Release 版本:
+
+```bash
+git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
-git submodule init
-
-git submodule update
+# 已经克隆过仓库时执行这一行
+git submodule update --init --recursive
+xmake f -m release --USE_CUDA=false -y
xmake b -vy crossdesk
```
-编译选项
+
+amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
+
+启用 Wayland 和 DRM 的配置示例:
+
+```bash
+xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
+xmake b -vy crossdesk
```
+
+生成 Debian 安装包(必须在对应架构上先完成 Release 编译):
+
+```bash
+# amd64
+./scripts/linux/pkg_amd64.sh 1.0.0
+
+# arm64
+./scripts/linux/pkg_arm64.sh 1.0.0
+```
+
+打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
+
+### 通用编译选项
+
+```text
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
+--USE_WAYLAND=true/false: 在 Linux 上启用 Wayland/PipeWire 捕获,默认不启用
+--USE_DRM=true/false: 在 Linux 上启用 DRM 捕获,默认不启用
+--CROSSDESK_PORTABLE=true/false: 构建便携版本,默认不启用
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
# 示例
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
-运行
-```
+
+运行:
+
+```bash
xmake r crossdesk
```
diff --git a/README_EN.md b/README_EN.md
index 8837a5d..0e7dd17 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -2,7 +2,7 @@
[]()
-[](https://www.gnu.org/licenses/lgpl-3.0)
+[](https://www.gnu.org/licenses/gpl-3.0)
[](https://github.com/kunkundi/crossdesk/commits/web-client)
[](https://github.com/kunkundi/crossdesk/actions)
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
@@ -87,34 +87,77 @@ Requirements:
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/)
-Following packages need to be installed on Linux:
+### Linux
-```
-sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxfixes-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
+Linux builds currently support Ubuntu 22.04 or later on amd64 and arm64. Install the base build dependencies first:
+
+```bash
+sudo apt-get update
+sudo apt-get install -y \
+ git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
+ libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
+ libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
+ libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
+ libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
+ libasound2-dev libsndio-dev libpulse-dev
```
-Build:
-```
-git clone https://github.com/kunkundi/crossdesk.git
+Install the optional dependencies when Wayland capture or DRM capture is enabled:
+```bash
+sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
+```
+
+Clone the submodules and build a release binary:
+
+```bash
+git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
-git submodule init
-
-git submodule update
+# Run this when the repository has already been cloned
+git submodule update --init --recursive
+xmake f -m release --USE_CUDA=false -y
xmake b -vy crossdesk
```
-Build options:
-```
---USE_CUDA=true/false: enable CUDA acceleration codec, default: false
---CROSSDESK_VERSION=xxx: set the version number
-# example:
+The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
+
+Example configuration with Wayland and DRM capture enabled:
+
+```bash
+xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
+xmake b -vy crossdesk
+```
+
+Build a Debian package after compiling a release binary on the matching architecture:
+
+```bash
+# amd64
+./scripts/linux/pkg_amd64.sh 1.0.0
+
+# arm64
+./scripts/linux/pkg_arm64.sh 1.0.0
+```
+
+The package scripts install the shared Slint runtime in the private `/usr/lib/crossdesk` directory, so users do not need to install `libslint_cpp.so` separately.
+
+### Common build options
+
+```text
+--USE_CUDA=true/false: Enable CUDA hardware codec acceleration, disabled by default
+--USE_WAYLAND=true/false: Enable Wayland/PipeWire capture on Linux, disabled by default
+--USE_DRM=true/false: Enable DRM capture on Linux, disabled by default
+--CROSSDESK_PORTABLE=true/false: Build the portable variant, disabled by default
+--CROSSDESK_VERSION=xxx: Set the CrossDesk version
+
+# Example
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
+
Run:
-```
+
+```bash
xmake r crossdesk
```
diff --git a/scripts/linux/package_deb.sh b/scripts/linux/package_deb.sh
new file mode 100755
index 0000000..c067281
--- /dev/null
+++ b/scripts/linux/package_deb.sh
@@ -0,0 +1,183 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ $# -lt 3 || $# -gt 4 ]]; then
+ echo "Usage: $0 [extra-recommendation]" >&2
+ exit 2
+fi
+
+DEBIAN_ARCH="$1"
+XMAKE_ARCH="$2"
+INPUT_VERSION="$3"
+EXTRA_RECOMMENDATION="${4:-}"
+
+PKG_NAME="crossdesk"
+APP_NAME="CrossDesk"
+MAINTAINER="Junkun Di "
+DESCRIPTION="A simple cross-platform remote desktop client."
+ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
+PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+
+normalize_app_version() {
+ local input="$1"
+ local prefix=""
+ local body="$input"
+
+ if [[ "$body" == v* ]]; then
+ prefix="v"
+ body="${body#v}"
+ fi
+
+ if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
+ echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
+ else
+ echo "$input"
+ fi
+}
+
+find_slint_runtime() {
+ local needed="$1"
+ local resolved=""
+
+ resolved="$(ldd "$BUILD_BINARY" 2>/dev/null | awk -v needed="$needed" \
+ '$1 == needed && $2 == "=>" && $3 ~ /^\// { print $3; exit }')"
+ if [[ -n "$resolved" && -f "$resolved" ]]; then
+ echo "$resolved"
+ return 0
+ fi
+
+ local package_roots=()
+ if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
+ package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
+ fi
+ package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
+
+ local package_root=""
+ local candidate=""
+ for package_root in "${package_roots[@]}"; do
+ [[ -d "$package_root/s/slint" ]] || continue
+ candidate="$(find "$package_root/s/slint" -path "*/lib/$needed" -print -quit 2>/dev/null || true)"
+ if [[ -n "$candidate" ]]; then
+ echo "$candidate"
+ return 0
+ fi
+ done
+
+ return 1
+}
+
+APP_VERSION="$(normalize_app_version "$INPUT_VERSION")"
+DEB_VERSION="${APP_VERSION#v}"
+if [[ ! "$DEB_VERSION" =~ ^[0-9][0-9A-Za-z.+:~_-]*$ ]]; then
+ echo "Invalid Debian version: $DEB_VERSION" >&2
+ exit 2
+fi
+
+BUILD_BINARY="$PROJECT_ROOT/build/linux/$XMAKE_ARCH/release/crossdesk"
+if [[ ! -x "$BUILD_BINARY" ]]; then
+ echo "Missing release binary: $BUILD_BINARY" >&2
+ echo "Build it first with: xmake f -m release -y && xmake b -vy crossdesk" >&2
+ exit 1
+fi
+
+for command_name in readelf ldd dpkg-deb; do
+ if ! command -v "$command_name" >/dev/null 2>&1; then
+ echo "Required packaging command is missing: $command_name" >&2
+ exit 1
+ fi
+done
+
+WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-deb.XXXXXX")"
+trap 'rm -rf "$WORK_DIR"' EXIT
+
+DEB_DIR="$WORK_DIR/${PKG_NAME}-${DEB_VERSION}"
+DEBIAN_DIR="$DEB_DIR/DEBIAN"
+BIN_DIR="$DEB_DIR/usr/bin"
+PRIVATE_DIR="$DEB_DIR/usr/lib/$PKG_NAME"
+ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
+DESKTOP_DIR="$DEB_DIR/usr/share/applications"
+OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
+
+install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
+install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
+
+# Slint is intentionally built as a shared library. Keep that runtime private
+# to CrossDesk instead of relying on a non-existent distribution package.
+SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
+ 's/.*Shared library: \[\(libslint_cpp[^]]*\)\].*/\1/p' | head -n 1)"
+if [[ -n "$SLINT_NEEDED" ]]; then
+ SLINT_LIBRARY="$(find_slint_runtime "$SLINT_NEEDED" || true)"
+ if [[ -z "$SLINT_LIBRARY" ]]; then
+ echo "Unable to locate the required Slint runtime: $SLINT_NEEDED" >&2
+ exit 1
+ fi
+ install -m 0755 "$SLINT_LIBRARY" "$PRIVATE_DIR/$SLINT_NEEDED"
+fi
+
+cat > "$BIN_DIR/$PKG_NAME" <<'EOF'
+#!/bin/sh
+private_lib_dir="/usr/lib/crossdesk"
+if [ -n "${LD_LIBRARY_PATH:-}" ]; then
+ export LD_LIBRARY_PATH="$private_lib_dir:$LD_LIBRARY_PATH"
+else
+ export LD_LIBRARY_PATH="$private_lib_dir"
+fi
+exec /usr/lib/crossdesk/crossdesk-bin "$@"
+EOF
+chmod 0755 "$BIN_DIR/$PKG_NAME"
+ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
+
+for size in 16 24 32 48 64 96 128 256; do
+ install -d "$ICON_BASE_DIR/${size}x${size}/apps"
+ install -m 0644 "$PROJECT_ROOT/icons/linux/crossdesk_${size}x${size}.png" \
+ "$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
+done
+
+RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS"
+if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
+ RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
+fi
+
+cat > "$DEBIAN_DIR/control" <= 2.29), libstdc++6 (>= 9), libx11-6, libxext6,
+ libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
+ libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
+ libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
+ libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
+Recommends: $RECOMMENDS
+Priority: optional
+Section: utils
+EOF
+
+cat > "$DESKTOP_DIR/$PKG_NAME.desktop" </dev/null
+dpkg-deb --contents "$OUTPUT_FILE" > "$WORK_DIR/package-contents.txt"
+
+if [[ -n "$SLINT_NEEDED" ]] && \
+ ! grep -q "usr/lib/$PKG_NAME/$SLINT_NEEDED" "$WORK_DIR/package-contents.txt"; then
+ echo "Packaged artifact is missing $SLINT_NEEDED" >&2
+ exit 1
+fi
+
+echo "Deb package created: $OUTPUT_FILE"
diff --git a/scripts/linux/pkg_amd64.sh b/scripts/linux/pkg_amd64.sh
old mode 100644
new mode 100755
index be9fab2..60b33b4
--- a/scripts/linux/pkg_amd64.sh
+++ b/scripts/linux/pkg_amd64.sh
@@ -1,116 +1,11 @@
-#!/bin/bash
-set -e
+#!/usr/bin/env bash
+set -euo pipefail
-PKG_NAME="crossdesk"
-APP_NAME="CrossDesk"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-ARCHITECTURE="amd64"
-MAINTAINER="Junkun Di "
-DESCRIPTION="A simple cross-platform remote desktop client."
-ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
-PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
-
-normalize_app_version() {
- local input="$1"
- local prefix=""
- local body="$input"
-
- if [[ "$body" == v* ]]; then
- prefix="v"
- body="${body#v}"
- fi
-
- if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
- echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
- else
- echo "$input"
- fi
-}
-
-APP_VERSION="$(normalize_app_version "$1")"
-
-# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
-DEB_VERSION="${APP_VERSION#v}"
-
-DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
-DEBIAN_DIR="$DEB_DIR/DEBIAN"
-BIN_DIR="$DEB_DIR/usr/bin"
-ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
-DESKTOP_DIR="$DEB_DIR/usr/share/applications"
-
-rm -rf "$DEB_DIR"
-
-mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
-
-cp build/linux/x86_64/release/crossdesk "$BIN_DIR/$PKG_NAME"
-chmod +x "$BIN_DIR/$PKG_NAME"
-
-ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
-
-for size in 16 24 32 48 64 96 128 256; do
- mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
- cp "icons/linux/crossdesk_${size}x${size}.png" \
- "$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
-done
-
-cat > "$DEBIAN_DIR/control" << EOF
-Package: $PKG_NAME
-Version: $DEB_VERSION
-Architecture: $ARCHITECTURE
-Maintainer: $MAINTAINER
-Description: $DESCRIPTION
-Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
- libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
- libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
- libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
-Recommends: $PORTAL_RUNTIME_RECOMMENDS, nvidia-cuda-toolkit
-Priority: optional
-Section: utils
-EOF
-
-cat > "$DESKTOP_DIR/$PKG_NAME.desktop" << EOF
-[Desktop Entry]
-Version=$DEB_VERSION
-Name=$APP_NAME
-Comment=$DESCRIPTION
-Exec=/usr/bin/$PKG_NAME
-Icon=$PKG_NAME
-Terminal=false
-Type=Application
-Categories=Utility;
-EOF
-
-cat > "$DEBIAN_DIR/postrm" << EOF
-#!/bin/bash
-set -e
-
-if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
- rm -f /usr/bin/$PKG_NAME || true
- rm -f /usr/bin/$APP_NAME || true
- rm -f /usr/share/applications/$PKG_NAME.desktop || true
- for size in 16 24 32 48 64 96 128 256; do
- rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
- done
+if [[ $# -ne 1 ]]; then
+ echo "Usage: $0 " >&2
+ exit 2
fi
-exit 0
-EOF
-chmod +x "$DEBIAN_DIR/postrm"
-
-cat > "$DEBIAN_DIR/postinst" << 'EOF'
-#!/bin/bash
-set -e
-
-exit 0
-EOF
-
-chmod +x "$DEBIAN_DIR/postinst"
-
-dpkg-deb --build "$DEB_DIR"
-
-OUTPUT_FILE="${PKG_NAME}-linux-${ARCHITECTURE}-${APP_VERSION}.deb"
-mv "$DEB_DIR.deb" "$OUTPUT_FILE"
-
-rm -rf "$DEB_DIR"
-
-echo "✅ Deb package created: $OUTPUT_FILE"
+exec bash "$SCRIPT_DIR/package_deb.sh" amd64 x86_64 "$1" nvidia-cuda-toolkit
diff --git a/scripts/linux/pkg_arm64.sh b/scripts/linux/pkg_arm64.sh
old mode 100644
new mode 100755
index ff97436..ab335e6
--- a/scripts/linux/pkg_arm64.sh
+++ b/scripts/linux/pkg_arm64.sh
@@ -1,116 +1,11 @@
-#!/bin/bash
-set -e
+#!/usr/bin/env bash
+set -euo pipefail
-PKG_NAME="crossdesk"
-APP_NAME="CrossDesk"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-ARCHITECTURE="arm64"
-MAINTAINER="Junkun Di "
-DESCRIPTION="A simple cross-platform remote desktop client."
-ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
-PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
-
-normalize_app_version() {
- local input="$1"
- local prefix=""
- local body="$input"
-
- if [[ "$body" == v* ]]; then
- prefix="v"
- body="${body#v}"
- fi
-
- if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
- echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
- else
- echo "$input"
- fi
-}
-
-APP_VERSION="$(normalize_app_version "$1")"
-
-# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
-DEB_VERSION="${APP_VERSION#v}"
-
-DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
-DEBIAN_DIR="$DEB_DIR/DEBIAN"
-BIN_DIR="$DEB_DIR/usr/bin"
-ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
-DESKTOP_DIR="$DEB_DIR/usr/share/applications"
-
-rm -rf "$DEB_DIR"
-
-mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
-
-cp build/linux/arm64/release/crossdesk "$BIN_DIR"
-chmod +x "$BIN_DIR/$PKG_NAME"
-
-ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
-
-for size in 16 24 32 48 64 96 128 256; do
- mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
- cp "icons/linux/crossdesk_${size}x${size}.png" \
- "$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
-done
-
-cat > "$DEBIAN_DIR/control" << EOF
-Package: $PKG_NAME
-Version: $DEB_VERSION
-Architecture: $ARCHITECTURE
-Maintainer: $MAINTAINER
-Description: $DESCRIPTION
-Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
- libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
- libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
- libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
-Recommends: $PORTAL_RUNTIME_RECOMMENDS
-Priority: optional
-Section: utils
-EOF
-
-cat > "$DESKTOP_DIR/$PKG_NAME.desktop" << EOF
-[Desktop Entry]
-Version=$DEB_VERSION
-Name=$APP_NAME
-Comment=$DESCRIPTION
-Exec=/usr/bin/$PKG_NAME
-Icon=$PKG_NAME
-Terminal=false
-Type=Application
-Categories=Utility;
-EOF
-
-cat > "$DEBIAN_DIR/postrm" << EOF
-#!/bin/bash
-set -e
-
-if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
- rm -f /usr/bin/$PKG_NAME || true
- rm -f /usr/bin/$APP_NAME || true
- rm -f /usr/share/applications/$PKG_NAME.desktop || true
- for size in 16 24 32 48 64 96 128 256; do
- rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
- done
+if [[ $# -ne 1 ]]; then
+ echo "Usage: $0 " >&2
+ exit 2
fi
-exit 0
-EOF
-chmod +x "$DEBIAN_DIR/postrm"
-
-cat > "$DEBIAN_DIR/postinst" << 'EOF'
-#!/bin/bash
-set -e
-
-exit 0
-EOF
-
-chmod +x "$DEBIAN_DIR/postinst"
-
-dpkg-deb --build "$DEB_DIR"
-
-OUTPUT_FILE="crossdesk-linux-arm64-$APP_VERSION.deb"
-mv "$DEB_DIR.deb" "$OUTPUT_FILE"
-
-rm -rf "$DEB_DIR"
-
-echo "✅ Deb package created: $OUTPUT_FILE"
+exec bash "$SCRIPT_DIR/package_deb.sh" arm64 arm64 "$1"
diff --git a/src/common/filesystem_utf8.h b/src/common/filesystem_utf8.h
new file mode 100644
index 0000000..8e31fef
--- /dev/null
+++ b/src/common/filesystem_utf8.h
@@ -0,0 +1,21 @@
+#ifndef CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
+#define CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
+
+#include
+#include
+#include
+
+namespace crossdesk {
+
+inline std::filesystem::path PathFromUtf8(std::string_view value) {
+#if defined(__cpp_char8_t)
+ const auto *begin = reinterpret_cast(value.data());
+ return std::filesystem::path(std::u8string(begin, begin + value.size()));
+#else
+ return std::filesystem::u8path(value.begin(), value.end());
+#endif
+}
+
+} // namespace crossdesk
+
+#endif // CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
diff --git a/src/gui/application/application_state.h b/src/gui/application/application_state.h
index b3dc83e..da63bad 100644
--- a/src/gui/application/application_state.h
+++ b/src/gui/application/application_state.h
@@ -1,24 +1,12 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
-#include
-
#include
#include
#include
#include
#include
-#include "imgui.h"
-
-#if _WIN32
-#include "platform/tray/win_tray.h"
-#elif defined(__APPLE__)
-#include "platform/tray/mac_tray.h"
-#elif defined(__linux__)
-#include "platform/tray/linux_tray.h"
-#endif
-
namespace crossdesk::gui_detail {
struct MainWindowState {
@@ -27,25 +15,10 @@ struct MainWindowState {
float title_bar_button_width_ = 30;
float title_bar_button_height_ = 30;
- SDL_Window *main_window_ = nullptr;
- SDL_Renderer *main_renderer_ = nullptr;
- ImGuiContext *main_ctx_ = nullptr;
- ImFont *main_windows_system_chinese_font_ = nullptr;
- ImFont *stream_windows_system_chinese_font_ = nullptr;
- ImFont *server_windows_system_chinese_font_ = nullptr;
bool exit_ = false;
const int sdl_refresh_ms_ = 16;
-#if _WIN32
- std::unique_ptr tray_;
-#elif defined(__APPLE__)
- std::unique_ptr tray_;
-#elif defined(__linux__)
- std::unique_ptr tray_;
-#endif
bool main_window_minimized_ = false;
- uint32_t last_main_minimize_request_tick_ = 0;
- uint32_t last_stream_minimize_request_tick_ = 0;
int main_window_width_real_ = 720;
int main_window_height_real_ = 540;
float main_window_dpi_scaling_w_ = 1.0f;
@@ -77,8 +50,6 @@ struct MainWindowState {
float about_window_height_ = 170;
float update_notification_window_width_ = 400;
float update_notification_window_height_ = 320;
- uint32_t STREAM_REFRESH_EVENT = 0;
- uint32_t APP_EXIT_EVENT = 0;
};
// Application-global interaction flags that coordinate SDL events with media
@@ -116,14 +87,15 @@ struct InteractionState {
std::string controlled_remote_id_;
std::string focused_remote_id_;
std::string remote_client_id_;
- SDL_Event last_mouse_event{};
};
struct UpdateState {
nlohmann::json latest_version_info_ = nlohmann::json{};
bool update_available_ = false;
std::string latest_version_;
+ std::string release_name_;
std::string release_notes_;
+ std::string release_date_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0;
@@ -131,20 +103,15 @@ struct UpdateState {
};
struct StreamWindowState {
- SDL_Window *stream_window_ = nullptr;
- SDL_Renderer *stream_renderer_ = nullptr;
- ImGuiContext *stream_ctx_ = nullptr;
bool need_to_create_stream_window_ = false;
bool stream_window_created_ = false;
bool stream_window_inited_ = false;
bool window_maximized_ = false;
- bool stream_window_grabbed_ = false;
bool control_mouse_ = false;
int stream_window_width_default_ = 1280;
int stream_window_height_default_ = 720;
float stream_window_width_ = 1280;
float stream_window_height_ = 720;
- SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12;
int stream_window_width_real_ = 1280;
int stream_window_height_real_ = 720;
float stream_window_dpi_scaling_w_ = 1.0f;
@@ -152,9 +119,6 @@ struct StreamWindowState {
};
struct ServerWindowState {
- SDL_Window *server_window_ = nullptr;
- SDL_Renderer *server_renderer_ = nullptr;
- ImGuiContext *server_ctx_ = nullptr;
bool need_to_create_server_window_ = false;
bool need_to_destroy_server_window_ = false;
bool server_window_created_ = false;
@@ -164,7 +128,6 @@ struct ServerWindowState {
float server_window_width_ = 250;
float server_window_height_ = 150;
float server_window_title_bar_height_ = 30.0f;
- SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
int server_window_normal_width_ = 250;
int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f;
diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp
index 844bc87..abe51a8 100644
--- a/src/gui/application/gui_application.cpp
+++ b/src/gui/application/gui_application.cpp
@@ -1,51 +1,692 @@
#include "application/gui_application.h"
-#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
#include
-#include
+#include
+#include
#include
#include
+#include
+#include
+#include
#include
+#include
+#include
+#include
#include
+#include "crossdesk_ui.h"
+#include "fa_solid_900.h"
#include "localization.h"
+#if _WIN32
+#include "platform/tray/win_tray.h"
+#elif defined(__APPLE__)
+#include "platform/tray/mac_tray.h"
+#include "platform/window_drag.h"
+#elif defined(__linux__)
+#include "platform/tray/linux_tray.h"
+#endif
#include "rd_log.h"
#include "version_checker.h"
namespace crossdesk {
+namespace {
+
+using namespace std::chrono_literals;
+
+#if _WIN32
+HICON LoadSlintTrayIcon() {
+ HMODULE module = GetModuleHandleW(nullptr);
+ HICON icon = reinterpret_cast(
+ LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
+ return icon ? icon : LoadIconW(nullptr, IDI_APPLICATION);
+}
+#endif
+
+slint::SharedString UiText(std::string_view value) {
+ return slint::SharedString(value);
+}
+
+void RegisterFontAwesome(slint::Window &window) {
+ if (const auto error = window.window_handle().register_font_from_data(
+ fa_solid_900_ttf, fa_solid_900_ttf_len)) {
+ LOG_ERROR("Failed to register Font Awesome for Slint window: {}",
+ error->data());
+ }
+}
+
+std::string FormatPeerId(const char *id) {
+ if (!id) {
+ return {};
+ }
+ std::string compact(id);
+ if (compact.size() != 9) {
+ return compact;
+ }
+ compact.insert(3, " ");
+ compact.insert(7, " ");
+ return compact;
+}
+
+std::string FormatRemotePeerIdInput(std::string_view input) {
+ std::string digits;
+ digits.reserve(9);
+ for (const unsigned char ch : input) {
+ if (std::isdigit(ch) != 0 && digits.size() < 9) {
+ digits.push_back(static_cast(ch));
+ }
+ }
+ if (digits.size() > 3) {
+ digits.insert(3, " ");
+ }
+ if (digits.size() > 7) {
+ digits.insert(7, " ");
+ }
+ return digits;
+}
+
+std::string CompactPeerId(std::string id) {
+ id.erase(std::remove_if(id.begin(), id.end(), [](unsigned char ch) {
+ return std::isspace(ch) != 0;
+ }),
+ id.end());
+ return id;
+}
+
+std::string Trim(std::string value) {
+ const auto first = std::find_if_not(value.begin(), value.end(),
+ [](unsigned char ch) {
+ return std::isspace(ch) != 0;
+ });
+ const auto last = std::find_if_not(value.rbegin(), value.rend(),
+ [](unsigned char ch) {
+ return std::isspace(ch) != 0;
+ })
+ .base();
+ if (first >= last) {
+ return {};
+ }
+ return std::string(first, last);
+}
+
+std::string CleanReleaseNotesForUi(std::string markdown) {
+ size_t line_start = 0;
+ while (line_start < markdown.size()) {
+ size_t content_start = line_start;
+ while (content_start < markdown.size() && markdown[content_start] == '#') {
+ ++content_start;
+ }
+ if (content_start > line_start && content_start < markdown.size() &&
+ markdown[content_start] == ' ') {
+ ++content_start;
+ }
+ if (content_start > line_start) {
+ markdown.erase(line_start, content_start - line_start);
+ }
+ const size_t newline = markdown.find('\n', line_start);
+ if (newline == std::string::npos) {
+ break;
+ }
+ line_start = newline + 1;
+ }
+ size_t marker = 0;
+ while ((marker = markdown.find("**", marker)) != std::string::npos) {
+ markdown.erase(marker, 2);
+ }
+ return markdown;
+}
+
+std::string DecodeDroppedPath(std::string text) {
+ const size_t line_end = text.find_first_of("\r\n");
+ if (line_end != std::string::npos) {
+ text.resize(line_end);
+ }
+ text = Trim(std::move(text));
+ if (text.rfind("file://localhost/", 0) == 0) {
+ text.erase(0, 16);
+ text.insert(text.begin(), '/');
+ } else if (text.rfind("file://", 0) == 0) {
+ text.erase(0, 7);
+ }
+
+ std::string decoded;
+ decoded.reserve(text.size());
+ auto hex_value = [](char value) -> int {
+ if (value >= '0' && value <= '9') return value - '0';
+ if (value >= 'a' && value <= 'f') return value - 'a' + 10;
+ if (value >= 'A' && value <= 'F') return value - 'A' + 10;
+ return -1;
+ };
+ for (size_t i = 0; i < text.size(); ++i) {
+ if (text[i] == '%' && i + 2 < text.size()) {
+ const int high = hex_value(text[i + 1]);
+ const int low = hex_value(text[i + 2]);
+ if (high >= 0 && low >= 0) {
+ decoded.push_back(static_cast((high << 4) | low));
+ i += 2;
+ continue;
+ }
+ }
+ decoded.push_back(text[i]);
+ }
+#if defined(_WIN32)
+ if (decoded.size() >= 3 && decoded[0] == '/' &&
+ std::isalpha(static_cast(decoded[1])) &&
+ decoded[2] == ':') {
+ decoded.erase(decoded.begin());
+ }
+#endif
+ return decoded;
+}
+
+std::optional ParsePort(const slint::SharedString &text) {
+ const std::string value(text);
+ int port = 0;
+ const auto [end, error] =
+ std::from_chars(value.data(), value.data() + value.size(), port);
+ if (error != std::errc{} || end != value.data() + value.size() || port <= 0 ||
+ port > 65535) {
+ return std::nullopt;
+ }
+ return port;
+}
+
+std::string ConnectionStatusText(ConnectionStatus status, int language) {
+ switch (status) {
+ case ConnectionStatus::Connected:
+ return localization::p2p_connected[language];
+ case ConnectionStatus::Connecting:
+ return localization::p2p_connecting[language];
+ case ConnectionStatus::Gathering:
+ return localization::p2p_gathering[language];
+ case ConnectionStatus::Disconnected:
+ return localization::p2p_disconnected[language];
+ case ConnectionStatus::NoSuchTransmissionId:
+ return localization::no_such_id[language];
+ case ConnectionStatus::IncorrectPassword:
+ return localization::reinput_password[language];
+ case ConnectionStatus::Closed:
+ return localization::p2p_closed[language];
+ case ConnectionStatus::RemoteUnavailable:
+ return localization::device_offline[language];
+ case ConnectionStatus::Failed:
+ default:
+ return localization::p2p_failed[language];
+ }
+}
+
+std::string FormatBitrate(uint32_t bits_per_second) {
+ char text[48];
+ if (bits_per_second < 1000) {
+ std::snprintf(text, sizeof(text), "%u bps", bits_per_second);
+ } else if (bits_per_second < 1000000) {
+ std::snprintf(text, sizeof(text), "%u kbps", bits_per_second / 1000);
+ } else {
+ std::snprintf(text, sizeof(text), "%.1f mbps",
+ bits_per_second / 1000000.0);
+ }
+ return text;
+}
+
+std::string FormatTransferRate(uint32_t bits_per_second) {
+ return bits_per_second == 0 ? std::string{} : FormatBitrate(bits_per_second);
+}
+
+std::string FormatLossRate(float loss_rate) {
+ if (loss_rate < 0.01f) {
+ return "0%";
+ }
+ char text[24];
+ std::snprintf(text, sizeof(text), "%.0f%%", loss_rate * 100.0f);
+ return text;
+}
+
+std::string FormatFileSize(uint64_t bytes) {
+ char text[48];
+ if (bytes < 1024) {
+ std::snprintf(text, sizeof(text), "%llu B",
+ static_cast(bytes));
+ } else if (bytes < 1024 * 1024) {
+ std::snprintf(text, sizeof(text), "%.2f KB", bytes / 1024.0);
+ } else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
+ std::snprintf(text, sizeof(text), "%.2f MB",
+ bytes / (1024.0 * 1024.0));
+ } else {
+ std::snprintf(text, sizeof(text), "%.2f GB",
+ bytes / (1024.0 * 1024.0 * 1024.0));
+ }
+ return text;
+}
+
+uint32_t DecodeFirstUtf8CodePoint(const std::string &text) {
+ if (text.empty()) {
+ return 0;
+ }
+ const auto *bytes = reinterpret_cast(text.data());
+ if (bytes[0] < 0x80) {
+ return bytes[0];
+ }
+ if ((bytes[0] & 0xE0) == 0xC0 && text.size() >= 2) {
+ return ((bytes[0] & 0x1F) << 6) | (bytes[1] & 0x3F);
+ }
+ if ((bytes[0] & 0xF0) == 0xE0 && text.size() >= 3) {
+ return ((bytes[0] & 0x0F) << 12) | ((bytes[1] & 0x3F) << 6) |
+ (bytes[2] & 0x3F);
+ }
+ if ((bytes[0] & 0xF8) == 0xF0 && text.size() >= 4) {
+ return ((bytes[0] & 0x07) << 18) | ((bytes[1] & 0x3F) << 12) |
+ ((bytes[2] & 0x3F) << 6) | (bytes[3] & 0x3F);
+ }
+ return 0;
+}
+
+int SlintKeyToWindowsVk(const std::string &text) {
+ const uint32_t key = DecodeFirstUtf8CodePoint(text);
+ if (key >= 'a' && key <= 'z') {
+ return static_cast(key - 'a' + 'A');
+ }
+ if ((key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
+ return static_cast(key);
+ }
+ if (key >= 0xF704 && key <= 0xF71B) {
+ return 0x70 + static_cast(key - 0xF704);
+ }
+
+ switch (key) {
+ case 0x08:
+ return 0x08;
+ case 0x09:
+ case 0x19:
+ return 0x09;
+ case 0x0A:
+ return 0x0D;
+ case 0x1B:
+ return 0x1B;
+ case 0x7F:
+ return 0x2E;
+ case 0x10:
+ return 0xA0;
+ case 0x15:
+ return 0xA1;
+ case 0x11:
+ return 0xA2;
+ case 0x16:
+ return 0xA3;
+ case 0x12:
+ return 0xA4;
+ case 0x13:
+ return 0xA5;
+ case 0x17:
+ return 0x5B;
+ case 0x18:
+ return 0x5C;
+ case 0x14:
+ return 0x14;
+ case 0x20:
+ return 0x20;
+ case 0xF700:
+ return 0x26;
+ case 0xF701:
+ return 0x28;
+ case 0xF702:
+ return 0x25;
+ case 0xF703:
+ return 0x27;
+ case 0xF727:
+ return 0x2D;
+ case 0xF729:
+ return 0x24;
+ case 0xF72B:
+ return 0x23;
+ case 0xF72C:
+ return 0x21;
+ case 0xF72D:
+ return 0x22;
+ case 0xF72F:
+ return 0x91;
+ case 0xF730:
+ return 0x13;
+ case 0xF731:
+ return 0x2C;
+ case 0xF734:
+ return 0xB2;
+ case 0xF735:
+ return 0x5D;
+ case 0xF748:
+ return 0xA6;
+ case ';':
+ case ':':
+ return 0xBA;
+ case '\'':
+ case '"':
+ return 0xDE;
+ case '`':
+ case '~':
+ return 0xC0;
+ case ',':
+ case '<':
+ return 0xBC;
+ case '.':
+ case '>':
+ return 0xBE;
+ case '/':
+ case '?':
+ return 0xBF;
+ case '\\':
+ case '|':
+ return 0xDC;
+ case '[':
+ case '{':
+ return 0xDB;
+ case ']':
+ case '}':
+ return 0xDD;
+ case '-':
+ case '_':
+ return 0xBD;
+ case '=':
+ case '+':
+ return 0xBB;
+ case '!':
+ return '1';
+ case '@':
+ return '2';
+ case '#':
+ return '3';
+ case '$':
+ return '4';
+ case '%':
+ return '5';
+ case '^':
+ return '6';
+ case '&':
+ return '7';
+ case '*':
+ return '8';
+ case '(':
+ return '9';
+ case ')':
+ return '0';
+ default:
+ return -1;
+ }
+}
+
+struct WindowDragState {
+ bool active = false;
+ bool use_global_pointer = false;
+ float pointer_start_x = 0;
+ float pointer_start_y = 0;
+ float last_local_x = 0;
+ float last_local_y = 0;
+ float global_to_physical_scale = 1;
+ slint::PhysicalPosition window_start;
+ slint::PhysicalPosition last_target;
+};
+
+bool CanUseGlobalPointerPosition() {
+ const char *driver = SDL_GetCurrentVideoDriver();
+ if (driver == nullptr) {
+ return false;
+ }
+#if defined(__linux__) && !defined(__APPLE__)
+ // Wayland deliberately does not expose global pointer coordinates or allow
+ // clients to position top-level windows.
+ return std::strcmp(driver, "wayland") != 0;
+#else
+ return true;
+#endif
+}
+
+float ServerWindowLogicalWidth(int language) {
+ return language == 0 ? 250.0f : language == 1 ? 330.0f : 430.0f;
+}
+
+float ServerWindowLogicalHeight(bool collapsed) {
+ return collapsed ? 30.0f : 150.0f;
+}
+
+constexpr float kStreamWindowLogicalWidth = 1280.0f;
+constexpr float kStreamWindowLogicalHeight = 720.0f;
+
+SDL_DisplayID DisplayForSlintWindow(const slint::Window &window) {
+ const float scale = std::max(window.scale_factor(), 0.01f);
+ const auto position = window.position();
+ const auto size = window.size();
+ const SDL_Point center{
+ static_cast(std::lround(position.x / scale +
+ size.width / (2.0f * scale))),
+ static_cast(std::lround(position.y / scale +
+ size.height / (2.0f * scale)))};
+ const SDL_DisplayID display = SDL_GetDisplayForPoint(¢er);
+ return display != 0 ? display : SDL_GetPrimaryDisplay();
+}
+
+bool PositionWindowAtCenter(slint::Window &window,
+ const slint::Window &anchor,
+ float logical_width, float logical_height) {
+ if (!CanUseGlobalPointerPosition()) {
+ return false;
+ }
+
+ const SDL_DisplayID display = DisplayForSlintWindow(anchor);
+ SDL_Rect usable_bounds{};
+ if (display == 0 ||
+ !SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
+ LOG_WARN("Unable to obtain usable display bounds for stream window: {}",
+ SDL_GetError());
+ return false;
+ }
+
+ const float target_x = static_cast(usable_bounds.x) +
+ (static_cast(usable_bounds.w) - logical_width) /
+ 2.0f;
+ const float target_y = static_cast(usable_bounds.y) +
+ (static_cast(usable_bounds.h) - logical_height) /
+ 2.0f;
+ window.set_position(slint::LogicalPosition(
+ slint::Point{target_x, target_y}));
+ return true;
+}
+
+bool PositionWindowAtBottomRight(slint::Window &window, float logical_width,
+ float logical_height) {
+ if (!CanUseGlobalPointerPosition()) {
+ return false;
+ }
+
+ SDL_DisplayID display = 0;
+ const float scale = std::max(window.scale_factor(), 0.01f);
+ const auto position = window.position();
+ const auto size = window.size();
+ const SDL_Point center{
+ static_cast(std::lround(position.x / scale +
+ size.width / (2.0f * scale))),
+ static_cast(std::lround(position.y / scale +
+ size.height / (2.0f * scale)))};
+ display = SDL_GetDisplayForPoint(¢er);
+ if (display == 0) {
+ display = SDL_GetPrimaryDisplay();
+ }
+
+ SDL_Rect usable_bounds{};
+ if (display == 0 ||
+ !SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
+ LOG_WARN("Unable to obtain usable display bounds for server window: {}",
+ SDL_GetError());
+ return false;
+ }
+
+ const float target_x =
+ static_cast(usable_bounds.x) +
+ std::max(0.0f, static_cast(usable_bounds.w) - logical_width);
+ const float target_y =
+ static_cast(usable_bounds.y) +
+ std::max(0.0f, static_cast(usable_bounds.h) - logical_height);
+ window.set_position(slint::LogicalPosition(
+ slint::Point{target_x, target_y}));
+ return true;
+}
+
+template
+void DragWindow(WindowHandle &component, int phase, float mouse_x,
+ float mouse_y, WindowDragState &state) {
+ if (phase == 1) {
+#if defined(__APPLE__)
+ // Let AppKit run the native move loop. This preserves macOS window
+ // snapping, Spaces, multi-display, and high-DPI behavior without manually
+ // updating the window position for every pointer event.
+ if (!StartNativeWindowDrag()) {
+ LOG_WARN("Unable to start native macOS window drag");
+ }
+ state.active = false;
+ return;
+#endif
+ auto &window = component->window();
+ state.active = true;
+ state.last_local_x = mouse_x;
+ state.last_local_y = mouse_y;
+ state.window_start = window.position();
+ state.last_target = state.window_start;
+ state.use_global_pointer = CanUseGlobalPointerPosition();
+ if (state.use_global_pointer) {
+ SDL_GetGlobalMouseState(&state.pointer_start_x, &state.pointer_start_y);
+#if defined(__APPLE__)
+ // Cocoa reports global pointer positions in logical screen points,
+ // while Slint/winit accepts physical window positions.
+ state.global_to_physical_scale = window.scale_factor();
+#else
+ state.global_to_physical_scale = 1.0f;
+#endif
+ }
+ return;
+ }
+ if (phase == 0) {
+ state.active = false;
+ return;
+ }
+ if (!state.active || phase != 2) {
+ return;
+ }
+
+ auto &window = component->window();
+ if (state.use_global_pointer) {
+ float pointer_x = 0;
+ float pointer_y = 0;
+ SDL_GetGlobalMouseState(&pointer_x, &pointer_y);
+ const int delta_x = static_cast(std::lround(
+ (pointer_x - state.pointer_start_x) *
+ state.global_to_physical_scale));
+ const int delta_y = static_cast(std::lround(
+ (pointer_y - state.pointer_start_y) *
+ state.global_to_physical_scale));
+ const slint::PhysicalPosition target(slint::Point{
+ state.window_start.x + delta_x, state.window_start.y + delta_y});
+ if (target.x != state.last_target.x || target.y != state.last_target.y) {
+ window.set_position(target);
+ state.last_target = target;
+ }
+ return;
+ }
+
+ // Retain the previous behavior for backends without global pointer access.
+ const float scale = window.scale_factor();
+ const auto position = window.position();
+ const int delta_x = static_cast(
+ std::lround((mouse_x - state.last_local_x) * scale));
+ const int delta_y = static_cast(
+ std::lround((mouse_y - state.last_local_y) * scale));
+ if (delta_x != 0 || delta_y != 0) {
+ window.set_position(
+ slint::PhysicalPosition(slint::Point{
+ position.x + delta_x, position.y + delta_y}));
+ }
+ state.last_local_x = mouse_x;
+ state.last_local_y = mouse_y;
+}
+
+} // namespace
+
+struct GuiApplication::SlintUi {
+ slint::ComponentHandle main = ui::MainWindow::create();
+ std::optional> stream;
+ std::optional> server;
+ std::shared_ptr> recent_model =
+ std::make_shared>();
+ std::shared_ptr> tab_model =
+ std::make_shared>();
+ std::shared_ptr> display_model =
+ std::make_shared>();
+ std::shared_ptr> transfer_model =
+ std::make_shared>();
+ std::shared_ptr> stats_model =
+ std::make_shared>();
+ std::shared_ptr> controller_model =
+ std::make_shared>();
+ std::shared_ptr> controller_name_model =
+ std::make_shared>();
+ std::unordered_map displayed_frame_sequence;
+ std::vector tab_order;
+ std::vector tab_ids;
+ std::string tab_model_signature;
+ std::vector controller_ids;
+ std::string connection_dialog_remote_id;
+ bool connection_password_initialized = false;
+ bool portable_service_dialog_initialized = false;
+ std::string recent_model_signature;
+ slint::Timer timer;
+ WindowDragState server_drag;
+ int main_native_titlebar_attempts = 30;
+ int stream_live_resize_configuration_attempts = 0;
+ int stream_initial_position_attempts = 0;
+ int server_initial_position_attempts = 0;
+ int localized_language = -1;
+ bool capture_mode = false;
+ std::string capture_page;
+ std::chrono::steady_clock::time_point last_clipboard_poll{};
+#if _WIN32
+ std::unique_ptr tray;
+#elif defined(__APPLE__)
+ std::unique_ptr tray;
+#elif defined(__linux__)
+ std::unique_ptr tray;
+#endif
+};
GuiApplication::GuiApplication() = default;
-
GuiApplication::~GuiApplication() = default;
int GuiApplication::Run() {
path_manager_ = std::make_unique("CrossDesk");
- if (path_manager_) {
- exec_log_path_ = path_manager_->GetLogPath().string();
- dll_log_path_ = path_manager_->GetLogPath().string();
- cache_path_ = path_manager_->GetCachePath().string();
- config_center_ =
- std::make_unique(cache_path_ + "/config.ini");
- strncpy(signal_server_ip_self_,
- config_center_->GetSignalServerHost().c_str(),
- sizeof(signal_server_ip_self_) - 1);
- signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
- int signal_port_init = config_center_->GetSignalServerPort();
- if (signal_port_init > 0) {
- strncpy(signal_server_port_self_,
- std::to_string(signal_port_init).c_str(),
- sizeof(signal_server_port_self_) - 1);
- signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
- } else {
- signal_server_port_self_[0] = '\0';
- }
- } else {
- std::cerr << "Failed to create PathManager" << std::endl;
+ if (!path_manager_) {
return -1;
}
+ exec_log_path_ = path_manager_->GetLogPath().string();
+ dll_log_path_ = exec_log_path_;
+ cache_path_ = path_manager_->GetCachePath().string();
+ config_center_ =
+ std::make_unique(cache_path_ + "/config.ini");
InitializeLogger();
- LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION);
+ LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
+
+ strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
+ sizeof(signal_server_ip_self_) - 1);
+ const int signal_port = config_center_->GetSignalServerPort();
+ if (signal_port > 0) {
+ std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_),
+ "%d", signal_port);
+ }
latest_version_info_ = CheckUpdate();
if (!latest_version_info_.empty()) {
@@ -57,48 +698,45 @@ int GuiApplication::Run() {
latest_version_info_["version"].is_string()) {
version = latest_version_info_["version"].get();
}
-
- if (!version.empty()) {
- latest_version_ = 'v' + version;
- } else {
- latest_version_ = "";
- }
+ latest_version_ = version.empty() ? std::string{} : "v" + version;
if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) {
- release_notes_ = latest_version_info_["releaseNotes"].get();
- } else {
- release_notes_ = "";
+ release_notes_ =
+ latest_version_info_["releaseNotes"].get();
+ }
+ if (latest_version_info_.contains("releaseName") &&
+ latest_version_info_["releaseName"].is_string()) {
+ release_name_ = latest_version_info_["releaseName"].get();
+ }
+ if (latest_version_info_.contains("releaseDate") &&
+ latest_version_info_["releaseDate"].is_string()) {
+ release_date_ = latest_version_info_["releaseDate"].get();
}
update_available_ =
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
- LOG_INFO("Update check: current={}, latest={}, available={}",
- CROSSDESK_VERSION, latest_version_, update_available_);
- if (update_available_) {
- show_update_notification_window_ = true;
- }
- } else {
- latest_version_ = "";
- update_available_ = false;
- LOG_WARN("Update check skipped: version.json is empty or missing "
- "latest_version");
}
InitializeSettings();
- InitializeSDL();
+ if (!InitializeSDL()) {
+ return -1;
+ }
InitializeModules();
- InitializeMainWindow();
-
-#if _WIN32 && CROSSDESK_PORTABLE
- CheckPortableWindowsService();
-#endif
-
- const int scaled_video_width_ = 160;
- const int scaled_video_height_ = 90;
-
- MainLoop();
+ InitializeUi();
+ ui_->timer.start(slint::TimerMode::Repeated, 16ms, [this] { Tick(); });
+ if (ui_->capture_mode &&
+ (ui_->capture_page == "stream" || ui_->capture_page == "server")) {
+ slint::run_event_loop();
+ } else if (ui_->capture_mode) {
+ ui_->main->run();
+ } else {
+ // The native tray icon is managed outside Slint, so Slint cannot count it
+ // when deciding whether the last hidden window should stop the event loop.
+ // Keep the loop alive until the tray's explicit Exit action requests quit.
+ ui_->main->show();
+ slint::run_event_loop(slint::EventLoopMode::RunUntilQuit);
+ }
Cleanup();
-
return 0;
}
@@ -107,189 +745,1926 @@ void GuiApplication::InitializeLogger() { InitLogger(exec_log_path_); }
void GuiApplication::InitializeSettings() {
settings_.Load();
settings_.LoadRecentConnectionAliases();
-
localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_);
language_button_value_ = localization_language_index_;
-
- if (localization_language_index_ == 0) {
- localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
- } else if (localization_language_index_ == 1) {
- localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
- } else {
- localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
- }
+ localization_language_ =
+ static_cast(localization_language_index_);
}
-void GuiApplication::InitializeSDL() {
+bool GuiApplication::InitializeSDL() {
#if defined(__linux__) && !defined(__APPLE__)
if (!getenv("SDL_AUDIODRIVER")) {
- // Prefer PulseAudio first on Linux to avoid hard ALSA plugin dependency.
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
}
#endif
-
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
- LOG_ERROR("Error: {}", SDL_GetError());
- return;
+ LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
+ return false;
}
- const SDL_DisplayMode *dm = SDL_GetCurrentDisplayMode(0);
- if (dm) {
- screen_width_ = dm->w;
- screen_height_ = dm->h;
+ if (const SDL_DisplayMode *mode = SDL_GetCurrentDisplayMode(0)) {
+ screen_width_ = mode->w;
+ screen_height_ = mode->h;
}
-
- const uint32_t custom_event_base = SDL_RegisterEvents(3);
- if (custom_event_base == static_cast(-1)) {
- LOG_ERROR("Failed to register custom SDL events");
- } else {
- STREAM_REFRESH_EVENT = custom_event_base;
- APP_EXIT_EVENT = custom_event_base + 1;
- clipboard_.SetEventType(custom_event_base + 2);
- }
-
- LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_);
+ clipboard_.Initialize();
+ return true;
}
void GuiApplication::InitializeModules() {
- if (!modules_inited_) {
- devices_.Initialize();
- CreateConnectionPeer();
+ if (modules_inited_) {
+ return;
+ }
+ devices_.Initialize();
+ CreateConnectionPeer();
+ modules_inited_ = true;
+}
- modules_inited_ = true;
+void GuiApplication::InitializeUi() {
+ ui_ = std::make_unique();
+ RegisterFontAwesome(ui_->main->window());
+#ifdef CROSSDESK_DEBUG
+ auto read_capture_override = [](const char *path) -> std::string {
+ std::ifstream input(path);
+ std::string value;
+ std::getline(input, value);
+ return value;
+ };
+ if (const char *capture = std::getenv("CROSSDESK_UI_CAPTURE")) {
+ ui_->capture_mode = std::string_view(capture) != "0";
+ }
+ if (const char *page = std::getenv("CROSSDESK_UI_CAPTURE_PAGE")) {
+ ui_->capture_page = page;
+ }
+ std::string capture_language_override;
+ if (ui_->capture_mode) {
+ if (const auto page = read_capture_override(
+ "/tmp/crossdesk-ui-capture-page");
+ !page.empty()) {
+ ui_->capture_page = page;
+ }
+ capture_language_override = read_capture_override(
+ "/tmp/crossdesk-ui-capture-language");
+ }
+ const char *language = capture_language_override.empty()
+ ? std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE")
+ : capture_language_override.c_str();
+ if (language) {
+ int capture_language = 0;
+ const auto [end, error] = std::from_chars(
+ language, language + std::strlen(language), capture_language);
+ if (error == std::errc{} && end == language + std::strlen(language)) {
+ localization_language_index_ =
+ localization::detail::ClampLanguageIndex(capture_language);
+ language_button_value_ = localization_language_index_;
+ localization_language_ = static_cast(
+ localization_language_index_);
+ }
+ }
+#endif
+ ui_->main->set_recent_connections(ui_->recent_model);
+ ResetSettingsUi();
+ BindMainCallbacks();
+ InitializeSystemTray();
+ UpdateLocalization();
+ if (ui_->capture_mode) {
+ const bool detected_update = update_available_;
+ update_available_ = false;
+ if (ui_->capture_page == "settings") {
+ ui_->main->set_settings_open(true);
+ } else if (ui_->capture_page == "self-hosted") {
+ ui_->main->set_settings_open(true);
+ ui_->main->set_self_host_settings_open(true);
+ } else if (ui_->capture_page == "about") {
+ update_available_ = detected_update;
+ ui_->main->set_update_open(false);
+ ui_->main->set_about_open(true);
+ } else if (ui_->capture_page == "update") {
+ update_available_ = detected_update;
+ ui_->main->set_update_open(true);
+ } else if (ui_->capture_page == "reset-password") {
+ ui_->main->set_reset_password_open(true);
+ } else if (ui_->capture_page == "reset-password-invalid") {
+ ui_->main->set_reset_password_open(true);
+ ui_->main->set_reset_password_invalid(true);
+ ui_->main->set_new_password_input("123");
+ } else if (ui_->capture_page == "alias") {
+ ui_->main->set_alias_input("Office workstation");
+ ui_->main->set_alias_open(true);
+ } else if (ui_->capture_page == "delete") {
+ ui_->main->set_delete_open(true);
+ } else if (ui_->capture_page == "offline") {
+ ui_->main->set_offline_warning(
+ UiText(localization::device_offline[localization_language_index_]));
+ } else if (ui_->capture_page == "connection") {
+ ui_->main->set_connection_status_text(
+ UiText(localization::p2p_connecting[localization_language_index_]));
+ ui_->main->set_connection_pending(true);
+ ui_->main->set_connection_dialog_open(true);
+ } else if (ui_->capture_page == "connection-password") {
+ ui_->main->set_connection_status_text(
+ UiText(localization::reinput_password[localization_language_index_]));
+ ui_->main->set_connection_password_required(true);
+ ui_->main->set_connection_dialog_open(true);
+ } else if (ui_->capture_page == "service") {
+ ui_->main->set_portable_service_dialog_open(true);
+ } else if (ui_->capture_page == "service-notice") {
+ ui_->main->set_portable_service_suppressed_notice_open(true);
+ }
+ }
+ SyncMainWindow();
+
+ if (ui_->capture_mode && ui_->capture_page == "stream") {
+ ui_->stream.emplace(ui::StreamWindow::create());
+ RegisterFontAwesome((*ui_->stream)->window());
+ (*ui_->stream)->set_tabs(ui_->tab_model);
+ (*ui_->stream)->set_displays(ui_->display_model);
+ (*ui_->stream)->set_file_transfers(ui_->transfer_model);
+ (*ui_->stream)->set_stats_rows(ui_->stats_model);
+ ui::StreamTab preview_tab;
+ preview_tab.remote_id = "589173341";
+ preview_tab.title = "Mac";
+ preview_tab.connected = true;
+ ui_->tab_model->set_vector({preview_tab});
+ ui_->display_model->set_vector({slint::SharedString("Display 1")});
+ ui_->localized_language = -1;
+ UpdateLocalization();
+ (*ui_->stream)->set_status_text(
+ UiText(localization::p2p_connected[localization_language_index_]));
+#ifdef CROSSDESK_DEBUG
+ // A bordered 16:9 frame makes stream scaling/cropping regressions visible
+ // in the existing capture mode without requiring a live remote session.
+ constexpr int preview_width = 640;
+ constexpr int preview_height = 360;
+ slint::SharedPixelBuffer preview_pixels(preview_width,
+ preview_height);
+ auto *preview_data =
+ reinterpret_cast(preview_pixels.begin());
+ for (int y = 0; y < preview_height; ++y) {
+ for (int x = 0; x < preview_width; ++x) {
+ const bool border = x < 8 || y < 8 || x >= preview_width - 8 ||
+ y >= preview_height - 8;
+ const size_t offset =
+ (static_cast(y) * preview_width + x) * 3;
+ preview_data[offset] = border ? 240 : static_cast(
+ 35 + 150 * x /
+ preview_width);
+ preview_data[offset + 1] = border ? 70 : static_cast(
+ 35 + 150 * y /
+ preview_height);
+ preview_data[offset + 2] = border ? 70 : 90;
+ }
+ }
+ (*ui_->stream)->set_frame(slint::Image(std::move(preview_pixels)));
+ (*ui_->stream)->set_has_frame(true);
+ (*ui_->stream)->set_status_text("");
+#endif
+ BindStreamCallbacks();
+ (*ui_->stream)->show();
+ PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
+ kStreamWindowLogicalWidth,
+ kStreamWindowLogicalHeight);
+ ui_->main->hide();
+ } else if (ui_->capture_mode && ui_->capture_page == "server") {
+ ui_->server.emplace(ui::ServerWindow::create());
+ RegisterFontAwesome((*ui_->server)->window());
+ (*ui_->server)->set_controllers(ui_->controller_model);
+ (*ui_->server)->set_controller_names(ui_->controller_name_model);
+ (*ui_->server)->set_language_index(localization_language_index_);
+ const float server_width =
+ ServerWindowLogicalWidth(localization_language_index_);
+ const float server_height = ServerWindowLogicalHeight(false);
+ (*ui_->server)->window().set_size(slint::LogicalSize(
+ slint::Size{server_width, server_height}));
+ ui::ControllerEntry preview_controller;
+ preview_controller.remote_id = "589173341";
+ preview_controller.display_name = "Mac";
+ ui_->controller_model->set_vector({preview_controller});
+ ui_->controller_name_model->set_vector({slint::SharedString("Mac")});
+ (*ui_->server)->set_controller_label(
+ UiText(localization::controller[localization_language_index_]));
+ (*ui_->server)->set_connection_label(
+ UiText(localization::connection_status[localization_language_index_]));
+ (*ui_->server)->set_connection_status(
+ UiText(localization::p2p_connected[localization_language_index_]));
+ (*ui_->server)->set_file_transfer_label(
+ UiText(localization::file_transfer[localization_language_index_]));
+ (*ui_->server)->set_select_file_label(
+ UiText(localization::select_file[localization_language_index_]));
+ BindServerCallbacks();
+ (*ui_->server)->show();
+ PositionWindowAtBottomRight((*ui_->server)->window(), server_width,
+ server_height);
+ ui_->main->hide();
}
}
-void GuiApplication::InitializeMainWindow() {
- CreateMainWindow();
- clipboard_.Initialize();
- if (SDL_WINDOW_HIDDEN & SDL_GetWindowFlags(main_window_)) {
- SDL_ShowWindow(main_window_);
+void GuiApplication::InitializeSystemTray() {
+#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
+ if (!ui_) {
+ return;
}
-}
-
-void GuiApplication::MainLoop() {
- while (!exit_) {
- if (!peer_) {
- CreateConnectionPeer();
+ auto show_window = [this] {
+ if (!ui_) {
+ return;
}
-
- SDL_Event event;
- if (SDL_WaitEventTimeout(&event, sdl_refresh_ms_)) {
- ProcessSdlEvent(event);
+ ui_->main->show();
+ ui_->main->window().set_minimized(false);
+#if defined(__APPLE__)
+ // A window restored from the tray may only obtain its NSWindow after the
+ // callback returns, so let the regular UI tick configure it.
+ ui_->main_native_titlebar_attempts = 30;
+#endif
+ };
+ auto hide_window = [this] {
+ if (ui_) {
+ ui_->main->hide();
}
- // Also drain the pending value after a timeout so a full SDL event queue
- // cannot indefinitely delay a clipboard update received from the network.
- clipboard_.ApplyPendingRemoteText();
-
+ };
+ auto open_settings = [this] {
+ if (!ui_) {
+ return;
+ }
+ ui_->main->show();
+ ui_->main->window().set_minimized(false);
+ ui_->main->set_settings_open(true);
+#if defined(__APPLE__)
+ ui_->main_native_titlebar_attempts = 30;
+#endif
+ };
+ auto exit_app = [this] {
+ exit_ = true;
+ slint::quit_event_loop();
+ };
#if _WIN32
- MSG msg;
- while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
+ ui_->tray = std::make_unique(
+ std::move(show_window), std::move(hide_window), std::move(exit_app),
+ LoadSlintTrayIcon(), L"CrossDesk", localization_language_index_);
+#elif defined(__APPLE__)
+ ui_->tray = std::make_unique(
+ std::move(show_window), std::move(hide_window),
+ std::move(open_settings), std::move(exit_app), "CrossDesk",
+ localization_language_index_);
+#elif defined(__linux__)
+ ui_->tray = std::make_unique(
+ std::move(show_window), std::move(hide_window), std::move(exit_app),
+ "CrossDesk", localization_language_index_);
+#endif
+#endif
+}
+
+bool GuiApplication::MinimizeMainWindowToTray() {
+ if (!enable_minimize_to_tray_ || !ui_) {
+ return false;
+ }
+#if _WIN32 || defined(__APPLE__)
+ if (!ui_->tray) {
+ return false;
+ }
+ ui_->tray->MinimizeToTray();
+ return true;
+#elif defined(__linux__)
+ return ui_->tray && ui_->tray->MinimizeToTray();
+#else
+ return false;
+#endif
+}
+
+void GuiApplication::ResetSettingsUi() {
+ if (!ui_) {
+ return;
+ }
+ ui_->main->set_language_index(language_button_value_);
+ ui_->main->set_video_quality_index(video_quality_button_value_);
+ ui_->main->set_frame_rate_index(video_frame_rate_button_value_);
+ ui_->main->set_codec_index(video_encode_format_button_value_);
+ ui_->main->set_hardware_codec_enabled(enable_hardware_video_codec_);
+ ui_->main->set_turn_enabled(enable_turn_);
+ ui_->main->set_srtp_enabled(enable_srtp_);
+ ui_->main->set_self_hosted_enabled(enable_self_hosted_);
+ ui_->main->set_autostart_enabled(enable_autostart_);
+ ui_->main->set_daemon_enabled(enable_daemon_);
+ ui_->main->set_minimize_to_tray_enabled(enable_minimize_to_tray_);
+ ui_->main->set_file_save_path(file_transfer_save_path_buf_);
+ ui_->main->set_server_host(signal_server_ip_self_);
+ ui_->main->set_server_port(signal_server_port_self_);
+ ui_->main->set_coturn_port(coturn_server_port_self_);
+}
+
+void GuiApplication::BindMainCallbacks() {
+ auto &main = ui_->main;
+ main->window().on_close_requested([this] {
+ if (MinimizeMainWindowToTray()) {
+ // MinimizeMainWindowToTray() ensures the tray icon exists. Let Slint
+ // complete the close request by hiding the main window as well.
+ return slint::CloseRequestResponse::HideWindow;
}
-#elif defined(__linux__) && !defined(__APPLE__)
- if (tray_) {
- tray_->ProcessEvents();
+ exit_ = true;
+ return slint::CloseRequestResponse::HideWindow;
+ });
+ main->on_copy_local_id([this] {
+ if (!SDL_SetClipboardText(client_id_)) {
+ LOG_WARN("Copy local id failed: {}", SDL_GetError());
}
+ });
+ main->on_toggle_password_visibility([this] {
+ show_password_ = !show_password_;
+ ui_->main->set_password_visible(show_password_);
+ });
+ main->on_reset_password([this](slint::SharedString value) {
+ const std::string password(value);
+ if (password.size() != 6) {
+ return false;
+ }
+ std::memset(password_saved_, 0, sizeof(password_saved_));
+ std::strncpy(password_saved_, password.c_str(),
+ sizeof(password_saved_) - 1);
+
+ if (config_center_->IsSelfHosted()) {
+ std::string identity = self_hosted_id_;
+ if (const auto at = identity.find('@'); at != std::string::npos) {
+ identity.resize(at);
+ }
+ if (identity.empty()) {
+ identity = client_id_;
+ }
+ identity += "@" + password;
+ std::memset(self_hosted_id_, 0, sizeof(self_hosted_id_));
+ std::strncpy(self_hosted_id_, identity.c_str(),
+ sizeof(self_hosted_id_) - 1);
+ } else {
+ const std::string identity = std::string(client_id_) + "@" + password;
+ std::memset(client_id_with_password_, 0,
+ sizeof(client_id_with_password_));
+ std::strncpy(client_id_with_password_, identity.c_str(),
+ sizeof(client_id_with_password_) - 1);
+ }
+ settings_.Save();
+ if (peer_) {
+ LeaveConnection(peer_, client_id_);
+ DestroyPeer(&peer_);
+ }
+ return true;
+ });
+ main->on_connect_requested([this](slint::SharedString id) {
+ ConnectFromUi(std::string(id));
+ });
+ main->on_format_remote_id([](slint::SharedString value) {
+ return UiText(FormatRemotePeerIdInput(std::string_view(value)));
+ });
+ main->on_recent_connect([this](slint::SharedString id) {
+ ConnectFromUi(std::string(id));
+ });
+ main->on_recent_edit_alias(
+ [this](slint::SharedString id, slint::SharedString alias) {
+ const std::string remote_id(id);
+ const std::string trimmed = Trim(std::string(alias));
+ if (trimmed.empty()) {
+ settings_.EraseRecentConnectionAlias(remote_id);
+ } else {
+ settings_.SetRecentConnectionAlias(remote_id, trimmed);
+ }
+ settings_.SaveRecentConnectionAliases();
+ reload_recent_connections_ = true;
+ });
+ main->on_recent_delete([this](slint::SharedString id) {
+ const std::string remote_id(id);
+ const auto connection = std::find_if(
+ recent_connections_.begin(), recent_connections_.end(),
+ [&](const auto &entry) { return entry.second.remote_id == remote_id; });
+ if (connection != recent_connections_.end()) {
+ thumbnail_->DeleteThumbnail(connection->first);
+ settings_.EraseRecentConnectionAlias(remote_id);
+ settings_.SaveRecentConnectionAliases();
+ reload_recent_connections_ = true;
+ }
+ });
+ main->on_browse_save_path([this] {
+ const char *folder = tinyfd_selectFolderDialog(
+ localization::file_transfer_save_path[localization_language_index_]
+ .c_str(),
+ file_transfer_save_path_buf_[0] ? file_transfer_save_path_buf_ : nullptr);
+ if (folder) {
+ ui_->main->set_file_save_path(folder);
+ }
+ });
+ main->on_save_settings([this] { SaveSettingsFromUi(); });
+ main->on_cancel_settings([this] { ResetSettingsUi(); });
+ main->on_save_self_hosted_settings([this] {
+ const std::string host(ui_->main->get_server_host());
+ const auto signal_port = ParsePort(ui_->main->get_server_port());
+ const auto coturn_port = ParsePort(ui_->main->get_coturn_port());
+ if (!host.empty()) {
+ config_center_->SetServerHost(host);
+ std::memset(signal_server_ip_self_, 0, sizeof(signal_server_ip_self_));
+ std::strncpy(signal_server_ip_self_, host.c_str(),
+ sizeof(signal_server_ip_self_) - 1);
+ std::memset(signal_server_ip_, 0, sizeof(signal_server_ip_));
+ std::strncpy(signal_server_ip_, host.c_str(),
+ sizeof(signal_server_ip_) - 1);
+ }
+ if (signal_port) {
+ config_center_->SetServerPort(*signal_port);
+ std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_),
+ "%d", *signal_port);
+ std::snprintf(signal_server_port_, sizeof(signal_server_port_), "%d",
+ *signal_port);
+ }
+ if (coturn_port) {
+ config_center_->SetCoturnServerPort(*coturn_port);
+ std::snprintf(coturn_server_port_self_,
+ sizeof(coturn_server_port_self_), "%d", *coturn_port);
+ std::snprintf(coturn_server_port_, sizeof(coturn_server_port_), "%d",
+ *coturn_port);
+ }
+ });
+ main->on_cancel_self_hosted_settings([this] {
+ const std::string host = config_center_->GetSignalServerHost();
+ ui_->main->set_server_host(UiText(host));
+ const int signal_port = config_center_->GetSignalServerPort();
+ const int coturn_port = config_center_->GetCoturnServerPort();
+ ui_->main->set_server_port(
+ signal_port > 0 ? UiText(std::to_string(signal_port))
+ : slint::SharedString{});
+ ui_->main->set_coturn_port(
+ coturn_port > 0 ? UiText(std::to_string(coturn_port))
+ : slint::SharedString{});
+ });
+ main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
+ main->on_connection_cancel([this] {
+ const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
+ if (!props) {
+ show_connection_status_window_ = false;
+ return;
+ }
+ const ConnectionStatus status = props->connection_status_.load();
+ if (status == ConnectionStatus::IncorrectPassword) {
+ std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
+ password_validating_ = false;
+ } else {
+ CloseStreamTab(props->remote_id_);
+ re_enter_remote_id_ = true;
+ }
+ show_connection_status_window_ = false;
+ });
+ main->on_connection_acknowledge([this] {
+ const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
+ if (props) {
+ const ConnectionStatus status = props->connection_status_.load();
+ if (status == ConnectionStatus::NoSuchTransmissionId ||
+ status == ConnectionStatus::RemoteUnavailable) {
+ CloseStreamTab(props->remote_id_);
+ re_enter_remote_id_ = true;
+ }
+ }
+ show_connection_status_window_ = false;
+ });
+ main->on_connection_submit_password(
+ [this](slint::SharedString value, bool remember) {
+ const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
+ const std::string password(value);
+ if (!props || password.empty() || password.size() > 6) {
+ return;
+ }
+ std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
+ std::memcpy(props->remote_password_, password.data(), password.size());
+ props->remember_password_ = remember;
+ props->rejoin_ = true;
+ password_validating_ = true;
+ need_to_rejoin_ = true;
+ });
+ main->on_request_screen_recording_permission([this] {
+#ifdef __APPLE__
+ OpenScreenRecordingPreferences();
+ mac_screen_recording_permission_requested_ = true;
+ RefreshMacPermissionStatus(true);
+#endif
+ });
+ main->on_request_accessibility_permission([this] {
+#ifdef __APPLE__
+ OpenAccessibilityPreferences();
+ mac_accessibility_permission_requested_ = true;
+ RefreshMacPermissionStatus(true);
+#endif
+ });
+ main->on_install_portable_service([this] {
+#if _WIN32 && CROSSDESK_PORTABLE
+ portable_service_do_not_remind_ = false;
+ ui_->main->set_portable_service_do_not_remind(false);
+ StartPortableWindowsServiceInstall();
+#endif
+ });
+ main->on_cancel_portable_service([this] {
+#if _WIN32 && CROSSDESK_PORTABLE
+ if (portable_service_install_state_.load(std::memory_order_acquire) ==
+ PortableServiceInstallState::installing) {
+ return;
+ }
+ portable_service_do_not_remind_ =
+ ui_->main->get_portable_service_do_not_remind();
+ if (portable_service_do_not_remind_) {
+ portable_service_prompt_suppressed_ = true;
+ config_center_->SetPortableServicePromptSuppressed(true);
+ show_portable_service_prompt_suppressed_window_ = true;
+ }
+ show_portable_service_install_window_ = false;
+#endif
+ });
+ main->on_acknowledge_portable_service([this] {
+#if _WIN32 && CROSSDESK_PORTABLE
+ show_portable_service_install_window_ = false;
+ portable_service_installed_ =
+ portable_service_install_state_.load(std::memory_order_acquire) ==
+ PortableServiceInstallState::succeeded;
+ JoinPortableWindowsServiceInstallThread();
+#endif
+ });
+ main->on_acknowledge_portable_service_suppressed([this] {
+#if _WIN32 && CROSSDESK_PORTABLE
+ show_portable_service_prompt_suppressed_window_ = false;
+#endif
+ });
+}
+
+void GuiApplication::BindStreamCallbacks() {
+ if (!ui_->stream) {
+ return;
+ }
+ auto &stream = *ui_->stream;
+ stream->window().on_close_requested([this] {
+ std::vector ids;
+ {
+ std::shared_lock lock(remote_sessions_mutex_);
+ for (const auto &[id, _] : remote_sessions_) {
+ ids.push_back(id);
+ }
+ }
+ for (const auto &id : ids) {
+ CloseStreamTab(id);
+ }
+ return slint::CloseRequestResponse::HideWindow;
+ });
+ stream->on_select_tab([this](int index) { SelectStreamTab(index); });
+ stream->on_reorder_tab([this](int from, float drop_x, float tab_width) {
+ ReorderStreamTab(from, drop_x, tab_width);
+ });
+ stream->on_close_tab([this](slint::SharedString id) {
+ CloseStreamTab(std::string(id));
+ });
+ stream->on_switch_display([this](int index) {
+ auto props = SelectedSession();
+ if (!props || index < 0 ||
+ index >= static_cast(props->display_info_list_.size())) {
+ return;
+ }
+ props->selected_display_ = index;
+ RemoteAction action{};
+ action.type = ControlType::display_id;
+ action.d = index;
+ const std::string message = action.to_json();
+ SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
+ props->control_data_label_.c_str());
+ });
+ stream->on_send_shortcut([this](slint::SharedString shortcut) {
+ auto props = SelectedSession();
+ if (!props || !props->peer_) {
+ return;
+ }
+ RemoteAction action{};
+ action.type = ControlType::service_command;
+ action.c.flag = std::string(shortcut) == "Ctrl+Alt+Del"
+ ? ServiceCommandFlag::send_sas
+ : ServiceCommandFlag::lock_workstation;
+ const std::string message = action.to_json();
+ SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
+ props->control_data_label_.c_str());
+ });
+ stream->on_toggle_mouse_control([this] {
+ auto props = SelectedSession();
+ if (!props || !props->connection_established_) {
+ return;
+ }
+ props->control_mouse_ = !props->control_mouse_;
+ props->enable_mouse_control_ = props->control_mouse_;
+ start_keyboard_capturer_ = props->control_mouse_;
+ if (!props->control_mouse_) {
+ keyboard_.ForceReleasePressedKeys();
+ }
+ });
+ stream->on_toggle_audio([this] {
+ auto props = SelectedSession();
+ if (!props || !props->connection_established_) {
+ return;
+ }
+ props->audio_capture_button_pressed_ =
+ !props->audio_capture_button_pressed_;
+ RemoteAction action{};
+ action.type = ControlType::audio_capture;
+ action.a = props->audio_capture_button_pressed_;
+ const std::string message = action.to_json();
+ SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
+ props->control_data_label_.c_str());
+ });
+ stream->on_select_file([this] {
+ auto props = SelectedSession();
+ if (!props) {
+ return;
+ }
+ transfers_.ProcessSelectedFile(
+ OpenFileDialog(localization::select_file[localization_language_index_]),
+ props, props->file_label_);
+ });
+ stream->on_close_file_transfer([this] {
+ if (auto props = SelectedSession()) {
+ props->file_transfer_.file_transfer_window_visible_ = false;
+ }
+ });
+ stream->on_can_drop_file([](const slint::DataTransfer &data) {
+ return data.has_plain_text();
+ });
+ stream->on_file_dropped([this](const slint::DataTransfer &data) {
+ const auto text = data.plain_text();
+ auto props = SelectedSession();
+ if (!text || !props) {
+ return;
+ }
+ const std::string path = DecodeDroppedPath(std::string(*text));
+ if (!path.empty()) {
+ transfers_.ProcessSelectedFile(path, props, props->file_label_,
+ props->remote_id_);
+ }
+ });
+ stream->on_toggle_stats([this] {
+ if (auto props = SelectedSession()) {
+ props->net_traffic_stats_button_pressed_ =
+ !props->net_traffic_stats_button_pressed_;
+ }
+ });
+ stream->on_toggle_fullscreen([this] {
+ auto &stream = *ui_->stream;
+#if defined(__APPLE__)
+ const bool enter_fullscreen = !IsStreamWindowFullscreen();
+ // Update Slint's fullscreen layout before AppKit applies the new frame so
+ // the first fullscreen draw cannot briefly contain the windowed tab strip.
+ stream->set_fullscreen_enabled(enter_fullscreen);
+ if (!SetStreamWindowFullscreen(enter_fullscreen)) {
+ stream->set_fullscreen_enabled(!enter_fullscreen);
+ return;
+ }
+#else
+ auto &window = stream->window();
+ const bool enter_fullscreen = !window.is_fullscreen();
+#endif
+ fullscreen_button_pressed_ = enter_fullscreen;
+
+#if !defined(__APPLE__)
+ stream->set_fullscreen_enabled(enter_fullscreen);
+ window.request_redraw();
+ window.set_fullscreen(enter_fullscreen);
+#endif
+ });
+ stream->on_disconnect([this] {
+ if (auto props = SelectedSession()) {
+ CloseStreamTab(props->remote_id_);
+ }
+ });
+ stream->on_pointer_input([this](slint::PointerEventButton button,
+ slint::PointerEventKind kind, float x,
+ float y) {
+ SendPointerInput(static_cast(button), static_cast(kind), x, y);
+ });
+ stream->on_scroll_input([this](float dx, float dy, float x, float y) {
+ SendScrollInput(dx, dy, x, y);
+ });
+ stream->on_key_input(
+ [this](slint::SharedString text, bool pressed, bool control, bool alt,
+ bool shift, bool meta) {
+ SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
+ });
+}
+
+void GuiApplication::BindServerCallbacks() {
+ if (!ui_->server) {
+ return;
+ }
+ auto &server = *ui_->server;
+ server->on_title_drag([this](int phase, float x, float y) {
+ auto &drag = ui_->server_drag;
+ DragWindow(*ui_->server, phase, x, y, drag);
+ });
+ server->on_toggle_collapsed([this](bool collapsed) {
+ server_window_collapsed_ = collapsed;
+ const int language = (*ui_->server)->get_language_index();
+ const float width = ServerWindowLogicalWidth(language);
+ (*ui_->server)->window().set_size(
+ slint::LogicalSize(slint::Size{
+ width, ServerWindowLogicalHeight(collapsed)}));
+ });
+ server->on_controller_selected([this](int index) {
+ if (index < 0 || index >= static_cast(ui_->controller_ids.size())) {
+ return;
+ }
+ selected_server_remote_id_ = ui_->controller_ids[index];
+ });
+ server->on_select_file([this] {
+ if (selected_server_remote_id_.empty()) {
+ return;
+ }
+ transfers_.ProcessSelectedFile(
+ OpenFileDialog(localization::select_file[localization_language_index_]),
+ nullptr, file_label_, selected_server_remote_id_);
+ });
+ server->on_disconnect_controller([this] {
+ if (peer_ && !selected_server_remote_id_.empty()) {
+ LeaveConnection(peer_, selected_server_remote_id_.c_str());
+ }
+ });
+}
+
+void GuiApplication::Tick() {
+ if (exit_) {
+ slint::quit_event_loop();
+ return;
+ }
+ if (!peer_) {
+ CreateConnectionPeer();
+ }
+
+ SDL_Event event;
+#if defined(__APPLE__)
+ // Slint's winit backend owns the macOS AppKit event loop. SDL_PollEvent()
+ // implicitly pumps Cocoa events, so calling it from this Slint timer would
+ // re-enter winit's event handler and abort the process. Only drain events
+ // that are already in SDL's queue; clipboard changes are polled below and
+ // tray/window actions use direct Slint callbacks on macOS.
+ while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST,
+ SDL_EVENT_LAST) > 0) {
+#else
+ while (SDL_PollEvent(&event)) {
+#endif
+ if (event.type == SDL_EVENT_QUIT) {
+ exit_ = true;
+ } else if (event.type == SDL_EVENT_CLIPBOARD_UPDATE) {
+ clipboard_.HandleLocalUpdate();
+ }
+ }
+ if (ui_->capture_mode &&
+ (ui_->capture_page == "stream" || ui_->capture_page == "server")) {
+ return;
+ }
+ clipboard_.ApplyPendingRemoteText();
+#if defined(__linux__) && !defined(__APPLE__)
+ if (ui_->tray) {
+ ui_->tray->ProcessEvents();
+ }
+#endif
+ const auto now = std::chrono::steady_clock::now();
+ if (now - ui_->last_clipboard_poll >= 250ms) {
+ clipboard_.HandleLocalUpdate();
+ ui_->last_clipboard_poll = now;
+ }
+
+ if (need_to_rejoin_ && now - last_rejoin_check_time_ >= 1s) {
+ last_rejoin_check_time_ = now;
+ need_to_rejoin_ = false;
+ for (const auto &[_, props] : remote_sessions_) {
+ if (props && props->rejoin_) {
+ ConnectTo(props->remote_id_, props->remote_password_,
+ props->remember_password_);
+ }
+ }
+ }
+
+ UpdateLabels();
+ const bool was_reloading_recent_connections = reload_recent_connections_;
+ HandleRecentConnections();
+ if (was_reloading_recent_connections && !reload_recent_connections_) {
+ ui_->recent_model_signature.clear();
+ }
+ HandleConnectionStatusChange();
+ HandlePendingPresenceProbe();
+ HandleConnectionTimeouts();
+ HandleWindowsServiceIntegration();
+ devices_.UpdateInteractions();
+
+ UpdateLocalization();
+ SyncMainWindow();
+ SyncConnectionDialog();
+ SyncPlatformDialogs();
+ SyncStreamWindow();
+ SyncServerWindow();
+}
+
+void GuiApplication::UpdateLocalization() {
+ const int language =
+ localization::detail::ClampLanguageIndex(localization_language_index_);
+ if (!ui_ || ui_->localized_language == language) {
+ return;
+ }
+ auto &strings = ui_->main->global();
+ strings.set_local_desktop(UiText(localization::local_desktop[language]));
+ strings.set_remote_desktop(UiText(localization::remote_desktop[language]));
+ strings.set_recent_connections(
+ UiText(localization::recent_connections[language]));
+ strings.set_local_id(UiText(localization::local_id[language]));
+ strings.set_device_name(UiText(localization::device_name[language]));
+ strings.set_remote_id(UiText(localization::remote_id[language]));
+ strings.set_online(UiText(localization::online[language]));
+ strings.set_offline(UiText(localization::offline[language]));
+ strings.set_password(UiText(localization::password[language]));
+ strings.set_copied(
+ UiText(localization::local_id_copied_to_clipboard[language]));
+ strings.set_connect(UiText(localization::connect[language]));
+ strings.set_settings(UiText(localization::settings[language]));
+ strings.set_about(UiText(localization::about[language]));
+ strings.set_ok(UiText(localization::ok[language]));
+ strings.set_cancel(UiText(localization::cancel[language]));
+ strings.set_new_password(UiText(localization::new_password[language]));
+ strings.set_invalid_password(UiText(localization::max_password_len[language]));
+ strings.set_edit_alias(
+ UiText(localization::input_connection_alias[language]));
+ strings.set_delete_connection(
+ UiText(localization::delete_connection[language]));
+ strings.set_confirm_delete(
+ UiText(localization::confirm_delete_connection[language]));
+ strings.set_language(UiText(localization::language[language]));
+ strings.set_video_quality(UiText(localization::video_quality[language]));
+ strings.set_frame_rate(UiText(localization::video_frame_rate[language]));
+ strings.set_codec(UiText(localization::video_encode_format[language]));
+ strings.set_hardware_codec(
+ UiText(localization::enable_hardware_video_codec[language]));
+ strings.set_turn_relay(UiText(localization::enable_turn[language]));
+ strings.set_srtp(UiText(localization::enable_srtp[language]));
+ strings.set_self_hosted(
+ UiText(localization::self_hosted_server_config[language]));
+ strings.set_autostart(UiText(localization::enable_autostart[language]));
+ strings.set_daemon(UiText(localization::enable_daemon[language]));
+ strings.set_minimize_to_tray(
+ UiText(localization::minimize_to_tray[language]));
+ strings.set_file_save_path(
+ UiText(localization::file_transfer_save_path[language]));
+ strings.set_default_desktop(UiText(localization::default_desktop[language]));
+ strings.set_server_host(
+ UiText(localization::self_hosted_server_address[language]));
+ strings.set_server_port(
+ UiText(localization::self_hosted_server_port[language]));
+ strings.set_coturn_port(
+ UiText(localization::self_hosted_server_coturn_server_port[language]));
+ strings.set_version(UiText(localization::version[language]));
+ strings.set_signal_connected(
+ UiText(localization::signal_connected[language]));
+ strings.set_signal_disconnected(
+ UiText(localization::signal_disconnected[language]));
+ strings.set_tls_error(UiText(localization::signal_tls_cert_error[language]));
+ strings.set_update_available(
+ UiText(localization::new_version_available[language]));
+ strings.set_download(UiText(localization::update[language]));
+ strings.set_input_password(UiText(localization::input_password[language]));
+ strings.set_reinput_password(
+ UiText(localization::reinput_password[language]));
+ strings.set_remember_password(
+ UiText(localization::remember_password[language]));
+ strings.set_validate_password(
+ UiText(localization::validate_password[language]));
+ strings.set_request_permissions(
+ UiText(localization::request_permissions[language]));
+ strings.set_permission_required(
+ UiText(localization::permission_required_message[language]));
+ strings.set_screen_recording_permission(
+ UiText(localization::screen_recording_permission[language]));
+ strings.set_accessibility_permission(
+ UiText(localization::accessibility_permission[language]));
+ strings.set_service_setup_title(
+ UiText(localization::windows_service_setup_title[language]));
+ strings.set_service_setup_message(
+ UiText(localization::windows_service_setup_message[language]));
+ strings.set_service_settings_label(
+ UiText(localization::windows_service_settings_label[language]));
+ strings.set_install_service(
+ UiText(localization::install_windows_service[language]));
+ strings.set_service_installed(
+ UiText(localization::windows_service_installed[language]));
+ strings.set_do_not_remind(
+ UiText(localization::do_not_remind_again[language]));
+ strings.set_notification(UiText(localization::notification[language]));
+ strings.set_service_suppressed_message(
+ UiText(localization::windows_service_prompt_suppressed_message[language]));
+ strings.set_quality_low(UiText(localization::video_quality_low[language]));
+ strings.set_quality_medium(
+ UiText(localization::video_quality_medium[language]));
+ strings.set_quality_high(UiText(localization::video_quality_high[language]));
+ strings.set_codec_h264(UiText(localization::h264[language]));
+ strings.set_codec_av1(UiText(localization::av1[language]));
+ strings.set_self_hosted_settings(
+ UiText(localization::self_hosted_server_settings[language]));
+ strings.set_access_website(UiText(localization::access_website[language]));
+ strings.set_release_date_label(UiText(localization::release_date[language]));
+ strings.set_later(UiText(localization::cancel[language]));
+ const auto apply_stream_strings = [language](auto &stream_strings) {
+ stream_strings.set_select_display(
+ UiText(localization::select_display[language]));
+ stream_strings.set_send_shortcut(
+ UiText(localization::send_shortcut[language]));
+ stream_strings.set_control_mouse(
+ UiText(localization::control_mouse[language]));
+ stream_strings.set_release_mouse(
+ UiText(localization::release_mouse[language]));
+ stream_strings.set_audio(UiText(localization::audio_capture[language]));
+ stream_strings.set_mute(UiText(localization::mute[language]));
+ stream_strings.set_select_file(
+ UiText(localization::select_file[language]));
+ stream_strings.set_show_stats(
+ UiText(localization::show_net_traffic_stats[language]));
+ stream_strings.set_hide_stats(
+ UiText(localization::hide_net_traffic_stats[language]));
+ stream_strings.set_fullscreen(UiText(localization::fullscreen[language]));
+ stream_strings.set_exit_fullscreen(
+ UiText(localization::exit_fullscreen[language]));
+ stream_strings.set_disconnect(
+ UiText(localization::disconnect[language]));
+ stream_strings.set_file_transfer(
+ UiText(localization::file_transfer_progress[language]));
+ stream_strings.set_expand_control(
+ UiText(localization::expand_control_bar[language]));
+ stream_strings.set_collapse_control(
+ UiText(localization::collapse_control_bar[language]));
+ stream_strings.set_stats_in(UiText(localization::in[language]));
+ stream_strings.set_stats_out(UiText(localization::out[language]));
+ stream_strings.set_stats_loss_rate(
+ UiText(localization::loss_rate[language]));
+ stream_strings.set_stats_resolution(
+ UiText(localization::resolution[language]));
+ stream_strings.set_stats_connection_mode(
+ UiText(localization::connection_mode[language]));
+ };
+ auto &main_stream_strings = ui_->main->global();
+ apply_stream_strings(main_stream_strings);
+ if (ui_->stream) {
+ auto &active_stream_strings =
+ (*ui_->stream)->global();
+ apply_stream_strings(active_stream_strings);
+ }
+ ui_->localized_language = language;
+}
+
+void GuiApplication::SyncMainWindow() {
+ if (!ui_) {
+ return;
+ }
+#if defined(__APPLE__)
+ if (ui_->main_native_titlebar_attempts > 0) {
+ if (HideDisabledMainWindowZoomButton()) {
+ ui_->main_native_titlebar_attempts = 0;
+ } else {
+ --ui_->main_native_titlebar_attempts;
+ }
+ }
+#endif
+ if (re_enter_remote_id_) {
+ ui_->main->invoke_reset_remote_id();
+ re_enter_remote_id_ = false;
+ }
+ ui_->main->set_local_id(UiText(FormatPeerId(client_id_)));
+ ui_->main->set_local_password(password_saved_);
+ ui_->main->set_password_visible(show_password_);
+ ui_->main->set_signal_connected(signal_connected_);
+ ui_->main->set_signal_tls_error(
+ signal_status_ == SignalStatus::SignalTlsCertError);
+ ui_->main->set_update_available(update_available_);
+ ui_->main->set_current_version(CROSSDESK_VERSION);
+ ui_->main->set_latest_version(UiText(latest_version_));
+ ui_->main->set_release_name(UiText(release_name_));
+ ui_->main->set_release_notes(UiText(CleanReleaseNotesForUi(release_notes_)));
+ ui_->main->set_release_date(UiText(release_date_));
+ ui_->main->set_settings_session_active(stream_window_inited_);
+#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
+ !defined(__arm__) && USE_CUDA) || \
+ defined(__APPLE__))
+ ui_->main->set_hardware_codec_visible(true);
+#else
+ ui_->main->set_hardware_codec_visible(false);
#endif
- UpdateLabels();
- HandleRecentConnections();
- HandleConnectionStatusChange();
- HandlePendingPresenceProbe();
- HandleConnectionTimeouts();
- HandleStreamWindow();
- HandleServerWindow();
- HandleWindowsServiceIntegration();
+ if (show_offline_warning_window_) {
+ ui_->main->set_offline_warning(UiText(offline_warning_text_));
+ show_offline_warning_window_ = false;
+ }
- const bool main_window_visible =
- main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN);
- if (main_window_visible) {
- DrawMainWindow();
- }
- if (stream_window_inited_) {
- DrawStreamWindow();
- }
+ std::ostringstream signature_builder;
+ for (const auto &[key, connection] : recent_connections_) {
+ signature_builder << key << '\0' << connection.remote_id << '\0'
+ << connection.remote_host_name << '\0'
+ << settings_.RecentConnectionDisplayName(connection)
+ << '\0'
+ << device_presence_cache_.IsOnline(connection.remote_id)
+ << '\n';
+ }
+ const std::string signature = signature_builder.str();
+ if (ui_->recent_model_signature == signature) {
+ return;
+ }
+ ui_->recent_model_signature = signature;
- if (is_server_mode_) {
- DrawServerWindow();
+ std::vector model;
+ model.reserve(recent_connections_.size());
+ for (const auto &[_, connection] : recent_connections_) {
+ ui::RecentConnection item;
+ item.remote_id = UiText(connection.remote_id);
+ item.host_name = UiText(connection.remote_host_name);
+ item.display_name =
+ UiText(settings_.RecentConnectionDisplayName(connection));
+ item.online = device_presence_cache_.IsOnline(connection.remote_id);
+ std::vector rgba;
+ int image_width = 0;
+ int image_height = 0;
+ if (!connection.image_path.empty() &&
+ thumbnail_->DecodeImage(connection.image_path, &rgba, &image_width,
+ &image_height)) {
+ slint::SharedPixelBuffer pixels(image_width,
+ image_height);
+ std::memcpy(pixels.begin(), rgba.data(), rgba.size());
+ item.thumbnail = slint::Image(std::move(pixels));
}
+ model.push_back(std::move(item));
+ }
+ ui_->recent_model->set_vector(std::move(model));
+}
- devices_.UpdateInteractions();
+void GuiApplication::SyncConnectionDialog() {
+#ifdef CROSSDESK_DEBUG
+ if (ui_ && ui_->capture_mode &&
+ (ui_->capture_page == "connection" ||
+ ui_->capture_page == "connection-password")) {
+ return;
+ }
+#endif
+ if (!ui_ || !show_connection_status_window_) {
+ if (ui_) {
+ ui_->main->set_connection_dialog_open(false);
+ ui_->connection_password_initialized = false;
+ }
+ return;
+ }
+
+ auto props = FindRemoteSession(focused_remote_id_);
+ if (!props) {
+ ui_->main->set_connection_dialog_open(false);
+ return;
+ }
+
+ if (ui_->connection_dialog_remote_id != props->remote_id_) {
+ ui_->connection_password_initialized = false;
+ }
+ ui_->connection_dialog_remote_id = props->remote_id_;
+ const ConnectionStatus status = props->connection_status_.load();
+ const bool password_required = status == ConnectionStatus::IncorrectPassword;
+ const bool pending = status == ConnectionStatus::Connecting ||
+ status == ConnectionStatus::Gathering;
+ std::string text;
+ if (password_required) {
+ text = password_validating_
+ ? localization::validate_password[localization_language_index_]
+ : password_validating_time_ <= 1
+ ? localization::input_password[localization_language_index_]
+ : localization::reinput_password[localization_language_index_];
+ } else {
+ text = ConnectionStatusText(status, localization_language_index_);
+ }
+
+ ui_->main->set_connection_dialog_open(true);
+ ui_->main->set_connection_status_text(UiText(text));
+ ui_->main->set_connection_pending(pending);
+ ui_->main->set_connection_password_required(password_required);
+ ui_->main->set_connection_validating(password_validating_);
+ if (password_required && !password_validating_ &&
+ !ui_->connection_password_initialized) {
+ ui_->main->set_connection_password(props->remote_password_);
+ ui_->main->set_connection_remember_password(props->remember_password_);
+ ui_->connection_password_initialized = true;
+ } else if (!password_required) {
+ ui_->connection_password_initialized = false;
}
}
-void GuiApplication::HandleStreamWindow() {
- if (need_to_create_stream_window_) {
- CreateStreamWindow();
+void GuiApplication::SyncPlatformDialogs() {
+#ifdef CROSSDESK_DEBUG
+ if (ui_ && ui_->capture_mode &&
+ (ui_->capture_page == "service" ||
+ ui_->capture_page == "service-notice")) {
+ return;
+ }
+#endif
+#if _WIN32 && CROSSDESK_PORTABLE
+ CheckPortableWindowsService();
+ const PortableServiceInstallState state =
+ portable_service_install_state_.load(std::memory_order_acquire);
+ if (state == PortableServiceInstallState::succeeded) {
+ portable_service_installed_ = true;
+ }
+ ui_->main->set_portable_service_settings_visible(true);
+ ui_->main->set_portable_service_dialog_open(
+ show_portable_service_install_window_);
+ ui_->main->set_portable_service_suppressed_notice_open(
+ show_portable_service_prompt_suppressed_window_);
+ ui_->main->set_portable_service_installed(portable_service_installed_);
+ ui_->main->set_portable_service_installing(
+ state == PortableServiceInstallState::installing);
+ ui_->main->set_portable_service_succeeded(
+ state == PortableServiceInstallState::succeeded);
+ const std::string *status = nullptr;
+ if (state == PortableServiceInstallState::installing) {
+ status = &localization::installing_windows_service
+ [localization_language_index_];
+ } else if (state == PortableServiceInstallState::succeeded) {
+ status = &localization::windows_service_install_success
+ [localization_language_index_];
+ } else if (state == PortableServiceInstallState::failed) {
+ status = &localization::windows_service_install_failed
+ [localization_language_index_];
+ }
+ ui_->main->set_portable_service_status(
+ status ? UiText(*status) : slint::SharedString{});
+ if (show_portable_service_install_window_) {
+ if (!ui_->portable_service_dialog_initialized) {
+ ui_->main->set_portable_service_do_not_remind(
+ portable_service_do_not_remind_);
+ ui_->portable_service_dialog_initialized = true;
+ }
+ } else {
+ ui_->portable_service_dialog_initialized = false;
+ }
+#else
+ ui_->main->set_portable_service_settings_visible(false);
+ ui_->main->set_portable_service_dialog_open(false);
+ ui_->main->set_portable_service_suppressed_notice_open(false);
+#endif
+#ifdef __APPLE__
+ if (ui_->capture_mode) {
+ const bool show_permission = ui_->capture_page == "permission";
+ ui_->main->set_permission_dialog_open(show_permission);
+ ui_->main->set_screen_recording_granted(!show_permission);
+ ui_->main->set_accessibility_granted(!show_permission);
+ return;
+ }
+ RefreshMacPermissionStatus(false);
+ show_request_permission_window_ =
+ !mac_screen_recording_permission_granted_ ||
+ !mac_accessibility_permission_granted_;
+ ui_->main->set_permission_dialog_open(show_request_permission_window_);
+ ui_->main->set_screen_recording_granted(
+ mac_screen_recording_permission_granted_);
+ ui_->main->set_accessibility_granted(
+ mac_accessibility_permission_granted_);
+#else
+ ui_->main->set_permission_dialog_open(false);
+#endif
+}
+
+void GuiApplication::SyncStreamWindow() {
+ bool has_sessions = false;
+ {
+ std::shared_lock lock(remote_sessions_mutex_);
+ has_sessions = !remote_sessions_.empty();
+ }
+ if (need_to_create_stream_window_ && !ui_->stream) {
+ ui_->stream.emplace(ui::StreamWindow::create());
+ RegisterFontAwesome((*ui_->stream)->window());
+ // Slint globals belong to a component tree. Force the next localization
+ // pass to initialize the newly-created, independent stream window tree.
+ ui_->localized_language = -1;
+ (*ui_->stream)->set_tabs(ui_->tab_model);
+ (*ui_->stream)->set_displays(ui_->display_model);
+ (*ui_->stream)->set_file_transfers(ui_->transfer_model);
+ (*ui_->stream)->set_stats_rows(ui_->stats_model);
+ BindStreamCallbacks();
+ (*ui_->stream)->show();
+ // Repeat after the next layout pass because the backend resolves the
+ // initial DPI and preferred size asynchronously on some platforms.
+ ui_->stream_initial_position_attempts = 2;
+#if defined(__APPLE__)
+ ui_->stream_live_resize_configuration_attempts = 30;
+#endif
+ stream_window_created_ = true;
+ stream_window_inited_ = true;
need_to_create_stream_window_ = false;
}
+ if (!ui_->stream) {
+ return;
+ }
+ (*ui_->stream)->set_srtp_enabled(enable_srtp_);
+#if defined(__APPLE__)
+ if (ui_->stream_live_resize_configuration_attempts > 0) {
+ if (ConfigureStreamWindowLiveResize()) {
+ ui_->stream_live_resize_configuration_attempts = 0;
+ } else {
+ --ui_->stream_live_resize_configuration_attempts;
+ }
+ }
+#endif
+ if (ui_->stream_initial_position_attempts > 0) {
+ if (PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
+ kStreamWindowLogicalWidth,
+ kStreamWindowLogicalHeight)) {
+ --ui_->stream_initial_position_attempts;
+ } else {
+ ui_->stream_initial_position_attempts = 0;
+ }
+ }
+ if (!has_sessions) {
+#if defined(__APPLE__)
+ SetStreamWindowFullscreen(false);
+#endif
+ (*ui_->stream)->hide();
+ ui_->stream.reset();
+ stream_window_created_ = false;
+ stream_window_inited_ = false;
+ ui_->stream_initial_position_attempts = 0;
+ ui_->stream_live_resize_configuration_attempts = 0;
+ focused_remote_id_.clear();
+ controlled_remote_id_.clear();
+ ui_->tab_order.clear();
+ ui_->tab_ids.clear();
+ ui_->tab_model_signature.clear();
+ return;
+ }
- if (stream_window_inited_) {
- if (!stream_window_grabbed_ && control_mouse_) {
- SDL_SetWindowMouseGrab(stream_window_, true);
- stream_window_grabbed_ = true;
- } else if (stream_window_grabbed_ && !control_mouse_) {
- SDL_SetWindowMouseGrab(stream_window_, false);
- stream_window_grabbed_ = false;
+ std::vector tabs;
+ std::unordered_map tabs_by_id;
+ {
+ std::shared_lock lock(remote_sessions_mutex_);
+ tabs_by_id.reserve(remote_sessions_.size());
+ for (const auto &[id, props] : remote_sessions_) {
+ if (!props || !props->tab_opened_) {
+ continue;
+ }
+ ui::StreamTab tab;
+ tab.remote_id = UiText(id);
+ tab.title = UiText(props->remote_host_name_.empty()
+ ? id
+ : props->remote_host_name_);
+ tab.connected = props->connection_status_.load() ==
+ ConnectionStatus::Connected;
+ tabs_by_id.emplace(id, std::move(tab));
+ }
+ }
+ std::erase_if(ui_->tab_order, [&tabs_by_id](const std::string &id) {
+ return !tabs_by_id.contains(id);
+ });
+ for (const auto &[id, _] : tabs_by_id) {
+ if (std::find(ui_->tab_order.begin(), ui_->tab_order.end(), id) ==
+ ui_->tab_order.end()) {
+ ui_->tab_order.push_back(id);
+ }
+ }
+ ui_->tab_ids = ui_->tab_order;
+ tabs.reserve(ui_->tab_ids.size());
+ std::ostringstream tab_signature_builder;
+ for (const auto &id : ui_->tab_ids) {
+ if (auto found = tabs_by_id.find(id); found != tabs_by_id.end()) {
+ tab_signature_builder << id << '\0' << std::string(found->second.title)
+ << '\0' << found->second.connected << '\n';
+ tabs.push_back(std::move(found->second));
+ }
+ }
+ const std::string tab_signature = tab_signature_builder.str();
+ if (ui_->tab_model_signature != tab_signature) {
+ ui_->tab_model_signature = tab_signature;
+ ui_->tab_model->set_vector(std::move(tabs));
+ }
+
+ int selected_index = 0;
+ if (focused_remote_id_.empty() && !ui_->tab_ids.empty()) {
+ focused_remote_id_ = ui_->tab_ids.front();
+ }
+ for (int i = 0; i < static_cast(ui_->tab_ids.size()); ++i) {
+ if (ui_->tab_ids[i] == focused_remote_id_) {
+ selected_index = i;
+ break;
+ }
+ }
+ (*ui_->stream)->set_selected_tab(selected_index);
+ SelectStreamTab(selected_index);
+
+ auto props = SelectedSession();
+ if (!props) {
+ return;
+ }
+ const ConnectionStatus status = props->connection_status_.load();
+ // Once the peer is connected, the frame-waiting hint replaces the
+ // connection status. Keeping these states mutually exclusive also avoids a
+ // transient overlap when both properties are synchronized in one tick.
+ (*ui_->stream)->set_status_text(UiText(
+ status == ConnectionStatus::Connected
+ ? std::string{}
+ : ConnectionStatusText(status, localization_language_index_)));
+ (*ui_->stream)->set_receiving_text(
+ UiText(status == ConnectionStatus::Connected
+ ? localization::receiving_screen[localization_language_index_]
+ : std::string{}));
+ (*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
+ (*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
+#if defined(__APPLE__)
+ fullscreen_button_pressed_ = IsStreamWindowFullscreen();
+#else
+ fullscreen_button_pressed_ = (*ui_->stream)->window().is_fullscreen();
+#endif
+ (*ui_->stream)->set_fullscreen_enabled(fullscreen_button_pressed_);
+ (*ui_->stream)->set_stats_visible(
+ props->net_traffic_stats_button_pressed_);
+
+ std::vector displays;
+ displays.reserve(props->display_info_list_.size());
+ for (const auto &display : props->display_info_list_) {
+ displays.emplace_back(UiText(display.name));
+ }
+ ui_->display_model->set_vector(std::move(displays));
+ (*ui_->stream)->set_selected_display(props->selected_display_);
+
+ const auto &net = props->net_traffic_stats_;
+ std::vector stats_rows;
+ stats_rows.reserve(4);
+ const auto append_stats_row = [&](const std::string &label,
+ const auto &inbound,
+ const auto &outbound) {
+ ui::NetworkStatsRow row;
+ row.label = UiText(label);
+ row.inbound = UiText(FormatBitrate(inbound.bitrate));
+ row.outbound = UiText(FormatBitrate(outbound.bitrate));
+ row.loss_rate = UiText(FormatLossRate(inbound.loss_rate));
+ stats_rows.push_back(std::move(row));
+ };
+ append_stats_row(localization::video[localization_language_index_],
+ net.video_inbound_stats, net.video_outbound_stats);
+ append_stats_row(localization::audio[localization_language_index_],
+ net.audio_inbound_stats, net.audio_outbound_stats);
+ append_stats_row(localization::data[localization_language_index_],
+ net.data_inbound_stats, net.data_outbound_stats);
+ append_stats_row(localization::total[localization_language_index_],
+ net.total_inbound_stats, net.total_outbound_stats);
+ ui_->stats_model->set_vector(std::move(stats_rows));
+ (*ui_->stream)->set_stats_fps(UiText(std::to_string(props->fps_)));
+ (*ui_->stream)->set_stats_resolution(UiText(
+ std::to_string(props->video_width_) + "x" +
+ std::to_string(props->video_height_)));
+ (*ui_->stream)->set_stats_connection_mode(UiText(
+ props->traversal_mode_ == TraversalMode::P2P
+ ? localization::connection_mode_direct[localization_language_index_]
+ : localization::connection_mode_relay[localization_language_index_]));
+
+ std::vector file_list;
+ {
+ std::lock_guard lock(props->file_transfer_.file_transfer_list_mutex_);
+ file_list = props->file_transfer_.file_transfer_list_;
+ }
+ const auto transfer_priority = [](RemoteSession::FileTransferStatus status) {
+ switch (status) {
+ case RemoteSession::FileTransferStatus::Sending:
+ return 0;
+ case RemoteSession::FileTransferStatus::Completed:
+ return 1;
+ case RemoteSession::FileTransferStatus::Queued:
+ return 2;
+ case RemoteSession::FileTransferStatus::Failed:
+ return 3;
+ }
+ return 3;
+ };
+ std::stable_sort(file_list.begin(), file_list.end(),
+ [&](const auto &left, const auto &right) {
+ return transfer_priority(left.status) <
+ transfer_priority(right.status);
+ });
+
+ std::vector transfers;
+ transfers.reserve(file_list.size());
+ for (const auto &info : file_list) {
+ ui::FileTransferEntry item;
+ item.name = UiText(info.file_name);
+ const auto status_index = static_cast(info.status);
+ if (status_index == 0)
+ item.status = UiText(localization::queued[localization_language_index_]);
+ if (status_index == 1)
+ item.status = UiText(localization::sending[localization_language_index_]);
+ if (status_index == 2)
+ item.status = UiText(localization::completed[localization_language_index_]);
+ if (status_index == 3)
+ item.status = UiText(localization::failed[localization_language_index_]);
+ item.progress = status_index == 2
+ ? 1.0f
+ : info.file_size == 0
+ ? 0.0f
+ : std::clamp(
+ static_cast(info.sent_bytes) /
+ static_cast(info.file_size),
+ 0.0f, 1.0f);
+ item.speed = UiText(FormatTransferRate(info.rate_bps));
+ item.size = UiText(FormatFileSize(info.file_size));
+ transfers.push_back(std::move(item));
+ }
+ ui_->transfer_model->set_vector(std::move(transfers));
+ (*ui_->stream)->set_file_transfer_visible(
+ props->file_transfer_.file_transfer_window_visible_);
+
+ std::shared_ptr> frame;
+ int width = 0;
+ int height = 0;
+ uint64_t sequence = 0;
+ {
+ std::lock_guard lock(props->video_frame_mutex_);
+ frame = props->front_frame_;
+ width = props->video_width_;
+ height = props->video_height_;
+ sequence = props->video_frame_sequence_;
+ }
+ const size_t nv12_size = static_cast(width) * height * 3 / 2;
+ if (frame && width > 0 && height > 0 && frame->size() >= nv12_size &&
+ ui_->displayed_frame_sequence[props->remote_id_] != sequence) {
+ slint::SharedPixelBuffer pixels(width, height);
+ const int result = libyuv::NV12ToRAW(
+ frame->data(), width, frame->data() + static_cast(width) * height,
+ width, reinterpret_cast(pixels.begin()), width * 3, width,
+ height);
+ if (result == 0) {
+ (*ui_->stream)->set_frame(slint::Image(std::move(pixels)));
+ (*ui_->stream)->set_has_frame(true);
+ (*ui_->stream)->set_receiving_text("");
+ ui_->displayed_frame_sequence[props->remote_id_] = sequence;
+ }
+ } else if (!frame) {
+ (*ui_->stream)->set_has_frame(false);
+ }
+}
+
+void GuiApplication::SyncServerWindow() {
+ if (need_to_create_server_window_ && !ui_->server) {
+ ui_->server.emplace(ui::ServerWindow::create());
+ RegisterFontAwesome((*ui_->server)->window());
+ (*ui_->server)->set_controllers(ui_->controller_model);
+ (*ui_->server)->set_controller_names(ui_->controller_name_model);
+ (*ui_->server)->set_language_index(localization_language_index_);
+ server_window_collapsed_ = false;
+ (*ui_->server)->window().set_size(slint::LogicalSize(slint::Size{
+ ServerWindowLogicalWidth(localization_language_index_),
+ ServerWindowLogicalHeight(false)}));
+ BindServerCallbacks();
+ (*ui_->server)->show();
+ // Apply once immediately and once after the next layout pass. This keeps
+ // the right and bottom edges correct when the backend resolves DPI and
+ // language-dependent preferred width asynchronously on first show.
+ ui_->server_initial_position_attempts = 2;
+ server_window_created_ = true;
+ server_window_inited_ = true;
+ need_to_create_server_window_ = false;
+ }
+ if (need_to_destroy_server_window_ && ui_->server) {
+ (*ui_->server)->hide();
+ ui_->server.reset();
+ server_window_created_ = false;
+ server_window_inited_ = false;
+ ui_->server_initial_position_attempts = 0;
+ need_to_destroy_server_window_ = false;
+ }
+ if (!ui_->server) {
+ return;
+ }
+
+ std::vector controllers;
+ std::vector names;
+ ui_->controller_ids.clear();
+ {
+ std::shared_lock lock(connection_status_mutex_);
+ controllers.reserve(connection_status_.size());
+ for (const auto &[id, _] : connection_status_) {
+ const auto host = connection_host_names_.find(id);
+ const std::string name =
+ host != connection_host_names_.end() && !host->second.empty()
+ ? host->second
+ : id;
+ ui::ControllerEntry entry;
+ entry.remote_id = UiText(id);
+ entry.display_name = UiText(name);
+ controllers.push_back(entry);
+ names.emplace_back(UiText(name));
+ ui_->controller_ids.push_back(id);
+ }
+ }
+ ui_->controller_model->set_vector(std::move(controllers));
+ ui_->controller_name_model->set_vector(std::move(names));
+ if (selected_server_remote_id_.empty() && !ui_->controller_ids.empty()) {
+ selected_server_remote_id_ = ui_->controller_ids.front();
+ }
+ int selected = 0;
+ for (int i = 0; i < static_cast(ui_->controller_ids.size()); ++i) {
+ if (ui_->controller_ids[i] == selected_server_remote_id_) {
+ selected = i;
+ break;
+ }
+ }
+ (*ui_->server)->set_selected_controller(selected);
+ (*ui_->server)->set_language_index(localization_language_index_);
+ if (ui_->server_initial_position_attempts > 0) {
+ const float server_width =
+ ServerWindowLogicalWidth(localization_language_index_);
+ const float server_height =
+ ServerWindowLogicalHeight(server_window_collapsed_);
+ (*ui_->server)->window().set_size(slint::LogicalSize(
+ slint::Size{server_width, server_height}));
+ if (PositionWindowAtBottomRight((*ui_->server)->window(), server_width,
+ server_height)) {
+ --ui_->server_initial_position_attempts;
+ } else {
+ ui_->server_initial_position_attempts = 0;
+ }
+ }
+ (*ui_->server)->set_controller_label(
+ UiText(localization::controller[localization_language_index_]));
+ (*ui_->server)->set_connection_label(
+ UiText(localization::connection_status[localization_language_index_]));
+ (*ui_->server)->set_file_transfer_label(
+ UiText(localization::file_transfer[localization_language_index_]));
+ (*ui_->server)->set_select_file_label(
+ UiText(localization::select_file[localization_language_index_]));
+
+ ConnectionStatus status = ConnectionStatus::Closed;
+ {
+ std::shared_lock lock(connection_status_mutex_);
+ if (const auto found = connection_status_.find(selected_server_remote_id_);
+ found != connection_status_.end()) {
+ status = found->second;
+ }
+ }
+ (*ui_->server)->set_connection_status(UiText(
+ ConnectionStatusText(status, localization_language_index_)));
+
+ auto &transfer = transfers_.global_state();
+ const auto sent = transfer.file_sent_bytes_.load();
+ const auto total = transfer.file_total_bytes_.load();
+ (*ui_->server)->set_file_transfer_visible(
+ transfer.file_transfer_window_visible_);
+ (*ui_->server)->set_sending_file(transfer.file_sending_.load());
+ (*ui_->server)->set_file_progress(
+ total == 0 ? 0.0f
+ : std::clamp(static_cast(sent) /
+ static_cast(total),
+ 0.0f, 1.0f));
+ (*ui_->server)->set_file_progress_text(
+ UiText(FormatTransferRate(transfer.file_send_rate_bps_.load())));
+ std::string current_file_name;
+ const uint32_t current_file_id = transfer.current_file_id_.load();
+ if (current_file_id != 0) {
+ std::lock_guard lock(transfer.file_transfer_list_mutex_);
+ for (const auto &info : transfer.file_transfer_list_) {
+ if (info.file_id == current_file_id) {
+ current_file_name = info.file_name;
+ break;
+ }
+ }
+ }
+ (*ui_->server)->set_current_file_name(UiText(current_file_name));
+ (*ui_->server)->set_file_size_text(
+ total == 0 ? slint::SharedString{}
+ : UiText(FormatFileSize(sent) + " / " +
+ FormatFileSize(total)));
+}
+
+void GuiApplication::SaveSettingsFromUi() {
+ auto &main = ui_->main;
+ language_button_value_ = localization::detail::ClampLanguageIndex(
+ main->get_language_index());
+ video_quality_button_value_ =
+ std::clamp(main->get_video_quality_index(), 0, 2);
+ video_frame_rate_button_value_ =
+ std::clamp(main->get_frame_rate_index(), 0, 1);
+ video_encode_format_button_value_ =
+ std::clamp(main->get_codec_index(), 0, 1);
+ enable_hardware_video_codec_ = main->get_hardware_codec_enabled();
+ enable_turn_ = main->get_turn_enabled();
+ enable_srtp_ = main->get_srtp_enabled();
+ enable_self_hosted_ = main->get_self_hosted_enabled();
+ enable_autostart_ = main->get_autostart_enabled();
+ enable_daemon_ = main->get_daemon_enabled();
+ enable_minimize_to_tray_ = main->get_minimize_to_tray_enabled();
+
+ localization_language_ =
+ static_cast(language_button_value_);
+ localization_language_index_ = language_button_value_;
+ config_center_->SetLanguage(localization_language_);
+ config_center_->SetVideoQuality(
+ static_cast(video_quality_button_value_));
+ config_center_->SetVideoFrameRate(static_cast(
+ video_frame_rate_button_value_));
+ config_center_->SetVideoEncodeFormat(
+ static_cast(
+ video_encode_format_button_value_));
+ config_center_->SetHardwareVideoCodec(enable_hardware_video_codec_);
+ config_center_->SetTurn(enable_turn_);
+ config_center_->SetSrtp(enable_srtp_);
+ config_center_->SetSelfHosted(enable_self_hosted_);
+ config_center_->SetAutostart(enable_autostart_);
+ config_center_->SetDaemon(enable_daemon_);
+ config_center_->SetMinimizeToTray(enable_minimize_to_tray_);
+
+ const std::string path(main->get_file_save_path());
+ std::memset(file_transfer_save_path_buf_, 0,
+ sizeof(file_transfer_save_path_buf_));
+ std::strncpy(file_transfer_save_path_buf_, path.c_str(),
+ sizeof(file_transfer_save_path_buf_) - 1);
+ config_center_->SetFileTransferSavePath(path);
+
+ const std::string host(main->get_server_host());
+ if (!host.empty()) {
+ config_center_->SetServerHost(host);
+ }
+ if (const auto port = ParsePort(main->get_server_port())) {
+ config_center_->SetServerPort(*port);
+ }
+ if (const auto port = ParsePort(main->get_coturn_port())) {
+ config_center_->SetCoturnServerPort(*port);
+ }
+
+ language_button_value_last_ = language_button_value_;
+ video_quality_button_value_last_ = video_quality_button_value_;
+ video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
+ video_encode_format_button_value_last_ =
+ video_encode_format_button_value_;
+ enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
+ enable_turn_last_ = enable_turn_;
+ enable_srtp_last_ = enable_srtp_;
+ enable_self_hosted_last_ = enable_self_hosted_;
+ enable_autostart_last_ = enable_autostart_;
+ enable_daemon_last_ = enable_daemon_;
+ enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
+ file_transfer_save_path_last_ = path;
+ ui_->localized_language = -1;
+
+ if (!stream_window_inited_) {
+ CloseAllRemoteSessions();
+ CreateConnectionPeer();
+ }
+}
+
+void GuiApplication::ConnectFromUi(const std::string &input) {
+ const std::string remote_id = CompactPeerId(input);
+ if (remote_id.empty()) {
+ return;
+ }
+ if (auto existing = FindRemoteSession(remote_id);
+ existing && existing->connection_established_) {
+ focused_remote_id_ = remote_id;
+ need_to_create_stream_window_ = true;
+ return;
+ }
+
+ for (const auto &[_, connection] : recent_connections_) {
+ if (connection.remote_id == remote_id) {
+ ConnectTo(remote_id, connection.password.c_str(),
+ connection.remember_password);
+ return;
+ }
+ }
+ ConnectTo(remote_id, "", false);
+}
+
+void GuiApplication::SelectStreamTab(int index) {
+ if (!ui_ || index < 0 || index >= static_cast(ui_->tab_ids.size())) {
+ return;
+ }
+ focused_remote_id_ = ui_->tab_ids[index];
+ controlled_remote_id_ = focused_remote_id_;
+ std::shared_lock lock(remote_sessions_mutex_);
+ for (auto &[id, props] : remote_sessions_) {
+ if (props) {
+ props->tab_selected_ = id == focused_remote_id_;
}
}
}
-void GuiApplication::HandleServerWindow() {
- if (need_to_create_server_window_) {
- CreateServerWindow();
- need_to_create_server_window_ = false;
+void GuiApplication::ReorderStreamTab(int from, float drop_x,
+ float tab_width) {
+ if (!ui_ || from < 0 ||
+ from >= static_cast(ui_->tab_order.size()) || tab_width <= 0.0f) {
+ return;
+ }
+ const float slot_width = tab_width + 1.0f;
+ const int target = std::clamp(static_cast(std::floor(drop_x / slot_width)),
+ 0,
+ static_cast(ui_->tab_order.size()) - 1);
+ if (target == from) {
+ return;
+ }
+ std::string moved = std::move(ui_->tab_order[from]);
+ ui_->tab_order.erase(ui_->tab_order.begin() + from);
+ ui_->tab_order.insert(ui_->tab_order.begin() + target, std::move(moved));
+ ui_->tab_ids = ui_->tab_order;
+ SelectStreamTab(target);
+}
+
+void GuiApplication::CloseStreamTab(const std::string &remote_id) {
+ std::shared_ptr props;
+ {
+ std::shared_lock lock(remote_sessions_mutex_);
+ if (const auto found = remote_sessions_.find(remote_id);
+ found != remote_sessions_.end()) {
+ props = found->second;
+ }
+ }
+ if (!props) {
+ return;
+ }
+ keyboard_.ReleaseRemotePressedKeys(remote_id, "stream_tab_closed");
+ CloseRemoteSession(props);
+ WaitForThumbnailSaveTasks();
+ reload_recent_connections_ = true;
+ recent_connection_image_save_time_ = 0;
+ ResetRemoteSessionResources(props);
+ {
+ std::unique_lock lock(remote_sessions_mutex_);
+ remote_sessions_.erase(remote_id);
+ }
+ ui_->displayed_frame_sequence.erase(remote_id);
+ if (focused_remote_id_ == remote_id) {
+ focused_remote_id_.clear();
+ controlled_remote_id_.clear();
+ }
+}
+
+std::shared_ptr
+GuiApplication::SelectedSession() {
+ std::shared_lock lock(remote_sessions_mutex_);
+ auto found = remote_sessions_.find(focused_remote_id_);
+ if (found != remote_sessions_.end()) {
+ return found->second;
+ }
+ for (const auto &[_, props] : remote_sessions_) {
+ if (props && props->tab_selected_) {
+ return props;
+ }
+ }
+ return nullptr;
+}
+
+void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
+ auto props = SelectedSession();
+ if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
+ return;
}
- if (need_to_destroy_server_window_) {
- DestroyServerWindow();
- DestroyServerWindowContext();
- need_to_destroy_server_window_ = false;
+ const auto size = (*ui_->stream)->window().size();
+ const float scale = (*ui_->stream)->window().scale_factor();
+ const float available_width = size.width / scale;
+ const float available_height =
+ size.height / scale -
+ (fullscreen_button_pressed_
+ ? 0.0f
+ : (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
+ float render_width = available_width;
+ float render_height = available_height;
+ float offset_x = 0;
+ float offset_y = 0;
+ if (props->video_width_ > 0 && props->video_height_ > 0 &&
+ available_width > 0 && available_height > 0) {
+ const float video_aspect =
+ static_cast(props->video_width_) / props->video_height_;
+ const float area_aspect = available_width / available_height;
+ if (video_aspect > area_aspect) {
+ render_height = available_width / video_aspect;
+ offset_y = (available_height - render_height) * 0.5f;
+ } else {
+ render_width = available_height * video_aspect;
+ offset_x = (available_width - render_width) * 0.5f;
+ }
}
+ if (x < offset_x || x > offset_x + render_width || y < offset_y ||
+ y > offset_y + render_height) {
+ return;
+ }
+
+ RemoteAction action{};
+ action.type = ControlType::mouse;
+ action.m.x = std::clamp((x - offset_x) / render_width, 0.0f, 1.0f);
+ action.m.y = std::clamp((y - offset_y) / render_height, 0.0f, 1.0f);
+ // Slint enum order: Cancel=0, Down=1, Up=2, Move=3 and
+ // Other=0, Left=1, Right=2, Middle=3.
+ if (kind == 3) {
+ action.m.flag = MouseFlag::move;
+ } else if (kind == 1) {
+ if (button == 1) action.m.flag = MouseFlag::left_down;
+ else if (button == 2) action.m.flag = MouseFlag::right_down;
+ else if (button == 3) action.m.flag = MouseFlag::middle_down;
+ else return;
+ } else if (kind == 2) {
+ if (button == 1) action.m.flag = MouseFlag::left_up;
+ else if (button == 2) action.m.flag = MouseFlag::right_up;
+ else if (button == 3) action.m.flag = MouseFlag::middle_up;
+ else return;
+ } else {
+ return;
+ }
+
+ controlled_remote_id_ = props->remote_id_;
+ const std::string message = action.to_json();
+ SendDataFrame(props->peer_, message.c_str(), message.size(),
+ props->mouse_label_.c_str());
+}
+
+void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
+ float y) {
+ auto props = SelectedSession();
+ if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
+ return;
+ }
+ const auto size = (*ui_->stream)->window().size();
+ const float scale = (*ui_->stream)->window().scale_factor();
+ const float width = size.width / scale;
+ const float height =
+ size.height / scale -
+ (fullscreen_button_pressed_
+ ? 0.0f
+ : (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
+ if (width <= 0 || height <= 0) {
+ return;
+ }
+ RemoteAction action{};
+ action.type = ControlType::mouse;
+ action.m.x = std::clamp(x / width, 0.0f, 1.0f);
+ action.m.y = std::clamp(y / height, 0.0f, 1.0f);
+ if (std::abs(delta_y) >= std::abs(delta_x)) {
+ action.m.flag = MouseFlag::wheel_vertical;
+ action.m.s = delta_y > 0 ? 1 : delta_y < 0 ? -1 : 0;
+ } else {
+ action.m.flag = MouseFlag::wheel_horizontal;
+ action.m.s = delta_x > 0 ? 1 : delta_x < 0 ? -1 : 0;
+ }
+ if (action.m.s == 0) {
+ return;
+ }
+ controlled_remote_id_ = props->remote_id_;
+ const std::string message = action.to_json();
+ SendDataFrame(props->peer_, message.c_str(), message.size(),
+ props->mouse_label_.c_str());
+}
+
+void GuiApplication::SendKeyInput(const std::string &text, bool pressed,
+ bool control, bool alt, bool shift,
+ bool meta) {
+ (void)control;
+ (void)alt;
+ (void)shift;
+ (void)meta;
+ if (auto props = SelectedSession()) {
+ controlled_remote_id_ = props->remote_id_;
+ focused_remote_id_ = props->remote_id_;
+ }
+ const int virtual_key = SlintKeyToWindowsVk(text);
+ if (virtual_key >= 0) {
+ keyboard_.SendKeyCommand(virtual_key, pressed);
+ }
+}
+
+std::string GuiApplication::OpenFileDialog(const std::string &title) {
+#if defined(__APPLE__)
+ return OpenNativeFileDialog(title);
+#else
+ const char *path =
+ tinyfd_openFileDialog(title.c_str(), "", 0, nullptr, nullptr, 0);
+ return path ? path : "";
+#endif
+}
+
+bool GuiApplication::OpenUrl(const std::string &url) {
+ return SDL_OpenURL(url.c_str());
}
void GuiApplication::Cleanup() {
- clipboard_.Shutdown();
-
- devices_.DestroyDevices();
- devices_.DestroyFactories();
- CloseAllRemoteSessions();
-
+ if (!ui_) {
+ SDL_Quit();
+ return;
+ }
+ ui_->timer.stop();
#if _WIN32 && CROSSDESK_PORTABLE
JoinPortableWindowsServiceInstallThread();
#endif
-
+ clipboard_.Shutdown();
+ keyboard_.ForceReleasePressedKeys();
+ devices_.DestroyDevices();
+ devices_.DestroyFactories();
+ CloseAllRemoteSessions();
WaitForThumbnailSaveTasks();
-
devices_.DestroyAudioOutput();
+ if (ui_->stream) {
+ (*ui_->stream)->hide();
+ }
+ if (ui_->server) {
+ (*ui_->server)->hide();
+ }
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
- tray_.reset();
+ if (ui_->tray) {
+ ui_->tray->RemoveTrayIcon();
+ ui_->tray.reset();
+ }
#endif
-
- if (stream_window_created_) {
- if (stream_window_) {
- SDL_SetWindowMouseGrab(stream_window_, false);
- }
- DestroyStreamWindow();
- }
- if (stream_ctx_) {
- DestroyStreamWindowContext();
- }
-
- if (server_window_created_) {
- DestroyServerWindow();
- }
- if (server_ctx_) {
- DestroyServerWindowContext();
- }
-
- DestroyMainWindowContext();
- DestroyMainWindow();
+ ui_.reset();
SDL_Quit();
}
-
} // namespace crossdesk
diff --git a/src/gui/application/gui_application.h b/src/gui/application/gui_application.h
index 1785f19..f39f96c 100644
--- a/src/gui/application/gui_application.h
+++ b/src/gui/application/gui_application.h
@@ -1,20 +1,15 @@
#ifndef CROSSDESK_GUI_APPLICATION_H_
#define CROSSDESK_GUI_APPLICATION_H_
-#include
+#include
#include
-#include
-#include "IconsFontAwesome6.h"
-#include "imgui_impl_sdl3.h"
-#include "imgui_impl_sdlrenderer3.h"
-#include "imgui_internal.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
-// SDL/ImGui application shell. Feature state and protocol behavior live in
-// GuiRuntime and its composed controllers.
+// Slint application shell. GuiRuntime remains the owner of transport, media,
+// device, clipboard, transfer and settings behavior.
class GuiApplication final : private GuiRuntime {
public:
GuiApplication();
@@ -23,88 +18,47 @@ public:
int Run();
private:
- // Native window integration.
- static SDL_HitTestResult HitTestCallback(SDL_Window *window,
- const SDL_Point *area, void *data);
+ struct SlintUi;
- // Application lifecycle.
void InitializeLogger();
void InitializeSettings();
- void InitializeSDL();
+ bool InitializeSDL();
void InitializeModules();
- void InitializeMainWindow();
- void MainLoop();
- void HandleStreamWindow();
- void HandleServerWindow();
- void Cleanup();
-
- // Window lifecycle and rendering.
- int CreateMainWindow();
- int DestroyMainWindow();
- int CreateStreamWindow();
- int DestroyStreamWindow();
- int CreateServerWindow();
- int DestroyServerWindow();
- int SetupFontAndStyle(ImFont **system_chinese_font_out);
- int DestroyMainWindowContext();
- int DestroyStreamWindowContext();
- int DestroyServerWindowContext();
- int DrawMainWindow();
- int DrawStreamWindow();
- int DrawServerWindow();
-
- // Views and dialogs.
- int TitleBar(bool main_window);
- int MainWindow();
- int UpdateNotificationWindow();
- int StreamWindow();
- int ServerWindow();
- int RemoteClientInfoWindow();
- int LocalWindow();
- int RemoteWindow();
- int RecentConnectionsWindow();
- int SettingWindow();
- int SelfHostedServerWindow();
- int ControlWindow(std::shared_ptr &props);
- int ControlBar(std::shared_ptr &props);
- int AboutWindow();
- int StatusBar();
- bool
- ConnectionStatusWindow(std::shared_ptr &props);
- int ShowRecentConnections();
- int ConfirmDeleteConnection();
- int EditRecentConnectionAliasWindow();
- int OfflineWarningWindow();
- int NetTrafficStats(std::shared_ptr &props);
- int FileTransferWindow(std::shared_ptr &props);
- void
- DrawConnectionStatusText(std::shared_ptr &props);
- void
- DrawReceivingScreenText(std::shared_ptr &props);
- bool OpenUrl(const std::string &url);
- void Hyperlink(const std::string &label, const std::string &url,
- float window_width);
- std::string OpenFileDialog(std::string title);
+ void InitializeUi();
+ void InitializeSystemTray();
bool MinimizeMainWindowToTray();
-
- // SDL event dispatch and input translation.
- void UpdateRenderRect();
- void ProcessSdlEvent(const SDL_Event &event);
- int ProcessKeyboardEvent(const SDL_Event &event);
- int ProcessMouseEvent(const SDL_Event &event);
- void CloseTab(decltype(remote_sessions_)::iterator &it);
-
+ void BindMainCallbacks();
+ void BindStreamCallbacks();
+ void BindServerCallbacks();
+ void Tick();
+ void SyncMainWindow();
+ void SyncConnectionDialog();
+ void SyncPlatformDialogs();
+ void SyncStreamWindow();
+ void SyncServerWindow();
+ void UpdateLocalization();
+ void ResetSettingsUi();
+ void SaveSettingsFromUi();
#if _WIN32 && CROSSDESK_PORTABLE
void CheckPortableWindowsService();
- int PortableServiceInstallWindow();
void StartPortableWindowsServiceInstall();
void JoinPortableWindowsServiceInstallThread();
#endif
+ void ConnectFromUi(const std::string &remote_id);
+ void SelectStreamTab(int index);
+ void ReorderStreamTab(int from, float drop_x, float tab_width);
+ void CloseStreamTab(const std::string &remote_id);
+ void SendPointerInput(int button, int kind, float x, float y);
+ void SendScrollInput(float delta_x, float delta_y, float x, float y);
+ void SendKeyInput(const std::string &text, bool pressed, bool control,
+ bool alt, bool shift, bool meta);
+ void Cleanup();
-#ifdef __APPLE__
- int RequestPermissionWindow();
- bool DrawToggleSwitch(const char *id, bool active, bool enabled);
-#endif
+ std::shared_ptr SelectedSession();
+ std::string OpenFileDialog(const std::string &title);
+ bool OpenUrl(const std::string &url);
+
+ std::unique_ptr ui_;
};
} // namespace crossdesk
diff --git a/src/gui/application/portable_service_integration.cpp b/src/gui/application/portable_service_integration.cpp
new file mode 100644
index 0000000..d355d9c
--- /dev/null
+++ b/src/gui/application/portable_service_integration.cpp
@@ -0,0 +1,144 @@
+#include "application/gui_application.h"
+
+#if _WIN32 && CROSSDESK_PORTABLE
+
+#include
+#include
+
+#include
+#include
+
+#include "rd_log.h"
+#include "service_host.h"
+
+namespace crossdesk {
+namespace {
+
+std::filesystem::path GetCurrentExecutablePath() {
+ std::vector buffer(MAX_PATH);
+ while (true) {
+ const DWORD length = GetModuleFileNameW(
+ nullptr, buffer.data(), static_cast(buffer.size()));
+ if (length == 0) {
+ return {};
+ }
+ if (length < buffer.size()) {
+ return std::filesystem::path(buffer.data(), buffer.data() + length);
+ }
+ if (buffer.size() >= 32768) {
+ return {};
+ }
+ buffer.resize(buffer.size() * 2);
+ }
+}
+
+bool InstallServiceWithElevation() {
+ const std::filesystem::path executable_path = GetCurrentExecutablePath();
+ if (executable_path.empty()) {
+ LOG_ERROR("Portable service install failed: current executable not found");
+ return false;
+ }
+
+ const std::filesystem::path service_path =
+ executable_path.parent_path() / L"crossdesk_service.exe";
+ const std::filesystem::path helper_path =
+ executable_path.parent_path() / L"crossdesk_session_helper.exe";
+ if (!std::filesystem::exists(service_path) ||
+ !std::filesystem::exists(helper_path)) {
+ LOG_ERROR("Portable service install failed: service binaries missing, "
+ "service={}, helper={}",
+ service_path.string(), helper_path.string());
+ return false;
+ }
+
+ const std::wstring executable = executable_path.wstring();
+ const std::wstring working_dir = executable_path.parent_path().wstring();
+ constexpr wchar_t parameters[] = L"--service-install";
+
+ SHELLEXECUTEINFOW execute_info{};
+ execute_info.cbSize = sizeof(execute_info);
+ execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
+ execute_info.lpVerb = L"runas";
+ execute_info.lpFile = executable.c_str();
+ execute_info.lpParameters = parameters;
+ execute_info.lpDirectory = working_dir.c_str();
+ execute_info.nShow = SW_HIDE;
+
+ if (!ShellExecuteExW(&execute_info)) {
+ LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
+ GetLastError());
+ return false;
+ }
+
+ const DWORD wait_result =
+ WaitForSingleObject(execute_info.hProcess, INFINITE);
+ DWORD exit_code = 1;
+ if (wait_result == WAIT_OBJECT_0) {
+ GetExitCodeProcess(execute_info.hProcess, &exit_code);
+ } else {
+ LOG_ERROR("Portable service install wait failed, result={}", wait_result);
+ }
+ CloseHandle(execute_info.hProcess);
+
+ if (exit_code != 0) {
+ LOG_ERROR("Portable service install command failed, exit_code={}",
+ exit_code);
+ return false;
+ }
+
+ const bool started = StartCrossDeskService();
+ if (!started) {
+ LOG_WARN("Portable service installed but start failed");
+ }
+ return IsCrossDeskServiceInstalled() && started;
+}
+
+} // namespace
+
+void GuiApplication::CheckPortableWindowsService() {
+ if (portable_service_prompt_checked_) {
+ return;
+ }
+ portable_service_prompt_checked_ = true;
+ portable_service_installed_ = IsCrossDeskServiceInstalled();
+ if (portable_service_installed_ || portable_service_prompt_suppressed_) {
+ return;
+ }
+
+ portable_service_install_state_.store(PortableServiceInstallState::idle,
+ std::memory_order_relaxed);
+ show_portable_service_install_window_ = true;
+}
+
+void GuiApplication::StartPortableWindowsServiceInstall() {
+ portable_service_do_not_remind_ = false;
+ PortableServiceInstallState expected = PortableServiceInstallState::idle;
+ if (!portable_service_install_state_.compare_exchange_strong(
+ expected, PortableServiceInstallState::installing,
+ std::memory_order_acq_rel)) {
+ if (expected != PortableServiceInstallState::failed) {
+ return;
+ }
+ portable_service_install_state_.store(
+ PortableServiceInstallState::installing, std::memory_order_release);
+ }
+
+ JoinPortableWindowsServiceInstallThread();
+ portable_service_install_thread_ = std::thread([this]() {
+ const bool installed = InstallServiceWithElevation();
+ portable_service_install_state_.store(
+ installed ? PortableServiceInstallState::succeeded
+ : PortableServiceInstallState::failed,
+ std::memory_order_release);
+ });
+}
+
+void GuiApplication::JoinPortableWindowsServiceInstallThread() {
+ if (portable_service_install_thread_.joinable()) {
+ portable_service_install_thread_.join();
+ }
+}
+
+} // namespace crossdesk
+
+#endif
diff --git a/src/gui/assets/localization/localization.h b/src/gui/assets/localization/localization.h
index acb6560..78fbd12 100644
--- a/src/gui/assets/localization/localization.h
+++ b/src/gui/assets/localization/localization.h
@@ -58,7 +58,7 @@ using TranslationTable =
inline std::unordered_map MakeLocalizedValues(
const TranslationRow& row) {
return {{"zh-CN", reinterpret_cast(row.zh)},
- {"en-US", row.en},
+ {"en-US", reinterpret_cast(row.en)},
{"ru-RU", reinterpret_cast(row.ru)}};
}
diff --git a/src/gui/assets/localization/localization_data.h b/src/gui/assets/localization/localization_data.h
index 8e6997d..49021e9 100644
--- a/src/gui/assets/localization/localization_data.h
+++ b/src/gui/assets/localization/localization_data.h
@@ -13,9 +13,9 @@ namespace detail {
struct TranslationRow {
const char* key;
- const char* zh;
- const char* en;
- const char* ru;
+ const void* zh;
+ const void* en;
+ const void* ru;
};
// Single source of truth for all UI strings.
diff --git a/src/gui/features/clipboard/clipboard_controller.cpp b/src/gui/features/clipboard/clipboard_controller.cpp
index 6ceaeae..8c4f70a 100644
--- a/src/gui/features/clipboard/clipboard_controller.cpp
+++ b/src/gui/features/clipboard/clipboard_controller.cpp
@@ -33,11 +33,6 @@ void ClipboardController::Initialize() {
SDL_free(clipboard_text);
}
- if (event_type_ == 0) {
- LOG_ERROR("Clipboard synchronization disabled: SDL event is unavailable");
- return;
- }
-
events_enabled_.store(true, std::memory_order_release);
}
@@ -75,7 +70,7 @@ void ClipboardController::QueueRemoteText(const char *data, size_t size) {
SDL_Event event{};
event.type = event_type_;
- if (!SDL_PushEvent(&event)) {
+ if (event_type_ != 0 && !SDL_PushEvent(&event)) {
// MainLoop also drains the pending value after its wait timeout.
LOG_WARN("Failed to wake SDL loop for remote clipboard text: {}",
SDL_GetError());
diff --git a/src/gui/features/file_transfer/file_transfer_manager.cpp b/src/gui/features/file_transfer/file_transfer_manager.cpp
index ca198d4..24b023e 100644
--- a/src/gui/features/file_transfer/file_transfer_manager.cpp
+++ b/src/gui/features/file_transfer/file_transfer_manager.cpp
@@ -13,6 +13,7 @@
#include
#include "file_transfer.h"
+#include "filesystem_utf8.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
@@ -40,7 +41,7 @@ void FileTransferManager::ProcessSelectedFile(
FileTransferState &state = state_for(props);
LOG_INFO("Selected file: {}", path.c_str());
- const std::filesystem::path file_path = std::filesystem::u8path(path);
+ const std::filesystem::path file_path = PathFromUtf8(path);
std::error_code ec;
if (!std::filesystem::is_regular_file(file_path, ec)) {
LOG_ERROR("Selected path is not a regular file: {}", path);
@@ -55,7 +56,9 @@ void FileTransferManager::ProcessSelectedFile(
{
std::lock_guard lock(state.file_transfer_list_mutex_);
FileTransferState::FileTransferInfo info;
- info.file_name = file_path.filename().u8string();
+ const auto utf8_name = file_path.filename().u8string();
+ info.file_name.assign(reinterpret_cast(utf8_name.data()),
+ utf8_name.size());
info.file_path = file_path;
info.file_size = file_size;
info.status = FileTransferState::FileTransferStatus::Queued;
@@ -341,39 +344,4 @@ void FileTransferManager::HandleAck(const char *data, size_t size) {
ProcessQueue(props);
}
-void FileTransferManager::HandleDropEvent(const SDL_Event &event) {
- if (event.type != SDL_EVENT_DROP_FILE ||
- !((owner_.stream_window_ &&
- SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) ||
- (owner_.server_window_ &&
- SDL_GetWindowID(owner_.server_window_) == event.window.windowID))) {
- return;
- }
-
- if (owner_.stream_window_ &&
- SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) {
- if (!owner_.stream_window_inited_ || !event.drop.data) {
- return;
- }
- std::shared_lock lock(owner_.remote_sessions_mutex_);
- for (const auto &[remote_id, props] : owner_.remote_sessions_) {
- if (props && props->tab_selected_ && props->peer_) {
- ProcessSelectedFile(static_cast(event.drop.data), props,
- props->file_label_);
- break;
- }
- }
- return;
- }
-
- if (owner_.server_window_inited_ && event.drop.data) {
- const auto file_path = std::filesystem::u8path(event.drop.data);
- std::error_code ec;
- if (std::filesystem::is_regular_file(file_path, ec)) {
- LOG_INFO("Drop file [{}] on server window (size: {} bytes)",
- event.drop.data, std::filesystem::file_size(file_path, ec));
- }
- }
-}
-
} // namespace crossdesk
diff --git a/src/gui/features/file_transfer/file_transfer_manager.h b/src/gui/features/file_transfer/file_transfer_manager.h
index 807a17a..748d02d 100644
--- a/src/gui/features/file_transfer/file_transfer_manager.h
+++ b/src/gui/features/file_transfer/file_transfer_manager.h
@@ -1,8 +1,6 @@
#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
-#include
-
#include
#include
#include
@@ -33,7 +31,6 @@ public:
const std::shared_ptr &props,
const std::string &file_label,
const std::string &remote_id = "");
- void HandleDropEvent(const SDL_Event &event);
void HandleAck(const char *data, size_t size);
private:
diff --git a/src/gui/platform/tray/linux_tray.cpp b/src/gui/platform/tray/linux_tray.cpp
index e734462..cb9e647 100644
--- a/src/gui/platform/tray/linux_tray.cpp
+++ b/src/gui/platform/tray/linux_tray.cpp
@@ -190,6 +190,18 @@ struct LinuxTrayImpl {
menu_label(GetMenuLabel(language_index_value)),
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
+ explicit LinuxTrayImpl(std::function show_window_callback,
+ std::function hide_window_callback,
+ std::function exit_callback,
+ std::string tray_tooltip, int language_index_value)
+ : show_window(std::move(show_window_callback)),
+ hide_window(std::move(hide_window_callback)),
+ exit_app(std::move(exit_callback)),
+ tooltip(std::move(tray_tooltip)),
+ language_index(language_index_value),
+ menu_label(GetMenuLabel(language_index_value)),
+ menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
+
~LinuxTrayImpl() { RemoveTrayIcon(); }
bool MinimizeToTray() {
@@ -197,7 +209,9 @@ struct LinuxTrayImpl {
return false;
}
- if (app_window) {
+ if (hide_window) {
+ hide_window();
+ } else if (app_window) {
SDL_HideWindow(app_window);
}
return true;
@@ -939,16 +953,20 @@ struct LinuxTrayImpl {
}
void ShowWindow() {
- if (!app_window) {
- return;
+ if (show_window) {
+ show_window();
+ } else if (app_window) {
+ SDL_ShowWindow(app_window);
+ SDL_RestoreWindow(app_window);
+ SDL_RaiseWindow(app_window);
}
-
- SDL_ShowWindow(app_window);
- SDL_RestoreWindow(app_window);
- SDL_RaiseWindow(app_window);
}
void RequestExit() {
+ if (exit_app) {
+ exit_app();
+ return;
+ }
SDL_Event event{};
event.type =
exit_event_type == 0 || exit_event_type == static_cast(-1)
@@ -958,6 +976,9 @@ struct LinuxTrayImpl {
}
::SDL_Window* app_window = nullptr;
+ std::function show_window;
+ std::function hide_window;
+ std::function exit_app;
std::string tooltip;
int language_index = 0;
uint32_t exit_event_type = 0;
@@ -1005,6 +1026,14 @@ LinuxTray::LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
: impl_(std::make_unique(app_window, tooltip, language_index,
exit_event_type)) {}
+LinuxTray::LinuxTray(std::function show_window,
+ std::function hide_window,
+ std::function exit_app,
+ const std::string& tooltip, int language_index)
+ : impl_(std::make_unique(
+ std::move(show_window), std::move(hide_window), std::move(exit_app),
+ tooltip, language_index)) {}
+
LinuxTray::~LinuxTray() = default;
bool LinuxTray::MinimizeToTray() { return impl_->MinimizeToTray(); }
diff --git a/src/gui/platform/tray/linux_tray.h b/src/gui/platform/tray/linux_tray.h
index fcf421c..d2d840b 100644
--- a/src/gui/platform/tray/linux_tray.h
+++ b/src/gui/platform/tray/linux_tray.h
@@ -10,6 +10,7 @@
#if defined(__linux__) && !defined(__APPLE__)
#include
+#include
#include
#include
@@ -23,6 +24,9 @@ class LinuxTray {
public:
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index, uint32_t exit_event_type);
+ LinuxTray(std::function show_window,
+ std::function hide_window, std::function exit_app,
+ const std::string& tooltip, int language_index);
~LinuxTray();
bool MinimizeToTray();
diff --git a/src/gui/platform/tray/mac_tray.h b/src/gui/platform/tray/mac_tray.h
index 172102a..2d761a5 100644
--- a/src/gui/platform/tray/mac_tray.h
+++ b/src/gui/platform/tray/mac_tray.h
@@ -7,6 +7,7 @@
#ifndef _MAC_TRAY_H_
#define _MAC_TRAY_H_
+#include
#include
#include
@@ -20,6 +21,11 @@ class MacTray {
public:
MacTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index);
+ MacTray(std::function show_window,
+ std::function hide_window,
+ std::function open_settings,
+ std::function exit_app,
+ const std::string& tooltip, int language_index);
~MacTray();
void MinimizeToTray();
diff --git a/src/gui/platform/tray/mac_tray.mm b/src/gui/platform/tray/mac_tray.mm
index e6c11c7..3cb9f32 100644
--- a/src/gui/platform/tray/mac_tray.mm
+++ b/src/gui/platform/tray/mac_tray.mm
@@ -13,6 +13,7 @@
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
+- (void)openSettings:(id)sender;
- (void)exitApplication:(id)sender;
@end
@@ -24,7 +25,24 @@ struct MacTrayImpl {
: app_window(window),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
- target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
+ target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
+ EnsureStatusItem();
+ }
+
+ explicit MacTrayImpl(std::function show_window_callback,
+ std::function hide_window_callback,
+ std::function open_settings_callback,
+ std::function exit_callback,
+ std::string tray_tooltip, int language_index_value)
+ : show_window(std::move(show_window_callback)),
+ hide_window(std::move(hide_window_callback)),
+ open_settings(std::move(open_settings_callback)),
+ exit_app(std::move(exit_callback)),
+ tooltip(std::move(tray_tooltip)),
+ language_index(language_index_value),
+ target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
+ EnsureStatusItem();
+ }
~MacTrayImpl() {
RemoveTrayIcon();
@@ -33,7 +51,9 @@ struct MacTrayImpl {
void MinimizeToTray() {
EnsureStatusItem();
- if (app_window) {
+ if (hide_window) {
+ hide_window();
+ } else if (app_window) {
SDL_HideWindow(app_window);
}
}
@@ -48,12 +68,12 @@ struct MacTrayImpl {
}
void ShowWindow() {
- if (!app_window) {
- return;
+ if (show_window) {
+ show_window();
+ } else if (app_window) {
+ SDL_ShowWindow(app_window);
+ SDL_RaiseWindow(app_window);
}
-
- SDL_ShowWindow(app_window);
- SDL_RaiseWindow(app_window);
[NSApp activateIgnoringOtherApps:YES];
}
@@ -64,6 +84,18 @@ struct MacTrayImpl {
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
+ NSString *settings_title =
+ NSStringFromUtf8(localization::settings
+ [localization::detail::ClampLanguageIndex(
+ language_index)]);
+ NSMenuItem *settings_item =
+ [[NSMenuItem alloc] initWithTitle:settings_title
+ action:@selector(openSettings:)
+ keyEquivalent:@""];
+ [settings_item setTarget:target];
+ [menu addItem:settings_item];
+ [menu addItem:[NSMenuItem separatorItem]];
+
NSString *exit_title =
NSStringFromUtf8(localization::exit_program
[localization::detail::ClampLanguageIndex(
@@ -86,11 +118,25 @@ struct MacTrayImpl {
}
void RequestExit() {
+ if (exit_app) {
+ exit_app();
+ return;
+ }
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
+ void OpenSettings() {
+ if (open_settings) {
+ open_settings();
+ } else {
+ ShowWindow();
+ return;
+ }
+ [NSApp activateIgnoringOtherApps:YES];
+ }
+
private:
void EnsureStatusItem() {
if (status_item) {
@@ -191,6 +237,10 @@ struct MacTrayImpl {
}
::SDL_Window *app_window = nullptr;
+ std::function show_window;
+ std::function hide_window;
+ std::function open_settings;
+ std::function exit_app;
std::string tooltip;
int language_index = 0;
NSStatusItem *status_item = nil;
@@ -202,6 +252,16 @@ MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
: impl_(
std::make_unique(app_window, tooltip, language_index)) {}
+MacTray::MacTray(std::function show_window,
+ std::function hide_window,
+ std::function open_settings,
+ std::function exit_app, const std::string &tooltip,
+ int language_index)
+ : impl_(std::make_unique(
+ std::move(show_window), std::move(hide_window),
+ std::move(open_settings), std::move(exit_app), tooltip,
+ language_index)) {}
+
MacTray::~MacTray() = default;
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
@@ -244,6 +304,13 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
}
}
+- (void)openSettings:(id)sender {
+ (void)sender;
+ if (owner_) {
+ owner_->OpenSettings();
+ }
+}
+
@end
#endif // __APPLE__
diff --git a/src/gui/platform/tray/win_tray.cpp b/src/gui/platform/tray/win_tray.cpp
index 1f93170..e0b1249 100644
--- a/src/gui/platform/tray/win_tray.cpp
+++ b/src/gui/platform/tray/win_tray.cpp
@@ -4,6 +4,8 @@
#include "localization.h"
+#include
+
namespace crossdesk {
// callback for the message-only window that handles tray icon messages
@@ -60,6 +62,16 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
}
+WinTray::WinTray(std::function show_window,
+ std::function hide_window,
+ std::function exit_app, HICON icon,
+ const std::wstring& tooltip, int language_index)
+ : WinTray(nullptr, icon, tooltip, language_index) {
+ show_window_ = std::move(show_window);
+ hide_window_ = std::move(hide_window);
+ exit_app_ = std::move(exit_app);
+}
+
WinTray::~WinTray() {
RemoveTrayIcon();
if (hwnd_message_only_) DestroyWindow(hwnd_message_only_);
@@ -67,20 +79,31 @@ WinTray::~WinTray() {
void WinTray::MinimizeToTray() {
Shell_NotifyIcon(NIM_ADD, &nid_);
- // hide application window
- ShowWindow(app_hwnd_, SW_HIDE);
+ if (hide_window_) {
+ hide_window_();
+ } else if (app_hwnd_) {
+ ShowWindow(app_hwnd_, SW_HIDE);
+ }
}
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
+void WinTray::ShowApplicationWindow() {
+ if (show_window_) {
+ show_window_();
+ } else if (app_hwnd_) {
+ ShowWindow(app_hwnd_, SW_SHOW);
+ SetForegroundWindow(app_hwnd_);
+ }
+}
+
bool WinTray::HandleTrayMessage(MSG* msg) {
if (!msg || msg->message != WM_TRAY_CALLBACK) return false;
switch (LOWORD(msg->lParam)) {
case WM_LBUTTONDBLCLK:
case WM_LBUTTONUP: {
- ShowWindow(app_hwnd_, SW_SHOW);
- SetForegroundWindow(app_hwnd_);
+ ShowApplicationWindow();
break;
}
@@ -99,13 +122,15 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
// handle menu command
if (cmd == 1001) {
- // exit application
- SDL_Event event;
- event.type = SDL_EVENT_QUIT;
- SDL_PushEvent(&event);
+ if (exit_app_) {
+ exit_app_();
+ } else {
+ SDL_Event event;
+ event.type = SDL_EVENT_QUIT;
+ SDL_PushEvent(&event);
+ }
} else if (cmd == 1002) {
- ShowWindow(app_hwnd_, SW_SHOW); // show main window
- SetForegroundWindow(app_hwnd_);
+ ShowApplicationWindow();
}
break;
}
diff --git a/src/gui/platform/tray/win_tray.h b/src/gui/platform/tray/win_tray.h
index a1e069b..17deed8 100644
--- a/src/gui/platform/tray/win_tray.h
+++ b/src/gui/platform/tray/win_tray.h
@@ -10,6 +10,7 @@
#include
#include
+#include