mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-25 09:59:21 +01:00
Un-submodule snowcap
ya know it turns out when you have a submodule it's a real pain to update things
This commit is contained in:
parent
975da0d14e
commit
83f968c3c9
62 changed files with 15182 additions and 4 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
||||||
[submodule "snowcap"]
|
|
||||||
path = snowcap
|
|
||||||
url = https://github.com/pinnacle-comp/snowcap
|
|
1
snowcap
1
snowcap
|
@ -1 +0,0 @@
|
||||||
Subproject commit 3dc265976aa1e715db483e1c69d1c84ce897e4a0
|
|
4188
snowcap/Cargo.lock
generated
Normal file
4188
snowcap/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
51
snowcap/Cargo.toml
Normal file
51
snowcap/Cargo.toml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"api/rust",
|
||||||
|
"snowcap-api-defs",
|
||||||
|
"api/lua/build"
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tokio-stream = { version = "0.1.15", features = ["net"] }
|
||||||
|
prost = "0.12.6"
|
||||||
|
tonic = "0.11.0"
|
||||||
|
tonic-reflection = "0.11.0"
|
||||||
|
tonic-build = "0.11.0"
|
||||||
|
xdg = "2.5.2"
|
||||||
|
snowcap-api-defs = { path = "./snowcap-api-defs" }
|
||||||
|
xkbcommon = "0.7.0"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
too_many_arguments = "allow"
|
||||||
|
type_complexity = "allow"
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "snowcap"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
smithay-client-toolkit = "0.19.1"
|
||||||
|
anyhow = { version = "1.0.86", features = ["backtrace"] }
|
||||||
|
iced = { version = "0.12.1", default-features = false, features = ["wgpu", "tokio"] }
|
||||||
|
iced_wgpu = "0.12.1"
|
||||||
|
iced_runtime = "0.12.1"
|
||||||
|
iced_futures = "0.12.0"
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
raw-window-handle = "0.6.2"
|
||||||
|
xdg = { workspace = true }
|
||||||
|
smithay-clipboard = "0.7.1"
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tokio-stream = { workspace = true }
|
||||||
|
futures = "0.3.30"
|
||||||
|
prost = { workspace = true }
|
||||||
|
tonic = { workspace = true }
|
||||||
|
tonic-reflection = { workspace = true }
|
||||||
|
snowcap-api-defs = { workspace = true }
|
||||||
|
xkbcommon = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
373
snowcap/LICENSE
Normal file
373
snowcap/LICENSE
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
15
snowcap/README.md
Normal file
15
snowcap/README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Snowcap
|
||||||
|
A very, *very* Wayland widget system built for Pinnacle
|
||||||
|
|
||||||
|
Currently in early development with preliminary integration into Pinnacle.
|
||||||
|
|
||||||
|
## What is Snowcap?
|
||||||
|
Snowcap is a widget system for Wayland, made for [Pinnacle](https://github.com/pinnacle-comp/pinnacle),
|
||||||
|
my WIP Wayland compositor.
|
||||||
|
|
||||||
|
It uses Smithay's [client toolkit](https://github.com/Smithay/client-toolkit) along with the
|
||||||
|
[Iced](https://github.com/iced-rs/iced) GUI library to draw various widgets on screen.
|
||||||
|
|
||||||
|
## Compositor Requirements
|
||||||
|
While I'm making this for Pinnacle, a side-goal is to have it at least somewhat compositor-agnostic.
|
||||||
|
To that end, compatible compositors must implement the wlr-layer-shell protocol for Snowcap to work.
|
16
snowcap/api/lua/.luarc.json
Normal file
16
snowcap/api/lua/.luarc.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
|
||||||
|
"workspace.library": [
|
||||||
|
"./",
|
||||||
|
"~/.luarocks/share/lua/5.4/grpc_client.lua",
|
||||||
|
"~/.luarocks/share/lua/5.4/grpc_client",
|
||||||
|
"~/.luarocks/share/lua/5.3/grpc_client.lua",
|
||||||
|
"~/.luarocks/share/lua/5.3/grpc_client",
|
||||||
|
"~/.luarocks/share/lua/5.2/grpc_client.lua",
|
||||||
|
"~/.luarocks/share/lua/5.2/grpc_client",
|
||||||
|
],
|
||||||
|
"runtime.version": "Lua 5.2",
|
||||||
|
|
||||||
|
"--comment": "Format using Stylua instead",
|
||||||
|
"format.enable": false,
|
||||||
|
}
|
2
snowcap/api/lua/.stylua.toml
Normal file
2
snowcap/api/lua/.stylua.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
indent_type = "Spaces"
|
||||||
|
column_width = 100
|
13
snowcap/api/lua/build/Cargo.toml
Normal file
13
snowcap/api/lua/build/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "lua-build"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
prost = "0.12.6"
|
||||||
|
prost-types = "0.12.6"
|
||||||
|
indexmap = "2.2.6"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.12.6"
|
||||||
|
walkdir = "2.5.0"
|
23
snowcap/api/lua/build/build.rs
Normal file
23
snowcap/api/lua/build/build.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=../../protobuf");
|
||||||
|
|
||||||
|
let mut proto_paths = Vec::new();
|
||||||
|
|
||||||
|
for entry in walkdir::WalkDir::new("../../protobuf") {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
|
||||||
|
if entry.file_type().is_file() && entry.path().extension().is_some_and(|ext| ext == "proto")
|
||||||
|
{
|
||||||
|
proto_paths.push(entry.into_path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let descriptor_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("lua-build.bin");
|
||||||
|
|
||||||
|
prost_build::Config::new()
|
||||||
|
.file_descriptor_set_path(descriptor_path)
|
||||||
|
.compile_protos(&proto_paths, &["../../protobuf"])
|
||||||
|
.unwrap();
|
||||||
|
}
|
333
snowcap/api/lua/build/src/main.rs
Normal file
333
snowcap/api/lua/build/src/main.rs
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use indexmap::{IndexMap, IndexSet};
|
||||||
|
use prost::Message as _;
|
||||||
|
use prost_types::{
|
||||||
|
field_descriptor_proto::{Label, Type},
|
||||||
|
DescriptorProto, EnumDescriptorProto, FieldDescriptorProto, ServiceDescriptorProto,
|
||||||
|
};
|
||||||
|
|
||||||
|
type EnumMap = IndexMap<String, EnumData>;
|
||||||
|
type MessageMap = IndexMap<String, MessageData>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MessageData {
|
||||||
|
fields: Vec<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Field {
|
||||||
|
name: String,
|
||||||
|
label: Option<Label>,
|
||||||
|
r#type: FieldType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum FieldType {
|
||||||
|
Builtin(Type),
|
||||||
|
Message(String),
|
||||||
|
Enum(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_message_enums(enums: &mut EnumMap, prefix: &str, message: &DescriptorProto) {
|
||||||
|
let prefix = format!("{prefix}.{}", message.name());
|
||||||
|
for r#enum in message.enum_type.iter() {
|
||||||
|
parse_enum(enums, &prefix, r#enum);
|
||||||
|
}
|
||||||
|
|
||||||
|
for msg in message.nested_type.iter() {
|
||||||
|
let name = msg.name();
|
||||||
|
parse_message_enums(enums, &format!("{prefix}.{name}"), msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_enum(enums: &mut EnumMap, prefix: &str, enum_desc: &EnumDescriptorProto) {
|
||||||
|
let name = enum_desc.name().to_string();
|
||||||
|
|
||||||
|
let values = enum_desc.value.iter().map(|val| {
|
||||||
|
let name = val.name().to_string();
|
||||||
|
let number = val.number.unwrap();
|
||||||
|
|
||||||
|
EnumValue { name, number }
|
||||||
|
});
|
||||||
|
|
||||||
|
enums.insert(
|
||||||
|
format!("{prefix}.{name}"),
|
||||||
|
EnumData {
|
||||||
|
values: values.collect(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_message(msgs: &mut MessageMap, prefix: &str, message: &DescriptorProto) {
|
||||||
|
let name = format!("{prefix}.{}", message.name());
|
||||||
|
|
||||||
|
let mut fields: HashMap<Option<i32>, Vec<Field>> = HashMap::new();
|
||||||
|
|
||||||
|
for field in message.field.iter() {
|
||||||
|
fields
|
||||||
|
// .entry(field.oneof_index)
|
||||||
|
.entry(None)
|
||||||
|
.or_default()
|
||||||
|
.push(parse_field(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = MessageData {
|
||||||
|
fields: fields.remove(&None).unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
msgs.insert(name.clone(), data);
|
||||||
|
|
||||||
|
for msg in message.nested_type.iter() {
|
||||||
|
parse_message(msgs, &name, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_field(field: &FieldDescriptorProto) -> Field {
|
||||||
|
Field {
|
||||||
|
name: field.name().to_string(),
|
||||||
|
label: field.label.is_some().then_some(field.label()),
|
||||||
|
r#type: {
|
||||||
|
if let Some(type_name) = field.type_name.as_ref() {
|
||||||
|
if let Some(r#type) = field.r#type.is_some().then_some(field.r#type()) {
|
||||||
|
match r#type {
|
||||||
|
Type::Enum => FieldType::Enum(type_name.clone()),
|
||||||
|
Type::Message => FieldType::Message(type_name.clone()),
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FieldType::Builtin(field.r#type())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FieldType::Builtin(field.r#type())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct EnumData {
|
||||||
|
values: Vec<EnumValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct EnumValue {
|
||||||
|
name: String,
|
||||||
|
number: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_enum_definitions(enums: &EnumMap) -> String {
|
||||||
|
let mut ret = String::new();
|
||||||
|
|
||||||
|
for (name, data) in enums.iter() {
|
||||||
|
let mut table = format!("---@enum {name}\nlocal {} = {{\n", name.replace('.', "_"));
|
||||||
|
|
||||||
|
for val in data.values.iter() {
|
||||||
|
table += &format!(" {} = {},\n", &val.name, val.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
table += "}\n\n";
|
||||||
|
|
||||||
|
ret += &table;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_message_classes(msgs: &MessageMap) -> String {
|
||||||
|
let mut ret = String::new();
|
||||||
|
|
||||||
|
for (name, data) in msgs.iter() {
|
||||||
|
let mut class = format!("---@class {name}\n");
|
||||||
|
|
||||||
|
for field in data.fields.iter() {
|
||||||
|
let r#type = match &field.r#type {
|
||||||
|
FieldType::Builtin(builtin) => match builtin {
|
||||||
|
Type::Double | Type::Float => "number",
|
||||||
|
Type::Int32
|
||||||
|
| Type::Int64
|
||||||
|
| Type::Uint32
|
||||||
|
| Type::Uint64
|
||||||
|
| Type::Fixed64
|
||||||
|
| Type::Fixed32
|
||||||
|
| Type::Sfixed32
|
||||||
|
| Type::Sfixed64
|
||||||
|
| Type::Sint32
|
||||||
|
| Type::Sint64 => "integer",
|
||||||
|
Type::Bool => "boolean",
|
||||||
|
Type::String | Type::Bytes => "string",
|
||||||
|
Type::Group | Type::Message | Type::Enum => "any",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
FieldType::Message(s) | FieldType::Enum(s) => s.trim_start_matches('.').to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let non_nil = if field
|
||||||
|
.label
|
||||||
|
.is_some_and(|label| matches!(label, Label::Required))
|
||||||
|
{
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"?"
|
||||||
|
};
|
||||||
|
|
||||||
|
let repeated = if field
|
||||||
|
.label
|
||||||
|
.is_some_and(|label| matches!(label, Label::Repeated))
|
||||||
|
{
|
||||||
|
"[]"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
class += &format!("---@field {} {type}{repeated}{non_nil}\n", &field.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class += "\n";
|
||||||
|
|
||||||
|
ret += &class;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Visited {
|
||||||
|
children: HashMap<String, Visited>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_message_tables(msgs: &MessageMap) -> String {
|
||||||
|
let mut ret = String::new();
|
||||||
|
|
||||||
|
let mut visited = HashMap::<String, Visited>::new();
|
||||||
|
|
||||||
|
for name in msgs.keys() {
|
||||||
|
let segments = name.trim_start_matches('.').split('.');
|
||||||
|
let mut current = &mut visited;
|
||||||
|
|
||||||
|
let mut prev_segments = Vec::new();
|
||||||
|
|
||||||
|
for segment in segments {
|
||||||
|
current = &mut current
|
||||||
|
.entry(segment.to_string())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
if prev_segments.is_empty() {
|
||||||
|
ret += &format!("local {segment} = {{}}\n")
|
||||||
|
} else {
|
||||||
|
ret += &format!(
|
||||||
|
"{} = {{}}\n",
|
||||||
|
prev_segments
|
||||||
|
.iter()
|
||||||
|
.chain([&segment])
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(".")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Visited {
|
||||||
|
children: HashMap::new(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.children;
|
||||||
|
|
||||||
|
prev_segments.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_table_enums(enums: &EnumMap) -> String {
|
||||||
|
let mut ret = String::new();
|
||||||
|
|
||||||
|
for name in enums.keys() {
|
||||||
|
let name = name.trim_start_matches('.');
|
||||||
|
let type_name = name.replace('.', "_");
|
||||||
|
|
||||||
|
ret += &format!("{name} = {type_name}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_service_defs(prefix: &str, service: &ServiceDescriptorProto) -> String {
|
||||||
|
let mut ret = String::new();
|
||||||
|
|
||||||
|
let name = format!("{prefix}.{}", service.name());
|
||||||
|
|
||||||
|
ret += &format!("{name} = {{}}\n");
|
||||||
|
|
||||||
|
for method in service.method.iter() {
|
||||||
|
ret += &format!("{name}.{} = {{}}\n", method.name());
|
||||||
|
ret += &format!("{name}.{}.service = \"{name}\"\n", method.name());
|
||||||
|
ret += &format!("{name}.{n}.method = \"{n}\"\n", n = method.name());
|
||||||
|
ret += &format!(
|
||||||
|
"{name}.{}.request = \"{}\"\n",
|
||||||
|
method.name(),
|
||||||
|
method.input_type()
|
||||||
|
);
|
||||||
|
ret += &format!(
|
||||||
|
"{name}.{}.response = \"{}\"\n",
|
||||||
|
method.name(),
|
||||||
|
method.output_type()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_returned_table(msgs: &MessageMap) -> String {
|
||||||
|
let mut toplevel_packages = IndexSet::new();
|
||||||
|
|
||||||
|
for name in msgs.keys() {
|
||||||
|
let toplevel_package = name.trim_start_matches('.').split('.').next();
|
||||||
|
if let Some(toplevel_package) = toplevel_package {
|
||||||
|
toplevel_packages.insert(toplevel_package.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = String::from("return {\n");
|
||||||
|
|
||||||
|
for pkg in toplevel_packages {
|
||||||
|
ret += &format!(" {pkg} = {pkg},\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
ret += "}\n";
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let file_descriptor_set_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/lua-build.bin"));
|
||||||
|
let file_descriptor_set =
|
||||||
|
prost_types::FileDescriptorSet::decode(&file_descriptor_set_bytes[..]).unwrap();
|
||||||
|
|
||||||
|
let mut enums = EnumMap::new();
|
||||||
|
let mut msgs = MessageMap::new();
|
||||||
|
|
||||||
|
let mut services = String::new();
|
||||||
|
|
||||||
|
for proto in file_descriptor_set.file.iter() {
|
||||||
|
let package = proto.package().to_string();
|
||||||
|
for r#enum in proto.enum_type.iter() {
|
||||||
|
parse_enum(&mut enums, &package, r#enum);
|
||||||
|
}
|
||||||
|
|
||||||
|
for msg in proto.message_type.iter() {
|
||||||
|
parse_message_enums(&mut enums, &package, msg);
|
||||||
|
parse_message(&mut msgs, &package, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
for service in proto.service.iter() {
|
||||||
|
services += &populate_service_defs(&package, service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
generate_enum_definitions(&enums) + "\n"
|
||||||
|
+ &generate_message_classes(&msgs) + "\n"
|
||||||
|
+ &generate_message_tables(&msgs) + "\n"
|
||||||
|
// + &populate_message_tables(&msgs)
|
||||||
|
+ &populate_table_enums(&enums) + "\n" + &services + "\n" + &generate_returned_table(&msgs)
|
||||||
|
);
|
||||||
|
}
|
35
snowcap/api/lua/snowcap-api-dev-1.rockspec
Normal file
35
snowcap/api/lua/snowcap-api-dev-1.rockspec
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package = "snowcap-api"
|
||||||
|
version = "dev-1"
|
||||||
|
source = {
|
||||||
|
url = "*** please add URL for source tarball, zip or repository here ***",
|
||||||
|
}
|
||||||
|
description = {
|
||||||
|
homepage = "*** please enter a project homepage ***",
|
||||||
|
license = "MPL 2.0",
|
||||||
|
}
|
||||||
|
dependencies = {
|
||||||
|
"lua >= 5.2",
|
||||||
|
"cqueues ~> 20200726",
|
||||||
|
"http ~> 0.4",
|
||||||
|
"lua-protobuf ~> 0.5",
|
||||||
|
"compat53 ~> 0.13",
|
||||||
|
"lualogging ~> 1.8.2",
|
||||||
|
|
||||||
|
-- Run just install
|
||||||
|
"lua-grpc-client >= dev-1",
|
||||||
|
}
|
||||||
|
build = {
|
||||||
|
type = "builtin",
|
||||||
|
modules = {
|
||||||
|
snowcap = "snowcap.lua",
|
||||||
|
["snowcap.grpc.client"] = "snowcap/grpc/client.lua",
|
||||||
|
["snowcap.grpc.protobuf"] = "snowcap/grpc/protobuf.lua",
|
||||||
|
["snowcap.grpc.defs"] = "snowcap/grpc/defs.lua",
|
||||||
|
["snowcap.input"] = "snowcap/input.lua",
|
||||||
|
["snowcap.input.keys"] = "snowcap/input/keys.lua",
|
||||||
|
["snowcap.widget"] = "snowcap/widget.lua",
|
||||||
|
["snowcap.layer"] = "snowcap/layer.lua",
|
||||||
|
["snowcap.util"] = "snowcap/util.lua",
|
||||||
|
["snowcap.log"] = "snowcap/log.lua",
|
||||||
|
},
|
||||||
|
}
|
34
snowcap/api/lua/snowcap.lua
Normal file
34
snowcap/api/lua/snowcap.lua
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
local client = require("snowcap.grpc.client").client
|
||||||
|
|
||||||
|
---@class snowcap.Snowcap
|
||||||
|
local snowcap = {
|
||||||
|
layer = require("snowcap.layer"),
|
||||||
|
widget = require("snowcap.widget"),
|
||||||
|
}
|
||||||
|
|
||||||
|
function snowcap.init()
|
||||||
|
require("snowcap.grpc.protobuf").build_protos()
|
||||||
|
require("snowcap.grpc.client").connect()
|
||||||
|
end
|
||||||
|
|
||||||
|
function snowcap.listen()
|
||||||
|
local success, err = client().loop:loop()
|
||||||
|
if not success then
|
||||||
|
print(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param setup_fn fun(snowcap: snowcap.Snowcap)
|
||||||
|
function snowcap.setup(setup_fn)
|
||||||
|
snowcap.init()
|
||||||
|
|
||||||
|
setup_fn(snowcap)
|
||||||
|
|
||||||
|
snowcap.listen()
|
||||||
|
end
|
||||||
|
|
||||||
|
return snowcap
|
33
snowcap/api/lua/snowcap/grpc/client.lua
Normal file
33
snowcap/api/lua/snowcap/grpc/client.lua
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
local client = {
|
||||||
|
---@type grpc_client.Client
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
client = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function socket_path()
|
||||||
|
local dir = os.getenv("XDG_RUNTIME_DIR")
|
||||||
|
if not dir then
|
||||||
|
print("$XDG_RUNTIME_DIR not set, exiting")
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local wayland_instance = os.getenv("WAYLAND_DISPLAY") or "wayland-0"
|
||||||
|
|
||||||
|
local path = dir .. "/snowcap-grpc-" .. wayland_instance .. ".sock"
|
||||||
|
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
function client.connect()
|
||||||
|
local c = require("grpc_client").new({
|
||||||
|
path = socket_path(),
|
||||||
|
})
|
||||||
|
|
||||||
|
setmetatable(client.client, { __index = c })
|
||||||
|
end
|
||||||
|
|
||||||
|
return client
|
294
snowcap/api/lua/snowcap/grpc/defs.lua
Normal file
294
snowcap/api/lua/snowcap/grpc/defs.lua
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
---@enum snowcap.widget.v0alpha1.Alignment
|
||||||
|
local snowcap_widget_v0alpha1_Alignment = {
|
||||||
|
ALIGNMENT_UNSPECIFIED = 0,
|
||||||
|
ALIGNMENT_START = 1,
|
||||||
|
ALIGNMENT_CENTER = 2,
|
||||||
|
ALIGNMENT_END = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.widget.v0alpha1.ScrollableAlignment
|
||||||
|
local snowcap_widget_v0alpha1_ScrollableAlignment = {
|
||||||
|
SCROLLABLE_ALIGNMENT_UNSPECIFIED = 0,
|
||||||
|
SCROLLABLE_ALIGNMENT_START = 1,
|
||||||
|
SCROLLABLE_ALIGNMENT_END = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.widget.v0alpha1.Font.Weight
|
||||||
|
local snowcap_widget_v0alpha1_Font_Weight = {
|
||||||
|
WEIGHT_UNSPECIFIED = 0,
|
||||||
|
WEIGHT_THIN = 1,
|
||||||
|
WEIGHT_EXTRA_LIGHT = 2,
|
||||||
|
WEIGHT_LIGHT = 3,
|
||||||
|
WEIGHT_NORMAL = 4,
|
||||||
|
WEIGHT_MEDIUM = 5,
|
||||||
|
WEIGHT_SEMIBOLD = 6,
|
||||||
|
WEIGHT_BOLD = 7,
|
||||||
|
WEIGHT_EXTRA_BOLD = 8,
|
||||||
|
WEIGHT_BLACK = 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.widget.v0alpha1.Font.Stretch
|
||||||
|
local snowcap_widget_v0alpha1_Font_Stretch = {
|
||||||
|
STRETCH_UNSPECIFIED = 0,
|
||||||
|
STRETCH_ULTRA_CONDENSED = 1,
|
||||||
|
STRETCH_EXTRA_CONDENSED = 2,
|
||||||
|
STRETCH_CONDENSED = 3,
|
||||||
|
STRETCH_SEMI_CONDENSED = 4,
|
||||||
|
STRETCH_NORMAL = 5,
|
||||||
|
STRETCH_SEMI_EXPANDED = 6,
|
||||||
|
STRETCH_EXPANDED = 7,
|
||||||
|
STRETCH_EXTRA_EXPANDED = 8,
|
||||||
|
STRETCH_ULTRA_EXPANDED = 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.widget.v0alpha1.Font.Style
|
||||||
|
local snowcap_widget_v0alpha1_Font_Style = {
|
||||||
|
STYLE_UNSPECIFIED = 0,
|
||||||
|
STYLE_NORMAL = 1,
|
||||||
|
STYLE_ITALIC = 2,
|
||||||
|
STYLE_OBLIQUE = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.layer.v0alpha1.Anchor
|
||||||
|
local snowcap_layer_v0alpha1_Anchor = {
|
||||||
|
ANCHOR_UNSPECIFIED = 0,
|
||||||
|
ANCHOR_TOP = 1,
|
||||||
|
ANCHOR_BOTTOM = 2,
|
||||||
|
ANCHOR_LEFT = 3,
|
||||||
|
ANCHOR_RIGHT = 4,
|
||||||
|
ANCHOR_TOP_LEFT = 5,
|
||||||
|
ANCHOR_TOP_RIGHT = 6,
|
||||||
|
ANCHOR_BOTTOM_LEFT = 7,
|
||||||
|
ANCHOR_BOTTOM_RIGHT = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.layer.v0alpha1.KeyboardInteractivity
|
||||||
|
local snowcap_layer_v0alpha1_KeyboardInteractivity = {
|
||||||
|
KEYBOARD_INTERACTIVITY_UNSPECIFIED = 0,
|
||||||
|
KEYBOARD_INTERACTIVITY_NONE = 1,
|
||||||
|
KEYBOARD_INTERACTIVITY_ON_DEMAND = 2,
|
||||||
|
KEYBOARD_INTERACTIVITY_EXCLUSIVE = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.layer.v0alpha1.Layer
|
||||||
|
local snowcap_layer_v0alpha1_Layer = {
|
||||||
|
LAYER_UNSPECIFIED = 0,
|
||||||
|
LAYER_BACKGROUND = 1,
|
||||||
|
LAYER_BOTTOM = 2,
|
||||||
|
LAYER_TOP = 3,
|
||||||
|
LAYER_OVERLAY = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
---@class snowcap.input.v0alpha1.Modifiers
|
||||||
|
---@field shift boolean?
|
||||||
|
---@field ctrl boolean?
|
||||||
|
---@field alt boolean?
|
||||||
|
---@field super boolean?
|
||||||
|
|
||||||
|
---@class snowcap.input.v0alpha1.KeyboardKeyRequest
|
||||||
|
---@field id integer?
|
||||||
|
|
||||||
|
---@class snowcap.input.v0alpha1.KeyboardKeyResponse
|
||||||
|
---@field key integer?
|
||||||
|
---@field modifiers snowcap.input.v0alpha1.Modifiers?
|
||||||
|
---@field pressed boolean?
|
||||||
|
|
||||||
|
---@class snowcap.input.v0alpha1.PointerButtonRequest
|
||||||
|
---@field id integer?
|
||||||
|
|
||||||
|
---@class snowcap.input.v0alpha1.PointerButtonResponse
|
||||||
|
---@field button integer?
|
||||||
|
---@field pressed boolean?
|
||||||
|
|
||||||
|
---@class google.protobuf.Empty
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Padding
|
||||||
|
---@field top number?
|
||||||
|
---@field right number?
|
||||||
|
---@field bottom number?
|
||||||
|
---@field left number?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Length
|
||||||
|
---@field fill google.protobuf.Empty?
|
||||||
|
---@field fill_portion integer?
|
||||||
|
---@field shrink google.protobuf.Empty?
|
||||||
|
---@field fixed number?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Color
|
||||||
|
---@field red number?
|
||||||
|
---@field green number?
|
||||||
|
---@field blue number?
|
||||||
|
---@field alpha number?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Font
|
||||||
|
---@field family snowcap.widget.v0alpha1.Font.Family?
|
||||||
|
---@field weight snowcap.widget.v0alpha1.Font.Weight?
|
||||||
|
---@field stretch snowcap.widget.v0alpha1.Font.Stretch?
|
||||||
|
---@field style snowcap.widget.v0alpha1.Font.Style?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Font.Family
|
||||||
|
---@field name string?
|
||||||
|
---@field serif google.protobuf.Empty?
|
||||||
|
---@field sans_serif google.protobuf.Empty?
|
||||||
|
---@field cursive google.protobuf.Empty?
|
||||||
|
---@field fantasy google.protobuf.Empty?
|
||||||
|
---@field monospace google.protobuf.Empty?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.WidgetDef
|
||||||
|
---@field text snowcap.widget.v0alpha1.Text?
|
||||||
|
---@field column snowcap.widget.v0alpha1.Column?
|
||||||
|
---@field row snowcap.widget.v0alpha1.Row?
|
||||||
|
---@field scrollable snowcap.widget.v0alpha1.Scrollable?
|
||||||
|
---@field container snowcap.widget.v0alpha1.Container?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Text
|
||||||
|
---@field text string?
|
||||||
|
---@field pixels number?
|
||||||
|
---@field width snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field height snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field horizontal_alignment snowcap.widget.v0alpha1.Alignment?
|
||||||
|
---@field vertical_alignment snowcap.widget.v0alpha1.Alignment?
|
||||||
|
---@field color snowcap.widget.v0alpha1.Color?
|
||||||
|
---@field font snowcap.widget.v0alpha1.Font?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Column
|
||||||
|
---@field spacing number?
|
||||||
|
---@field padding snowcap.widget.v0alpha1.Padding?
|
||||||
|
---@field item_alignment snowcap.widget.v0alpha1.Alignment?
|
||||||
|
---@field width snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field height snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field max_width number?
|
||||||
|
---@field clip boolean?
|
||||||
|
---@field children snowcap.widget.v0alpha1.WidgetDef[]?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Row
|
||||||
|
---@field spacing number?
|
||||||
|
---@field padding snowcap.widget.v0alpha1.Padding?
|
||||||
|
---@field item_alignment snowcap.widget.v0alpha1.Alignment?
|
||||||
|
---@field width snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field height snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field clip boolean?
|
||||||
|
---@field children snowcap.widget.v0alpha1.WidgetDef[]?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.ScrollableDirection
|
||||||
|
---@field vertical snowcap.widget.v0alpha1.ScrollableProperties?
|
||||||
|
---@field horizontal snowcap.widget.v0alpha1.ScrollableProperties?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.ScrollableProperties
|
||||||
|
---@field width number?
|
||||||
|
---@field margin number?
|
||||||
|
---@field scroller_width number?
|
||||||
|
---@field alignment snowcap.widget.v0alpha1.ScrollableAlignment?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Scrollable
|
||||||
|
---@field width snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field height snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field direction snowcap.widget.v0alpha1.ScrollableDirection?
|
||||||
|
---@field child snowcap.widget.v0alpha1.WidgetDef?
|
||||||
|
|
||||||
|
---@class snowcap.widget.v0alpha1.Container
|
||||||
|
---@field padding snowcap.widget.v0alpha1.Padding?
|
||||||
|
---@field width snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field height snowcap.widget.v0alpha1.Length?
|
||||||
|
---@field max_width number?
|
||||||
|
---@field max_height number?
|
||||||
|
---@field horizontal_alignment snowcap.widget.v0alpha1.Alignment?
|
||||||
|
---@field vertical_alignment snowcap.widget.v0alpha1.Alignment?
|
||||||
|
---@field clip boolean?
|
||||||
|
---@field child snowcap.widget.v0alpha1.WidgetDef?
|
||||||
|
---@field text_color snowcap.widget.v0alpha1.Color?
|
||||||
|
---@field background_color snowcap.widget.v0alpha1.Color?
|
||||||
|
---@field border_radius number?
|
||||||
|
---@field border_thickness number?
|
||||||
|
---@field border_color snowcap.widget.v0alpha1.Color?
|
||||||
|
|
||||||
|
---@class snowcap.layer.v0alpha1.NewLayerRequest
|
||||||
|
---@field widget_def snowcap.widget.v0alpha1.WidgetDef?
|
||||||
|
---@field width integer?
|
||||||
|
---@field height integer?
|
||||||
|
---@field anchor snowcap.layer.v0alpha1.Anchor?
|
||||||
|
---@field keyboard_interactivity snowcap.layer.v0alpha1.KeyboardInteractivity?
|
||||||
|
---@field exclusive_zone integer?
|
||||||
|
---@field layer snowcap.layer.v0alpha1.Layer?
|
||||||
|
|
||||||
|
---@class snowcap.layer.v0alpha1.NewLayerResponse
|
||||||
|
---@field layer_id integer?
|
||||||
|
|
||||||
|
---@class snowcap.layer.v0alpha1.CloseRequest
|
||||||
|
---@field layer_id integer?
|
||||||
|
|
||||||
|
---@class snowcap.v0alpha1.Nothing
|
||||||
|
|
||||||
|
|
||||||
|
local snowcap = {}
|
||||||
|
snowcap.input = {}
|
||||||
|
snowcap.input.v0alpha1 = {}
|
||||||
|
snowcap.input.v0alpha1.Modifiers = {}
|
||||||
|
snowcap.input.v0alpha1.KeyboardKeyRequest = {}
|
||||||
|
snowcap.input.v0alpha1.KeyboardKeyResponse = {}
|
||||||
|
snowcap.input.v0alpha1.PointerButtonRequest = {}
|
||||||
|
snowcap.input.v0alpha1.PointerButtonResponse = {}
|
||||||
|
local google = {}
|
||||||
|
google.protobuf = {}
|
||||||
|
google.protobuf.Empty = {}
|
||||||
|
snowcap.widget = {}
|
||||||
|
snowcap.widget.v0alpha1 = {}
|
||||||
|
snowcap.widget.v0alpha1.Padding = {}
|
||||||
|
snowcap.widget.v0alpha1.Length = {}
|
||||||
|
snowcap.widget.v0alpha1.Color = {}
|
||||||
|
snowcap.widget.v0alpha1.Font = {}
|
||||||
|
snowcap.widget.v0alpha1.Font.Family = {}
|
||||||
|
snowcap.widget.v0alpha1.WidgetDef = {}
|
||||||
|
snowcap.widget.v0alpha1.Text = {}
|
||||||
|
snowcap.widget.v0alpha1.Column = {}
|
||||||
|
snowcap.widget.v0alpha1.Row = {}
|
||||||
|
snowcap.widget.v0alpha1.ScrollableDirection = {}
|
||||||
|
snowcap.widget.v0alpha1.ScrollableProperties = {}
|
||||||
|
snowcap.widget.v0alpha1.Scrollable = {}
|
||||||
|
snowcap.widget.v0alpha1.Container = {}
|
||||||
|
snowcap.layer = {}
|
||||||
|
snowcap.layer.v0alpha1 = {}
|
||||||
|
snowcap.layer.v0alpha1.NewLayerRequest = {}
|
||||||
|
snowcap.layer.v0alpha1.NewLayerResponse = {}
|
||||||
|
snowcap.layer.v0alpha1.CloseRequest = {}
|
||||||
|
snowcap.v0alpha1 = {}
|
||||||
|
snowcap.v0alpha1.Nothing = {}
|
||||||
|
|
||||||
|
snowcap.widget.v0alpha1.Alignment = snowcap_widget_v0alpha1_Alignment
|
||||||
|
snowcap.widget.v0alpha1.ScrollableAlignment = snowcap_widget_v0alpha1_ScrollableAlignment
|
||||||
|
snowcap.widget.v0alpha1.Font.Weight = snowcap_widget_v0alpha1_Font_Weight
|
||||||
|
snowcap.widget.v0alpha1.Font.Stretch = snowcap_widget_v0alpha1_Font_Stretch
|
||||||
|
snowcap.widget.v0alpha1.Font.Style = snowcap_widget_v0alpha1_Font_Style
|
||||||
|
snowcap.layer.v0alpha1.Anchor = snowcap_layer_v0alpha1_Anchor
|
||||||
|
snowcap.layer.v0alpha1.KeyboardInteractivity = snowcap_layer_v0alpha1_KeyboardInteractivity
|
||||||
|
snowcap.layer.v0alpha1.Layer = snowcap_layer_v0alpha1_Layer
|
||||||
|
|
||||||
|
snowcap.input.v0alpha1.InputService = {}
|
||||||
|
snowcap.input.v0alpha1.InputService.KeyboardKey = {}
|
||||||
|
snowcap.input.v0alpha1.InputService.KeyboardKey.service = "snowcap.input.v0alpha1.InputService"
|
||||||
|
snowcap.input.v0alpha1.InputService.KeyboardKey.method = "KeyboardKey"
|
||||||
|
snowcap.input.v0alpha1.InputService.KeyboardKey.request = ".snowcap.input.v0alpha1.KeyboardKeyRequest"
|
||||||
|
snowcap.input.v0alpha1.InputService.KeyboardKey.response = ".snowcap.input.v0alpha1.KeyboardKeyResponse"
|
||||||
|
snowcap.input.v0alpha1.InputService.PointerButton = {}
|
||||||
|
snowcap.input.v0alpha1.InputService.PointerButton.service = "snowcap.input.v0alpha1.InputService"
|
||||||
|
snowcap.input.v0alpha1.InputService.PointerButton.method = "PointerButton"
|
||||||
|
snowcap.input.v0alpha1.InputService.PointerButton.request = ".snowcap.input.v0alpha1.PointerButtonRequest"
|
||||||
|
snowcap.input.v0alpha1.InputService.PointerButton.response = ".snowcap.input.v0alpha1.PointerButtonResponse"
|
||||||
|
snowcap.layer.v0alpha1.LayerService = {}
|
||||||
|
snowcap.layer.v0alpha1.LayerService.NewLayer = {}
|
||||||
|
snowcap.layer.v0alpha1.LayerService.NewLayer.service = "snowcap.layer.v0alpha1.LayerService"
|
||||||
|
snowcap.layer.v0alpha1.LayerService.NewLayer.method = "NewLayer"
|
||||||
|
snowcap.layer.v0alpha1.LayerService.NewLayer.request = ".snowcap.layer.v0alpha1.NewLayerRequest"
|
||||||
|
snowcap.layer.v0alpha1.LayerService.NewLayer.response = ".snowcap.layer.v0alpha1.NewLayerResponse"
|
||||||
|
snowcap.layer.v0alpha1.LayerService.Close = {}
|
||||||
|
snowcap.layer.v0alpha1.LayerService.Close.service = "snowcap.layer.v0alpha1.LayerService"
|
||||||
|
snowcap.layer.v0alpha1.LayerService.Close.method = "Close"
|
||||||
|
snowcap.layer.v0alpha1.LayerService.Close.request = ".snowcap.layer.v0alpha1.CloseRequest"
|
||||||
|
snowcap.layer.v0alpha1.LayerService.Close.response = ".google.protobuf.Empty"
|
||||||
|
|
||||||
|
return {
|
||||||
|
snowcap = snowcap,
|
||||||
|
google = google,
|
||||||
|
}
|
||||||
|
|
66
snowcap/api/lua/snowcap/grpc/protobuf.lua
Normal file
66
snowcap/api/lua/snowcap/grpc/protobuf.lua
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
require("compat53")
|
||||||
|
|
||||||
|
local pb = require("pb")
|
||||||
|
|
||||||
|
local protobuf = {}
|
||||||
|
|
||||||
|
local SNOWCAP_PROTO_DIR = (os.getenv("XDG_DATA_HOME") or (os.getenv("HOME") .. "/.local/share"))
|
||||||
|
.. "/snowcap/protobuf"
|
||||||
|
|
||||||
|
function protobuf.build_protos()
|
||||||
|
local version = "v0alpha1"
|
||||||
|
local proto_file_paths = {
|
||||||
|
SNOWCAP_PROTO_DIR .. "/snowcap/input/" .. version .. "/input.proto",
|
||||||
|
SNOWCAP_PROTO_DIR .. "/snowcap/layer/" .. version .. "/layer.proto",
|
||||||
|
SNOWCAP_PROTO_DIR .. "/snowcap/widget/" .. version .. "/widget.proto",
|
||||||
|
SNOWCAP_PROTO_DIR .. "/google/protobuf/empty.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
local cmd = "protoc --descriptor_set_out=/tmp/snowcap.pb --proto_path="
|
||||||
|
.. SNOWCAP_PROTO_DIR
|
||||||
|
.. " "
|
||||||
|
|
||||||
|
for _, file_path in ipairs(proto_file_paths) do
|
||||||
|
cmd = cmd .. file_path .. " "
|
||||||
|
end
|
||||||
|
|
||||||
|
local proc = assert(io.popen(cmd), "protoc is not installed")
|
||||||
|
local _ = proc:read("a")
|
||||||
|
proc:close()
|
||||||
|
|
||||||
|
local snowcap_pb = assert(io.open("/tmp/snowcap.pb", "r"), "no pb file generated")
|
||||||
|
local snowcap_pb_data = snowcap_pb:read("a")
|
||||||
|
snowcap_pb:close()
|
||||||
|
|
||||||
|
assert(pb.load(snowcap_pb_data), "failed to load .pb file")
|
||||||
|
|
||||||
|
pb.option("enum_as_value")
|
||||||
|
end
|
||||||
|
|
||||||
|
---@nodoc
|
||||||
|
---Encode the given `data` as the protobuf `type`.
|
||||||
|
---@param type string The absolute protobuf type
|
||||||
|
---@param data table The table of data, conforming to its protobuf definition
|
||||||
|
---@return string buffer The encoded buffer
|
||||||
|
function protobuf.encode(type, data)
|
||||||
|
local success, obj = pcall(pb.encode, type, data)
|
||||||
|
if not success then
|
||||||
|
print("failed to encode:", obj, "type:", type)
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local encoded_protobuf = obj
|
||||||
|
|
||||||
|
local packed_prefix = string.pack("I1", 0)
|
||||||
|
local payload_len = string.pack(">I4", encoded_protobuf:len())
|
||||||
|
|
||||||
|
local body = packed_prefix .. payload_len .. encoded_protobuf
|
||||||
|
|
||||||
|
return body
|
||||||
|
end
|
||||||
|
|
||||||
|
return protobuf
|
15
snowcap/api/lua/snowcap/input.lua
Normal file
15
snowcap/api/lua/snowcap/input.lua
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
local input = {
|
||||||
|
key = require("snowcap.input.keys"),
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class snowcap.input.Modifiers
|
||||||
|
---@field shift boolean
|
||||||
|
---@field ctrl boolean
|
||||||
|
---@field alt boolean
|
||||||
|
---@field super boolean
|
||||||
|
|
||||||
|
return input
|
4320
snowcap/api/lua/snowcap/input/keys.lua
Normal file
4320
snowcap/api/lua/snowcap/input/keys.lua
Normal file
File diff suppressed because it is too large
Load diff
158
snowcap/api/lua/snowcap/layer.lua
Normal file
158
snowcap/api/lua/snowcap/layer.lua
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
local log = require("snowcap.log")
|
||||||
|
local client = require("snowcap.grpc.client").client
|
||||||
|
local layer_service = require("snowcap.grpc.defs").snowcap.layer.v0alpha1.LayerService
|
||||||
|
local input_service = require("snowcap.grpc.defs").snowcap.input.v0alpha1.InputService
|
||||||
|
|
||||||
|
local widget = require("snowcap.widget")
|
||||||
|
|
||||||
|
---@class Layer
|
||||||
|
local layer = {}
|
||||||
|
|
||||||
|
---@class LayerHandleModule
|
||||||
|
local layer_handle = {}
|
||||||
|
|
||||||
|
---@class LayerHandle
|
||||||
|
---@field id integer
|
||||||
|
local LayerHandle = {}
|
||||||
|
|
||||||
|
function layer_handle.new(id)
|
||||||
|
---@type LayerHandle
|
||||||
|
local self = {
|
||||||
|
id = id,
|
||||||
|
}
|
||||||
|
setmetatable(self, { __index = LayerHandle })
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
---@enum snowcap.Anchor
|
||||||
|
local anchor = {
|
||||||
|
TOP = 1,
|
||||||
|
BOTTOM = 2,
|
||||||
|
LEFT = 3,
|
||||||
|
RIGHT = 4,
|
||||||
|
TOP_LEFT = 5,
|
||||||
|
TOP_RIGHT = 6,
|
||||||
|
BOTTOM_LEFT = 7,
|
||||||
|
BOTTOM_RIGHT = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.KeyboardInteractivity
|
||||||
|
local keyboard_interactivity = {
|
||||||
|
NONE = 1,
|
||||||
|
ON_DEMAND = 2,
|
||||||
|
EXCLUSIVE = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.ZLayer
|
||||||
|
local zlayer = {
|
||||||
|
BACKGROUND = 1,
|
||||||
|
BOTTOM = 2,
|
||||||
|
TOP = 3,
|
||||||
|
OVERLAY = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@alias snowcap.ExclusiveZone
|
||||||
|
---| integer
|
||||||
|
---| "respect"
|
||||||
|
---| "ignore"
|
||||||
|
|
||||||
|
---@param zone snowcap.ExclusiveZone
|
||||||
|
---@return integer
|
||||||
|
local function exclusive_zone_to_api(zone)
|
||||||
|
if type(zone) == "number" then
|
||||||
|
return zone
|
||||||
|
end
|
||||||
|
|
||||||
|
if zone == "respect" then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class LayerArgs
|
||||||
|
---@field widget snowcap.WidgetDef
|
||||||
|
---@field width integer
|
||||||
|
---@field height integer
|
||||||
|
---@field anchor snowcap.Anchor?
|
||||||
|
---@field keyboard_interactivity snowcap.KeyboardInteractivity
|
||||||
|
---@field exclusive_zone snowcap.ExclusiveZone
|
||||||
|
---@field layer snowcap.ZLayer
|
||||||
|
|
||||||
|
---@param args LayerArgs
|
||||||
|
---@return LayerHandle|nil handle A handle to the layer surface, or nil if an error occurred.
|
||||||
|
function layer.new_widget(args)
|
||||||
|
---@type snowcap.layer.v0alpha1.NewLayerRequest
|
||||||
|
local request = {
|
||||||
|
layer = args.layer,
|
||||||
|
exclusive_zone = exclusive_zone_to_api(args.exclusive_zone),
|
||||||
|
width = args.width,
|
||||||
|
height = args.height,
|
||||||
|
anchor = args.anchor,
|
||||||
|
keyboard_interactivity = args.keyboard_interactivity,
|
||||||
|
widget_def = widget.widget_def_into_api(args.widget),
|
||||||
|
}
|
||||||
|
|
||||||
|
local response, err = client:unary_request(layer_service.NewLayer, request)
|
||||||
|
|
||||||
|
if err then
|
||||||
|
log:error(err)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@cast response snowcap.layer.v0alpha1.NewLayerResponse
|
||||||
|
|
||||||
|
if not response.layer_id then
|
||||||
|
log:error("no layer_id received")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return layer_handle.new(response.layer_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param on_press fun(mods: snowcap.input.Modifiers, key: snowcap.Key)
|
||||||
|
function LayerHandle:on_key_press(on_press)
|
||||||
|
local err = client:server_streaming_request(
|
||||||
|
input_service.KeyboardKey,
|
||||||
|
{ id = self.id },
|
||||||
|
function(response)
|
||||||
|
---@cast response snowcap.input.v0alpha1.KeyboardKeyResponse
|
||||||
|
|
||||||
|
if not response.pressed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local mods = response.modifiers or {}
|
||||||
|
mods.shift = mods.shift or false
|
||||||
|
mods.ctrl = mods.ctrl or false
|
||||||
|
mods.alt = mods.alt or false
|
||||||
|
mods.super = mods.super or false
|
||||||
|
|
||||||
|
---@cast mods snowcap.input.Modifiers
|
||||||
|
|
||||||
|
on_press(mods, response.key or 0)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
if err then
|
||||||
|
log:error(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function LayerHandle:close()
|
||||||
|
local _, err = client:unary_request(layer_service.Close, { layer_id = self.id })
|
||||||
|
|
||||||
|
if err then
|
||||||
|
log:error(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
layer.anchor = anchor
|
||||||
|
layer.keyboard_interactivity = keyboard_interactivity
|
||||||
|
layer.zlayer = zlayer
|
||||||
|
|
||||||
|
return layer
|
35
snowcap/api/lua/snowcap/log.lua
Normal file
35
snowcap/api/lua/snowcap/log.lua
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
local logging = require("logging")
|
||||||
|
|
||||||
|
---@class snowcap.Log
|
||||||
|
---@field debug function
|
||||||
|
---@field info function
|
||||||
|
---@field warn function
|
||||||
|
---@field error function
|
||||||
|
---@field fatal function
|
||||||
|
local log = {}
|
||||||
|
|
||||||
|
local log_patterns = logging.buildLogPatterns({
|
||||||
|
[logging.ERROR] = "%level %message (at %source)",
|
||||||
|
}, "%level %message")
|
||||||
|
|
||||||
|
local console_logger = logging.new(function(self, level, message)
|
||||||
|
print(
|
||||||
|
logging.prepareLogMsg(
|
||||||
|
log_patterns[level],
|
||||||
|
logging.date(logging.defaultTimestampPattern()),
|
||||||
|
level,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
end, logging.defaultLevel())
|
||||||
|
|
||||||
|
setmetatable(log, {
|
||||||
|
__index = console_logger,
|
||||||
|
})
|
||||||
|
|
||||||
|
return log
|
252
snowcap/api/lua/snowcap/util.lua
Normal file
252
snowcap/api/lua/snowcap/util.lua
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
---Create `Rectangle`s.
|
||||||
|
---@class RectangleModule
|
||||||
|
local rectangle = {}
|
||||||
|
|
||||||
|
---@classmod
|
||||||
|
---A rectangle with a position and size.
|
||||||
|
---@class Rectangle
|
||||||
|
---@field x number The x-position of the top-left corner
|
||||||
|
---@field y number The y-position of the top-left corner
|
||||||
|
---@field width number The width of the rectangle
|
||||||
|
---@field height number The height of the rectangle
|
||||||
|
local Rectangle = {}
|
||||||
|
|
||||||
|
---Split this rectangle along `axis` at `at`.
|
||||||
|
---
|
||||||
|
---If `thickness` is specified, the split will chop off a section of this
|
||||||
|
---rectangle from `at` to `at + thickness`.
|
||||||
|
---
|
||||||
|
---`at` is relative to the space this rectangle is in, not
|
||||||
|
---this rectangle's origin.
|
||||||
|
---
|
||||||
|
---@param axis "horizontal" | "vertical"
|
||||||
|
---@param at number
|
||||||
|
---@param thickness number?
|
||||||
|
---
|
||||||
|
---@return Rectangle rect1 The first rectangle.
|
||||||
|
---@return Rectangle|nil rect2 The second rectangle, if there is one.
|
||||||
|
function Rectangle:split_at(axis, at, thickness)
|
||||||
|
---@diagnostic disable-next-line: redefined-local
|
||||||
|
local thickness = thickness or 0
|
||||||
|
|
||||||
|
if axis == "horizontal" then
|
||||||
|
-- Split is off to the top, at most chop off to `thickness`
|
||||||
|
if at <= self.y then
|
||||||
|
local diff = at - self.y + thickness
|
||||||
|
if diff > 0 then
|
||||||
|
self.y = self.y + diff
|
||||||
|
self.height = self.height - diff
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
-- Split is to the bottom, then do nothing
|
||||||
|
elseif at >= self.y + self.height then
|
||||||
|
return self
|
||||||
|
-- Split only chops bottom off
|
||||||
|
elseif at + thickness >= self.y + self.height then
|
||||||
|
local diff = (self.y + self.height) - at
|
||||||
|
self.height = self.height - diff
|
||||||
|
return self
|
||||||
|
-- Do a split
|
||||||
|
else
|
||||||
|
local x = self.x
|
||||||
|
local top_y = self.y
|
||||||
|
local width = self.width
|
||||||
|
local top_height = at - self.y
|
||||||
|
|
||||||
|
local bot_y = at + thickness
|
||||||
|
local bot_height = self.y + self.height - at - thickness
|
||||||
|
|
||||||
|
local rect1 = rectangle.new(x, top_y, width, top_height)
|
||||||
|
local rect2 = rectangle.new(x, bot_y, width, bot_height)
|
||||||
|
|
||||||
|
return rect1, rect2
|
||||||
|
end
|
||||||
|
elseif axis == "vertical" then
|
||||||
|
-- Split is off to the left, at most chop off to `thickness`
|
||||||
|
if at <= self.x then
|
||||||
|
local diff = at - self.x + thickness
|
||||||
|
if diff > 0 then
|
||||||
|
self.x = self.x + diff
|
||||||
|
self.width = self.width - diff
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
-- Split is to the right, then do nothing
|
||||||
|
elseif at >= self.x + self.width then
|
||||||
|
return self
|
||||||
|
-- Split only chops bottom off
|
||||||
|
elseif at + thickness >= self.x + self.width then
|
||||||
|
local diff = (self.x + self.width) - at
|
||||||
|
self.width = self.width - diff
|
||||||
|
return self
|
||||||
|
-- Do a split
|
||||||
|
else
|
||||||
|
local left_x = self.x
|
||||||
|
local y = self.y
|
||||||
|
local left_width = at - self.x
|
||||||
|
local height = self.height
|
||||||
|
|
||||||
|
local right_x = at + thickness
|
||||||
|
local right_width = self.x + self.width - at - thickness
|
||||||
|
|
||||||
|
local rect1 = rectangle.new(left_x, y, left_width, height)
|
||||||
|
local rect2 = rectangle.new(right_x, y, right_width, height)
|
||||||
|
|
||||||
|
return rect1, rect2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print("Invalid axis:", axis)
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return Rectangle
|
||||||
|
function rectangle.new(x, y, width, height)
|
||||||
|
---@type Rectangle
|
||||||
|
local self = {
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
}
|
||||||
|
setmetatable(self, { __index = Rectangle })
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
---Utility functions.
|
||||||
|
---@class Util
|
||||||
|
local util = {
|
||||||
|
rectangle = rectangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
---Batch a set of requests that will be sent to the compositor all at once.
|
||||||
|
---
|
||||||
|
---Normally, all API calls are blocking. For example, calling `Window.get_all`
|
||||||
|
---then calling `WindowHandle.props` on each returned window handle will block
|
||||||
|
---after each `props` call waiting for the compositor to respond:
|
||||||
|
---
|
||||||
|
---```
|
||||||
|
---local handles = Window.get_all()
|
||||||
|
---
|
||||||
|
--- -- Collect all the props into this table
|
||||||
|
---local props = {}
|
||||||
|
---
|
||||||
|
--- -- This for loop will block after each call. If the compositor is running slowly
|
||||||
|
--- -- for whatever reason, this will take a long time to complete as it requests
|
||||||
|
--- -- properties sequentially.
|
||||||
|
---for i, handle in ipairs(handles) do
|
||||||
|
--- props[i] = handle:props()
|
||||||
|
---end
|
||||||
|
---```
|
||||||
|
---
|
||||||
|
---In order to mitigate this issue, you can batch up a set of API calls using this function.
|
||||||
|
---This will send all requests to the compositor at once without blocking, then wait for the compositor
|
||||||
|
---to respond.
|
||||||
|
---
|
||||||
|
---You must wrap each request in a function, otherwise they would just get
|
||||||
|
---evaluated at the callsite in a blocking manner.
|
||||||
|
---
|
||||||
|
---### Example
|
||||||
|
---```lua
|
||||||
|
---local handles = window.get_all()
|
||||||
|
---
|
||||||
|
--- ---@type (fun(): WindowProperties)[]
|
||||||
|
---local requests = {}
|
||||||
|
---
|
||||||
|
--- -- Wrap each request to `props` in another function
|
||||||
|
---for i, handle in ipairs(handles) do
|
||||||
|
--- requests[i] = function()
|
||||||
|
--- return handle:props()
|
||||||
|
--- end
|
||||||
|
---end
|
||||||
|
---
|
||||||
|
--- -- Batch send these requests
|
||||||
|
---local props = require("pinnacle.util").batch(requests)
|
||||||
|
--- -- `props` now contains the `WindowProperties` of all the windows above
|
||||||
|
---```
|
||||||
|
---
|
||||||
|
---@generic T
|
||||||
|
---
|
||||||
|
---@param requests (fun(): T)[] The requests that you want to batch up, wrapped in a function.
|
||||||
|
---
|
||||||
|
---@return T[] responses The results of each request in the same order that they were in `requests`.
|
||||||
|
function util.batch(requests)
|
||||||
|
if #requests == 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local loop = require("cqueues").new()
|
||||||
|
|
||||||
|
local responses = {}
|
||||||
|
|
||||||
|
for i, request in ipairs(requests) do
|
||||||
|
loop:wrap(function()
|
||||||
|
responses[i] = request()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
loop:loop()
|
||||||
|
|
||||||
|
return responses
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Taken from the following stackoverflow answer:
|
||||||
|
-- https://stackoverflow.com/a/16077650
|
||||||
|
local function deep_copy_rec(obj, seen)
|
||||||
|
seen = seen or {}
|
||||||
|
if obj == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if seen[obj] then
|
||||||
|
return seen[obj]
|
||||||
|
end
|
||||||
|
|
||||||
|
local no
|
||||||
|
if type(obj) == "table" then
|
||||||
|
no = {}
|
||||||
|
seen[obj] = no
|
||||||
|
|
||||||
|
for k, v in next, obj, nil do
|
||||||
|
no[deep_copy_rec(k, seen)] = deep_copy_rec(v, seen)
|
||||||
|
end
|
||||||
|
setmetatable(no, deep_copy_rec(getmetatable(obj), seen))
|
||||||
|
else -- number, string, boolean, etc
|
||||||
|
no = obj
|
||||||
|
end
|
||||||
|
return no
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create a deep copy of an object.
|
||||||
|
---
|
||||||
|
---@generic T
|
||||||
|
---
|
||||||
|
---@param obj T The object to deep copy.
|
||||||
|
---
|
||||||
|
---@return T deep_copy A deep copy of `obj`
|
||||||
|
function util.deep_copy(obj)
|
||||||
|
return deep_copy_rec(obj, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create a table with entries key->value and value->key for all given pairs.
|
||||||
|
---
|
||||||
|
---@generic T
|
||||||
|
---@param key_value_pairs T
|
||||||
|
---
|
||||||
|
---@return T bijective_table A table with pairs key->value and value->key
|
||||||
|
function util.bijective_table(key_value_pairs)
|
||||||
|
local ret = {}
|
||||||
|
|
||||||
|
for key, value in pairs(key_value_pairs) do
|
||||||
|
ret[key] = value
|
||||||
|
ret[value] = key
|
||||||
|
end
|
||||||
|
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
return util
|
370
snowcap/api/lua/snowcap/widget.lua
Normal file
370
snowcap/api/lua/snowcap/widget.lua
Normal file
|
@ -0,0 +1,370 @@
|
||||||
|
-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
---@class snowcap.WidgetDef
|
||||||
|
---@field text snowcap.Text?
|
||||||
|
---@field column snowcap.Column?
|
||||||
|
---@field row snowcap.Row?
|
||||||
|
---@field scrollable snowcap.Scrollable?
|
||||||
|
---@field container snowcap.Container?
|
||||||
|
|
||||||
|
---@class snowcap.Text
|
||||||
|
---@field text string
|
||||||
|
---@field size number?
|
||||||
|
---@field width snowcap.Length?
|
||||||
|
---@field height snowcap.Length?
|
||||||
|
---@field halign snowcap.Alignment?
|
||||||
|
---@field valign snowcap.Alignment?
|
||||||
|
---@field color snowcap.Color?
|
||||||
|
---@field font snowcap.Font?
|
||||||
|
|
||||||
|
---@class snowcap.Column
|
||||||
|
---@field spacing number?
|
||||||
|
---@field padding snowcap.Padding?
|
||||||
|
---@field item_alignment snowcap.Alignment?
|
||||||
|
---@field width snowcap.Length?
|
||||||
|
---@field height snowcap.Length?
|
||||||
|
---@field max_width number?
|
||||||
|
---@field clip boolean?
|
||||||
|
---@field children snowcap.WidgetDef[]
|
||||||
|
|
||||||
|
---@class snowcap.Row
|
||||||
|
---@field spacing number?
|
||||||
|
---@field padding snowcap.Padding?
|
||||||
|
---@field item_alignment snowcap.Alignment?
|
||||||
|
---@field width snowcap.Length?
|
||||||
|
---@field height snowcap.Length?
|
||||||
|
---@field clip boolean?
|
||||||
|
---@field children snowcap.WidgetDef[]
|
||||||
|
|
||||||
|
---@class snowcap.Scrollable
|
||||||
|
---@field width snowcap.Length?
|
||||||
|
---@field height snowcap.Length?
|
||||||
|
---@field direction snowcap.Scrollable.Direction?
|
||||||
|
---@field child snowcap.WidgetDef
|
||||||
|
|
||||||
|
---@class snowcap.Scrollable.Direction
|
||||||
|
---@field vertical snowcap.Scrollable.Properties?
|
||||||
|
---@field horizontal snowcap.Scrollable.Properties?
|
||||||
|
|
||||||
|
---@class snowcap.Scrollable.Properties
|
||||||
|
---@field width number?
|
||||||
|
---@field height number?
|
||||||
|
---@field scroller_width number?
|
||||||
|
---@field alignment snowcap.Scrollable.Alignment?
|
||||||
|
|
||||||
|
---@class snowcap.Container
|
||||||
|
---@field padding snowcap.Padding?
|
||||||
|
---@field width snowcap.Length?
|
||||||
|
---@field height snowcap.Length?
|
||||||
|
---@field max_width number?
|
||||||
|
---@field max_height number?
|
||||||
|
---@field halign snowcap.Alignment?
|
||||||
|
---@field valign snowcap.Alignment?
|
||||||
|
---@field clip boolean?
|
||||||
|
---@field child snowcap.WidgetDef
|
||||||
|
---@field text_color snowcap.Color?
|
||||||
|
---@field background_color snowcap.Color?
|
||||||
|
---@field border_radius number?
|
||||||
|
---@field border_thickness number?
|
||||||
|
---@field border_color snowcap.Color?
|
||||||
|
|
||||||
|
local scrollable = {
|
||||||
|
---@enum snowcap.Scrollable.Alignment
|
||||||
|
alignment = {
|
||||||
|
START = 1,
|
||||||
|
END = 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class snowcap.Length
|
||||||
|
---@field fill {}?
|
||||||
|
---@field fill_portion integer?
|
||||||
|
---@field shrink {}?
|
||||||
|
---@field fixed number?
|
||||||
|
|
||||||
|
local length = {
|
||||||
|
---@type snowcap.Length
|
||||||
|
Fill = { fill = {} },
|
||||||
|
---@type fun(portion: integer): snowcap.Length
|
||||||
|
FillPortion = function(portion)
|
||||||
|
return { fill_portion = portion }
|
||||||
|
end,
|
||||||
|
---@type snowcap.Length
|
||||||
|
Shrink = { shrink = {} },
|
||||||
|
---@type fun(size: number): snowcap.Length
|
||||||
|
Fixed = function(size)
|
||||||
|
return { fixed = size }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum snowcap.Alignment
|
||||||
|
local alignment = {
|
||||||
|
START = 1,
|
||||||
|
CENTER = 2,
|
||||||
|
END = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class snowcap.Color
|
||||||
|
---@field red number?
|
||||||
|
---@field green number?
|
||||||
|
---@field blue number?
|
||||||
|
---@field alpha number?
|
||||||
|
|
||||||
|
local color = {}
|
||||||
|
|
||||||
|
---@param r number
|
||||||
|
---@param g number
|
||||||
|
---@param b number
|
||||||
|
---@param a number?
|
||||||
|
---
|
||||||
|
---@return snowcap.Color
|
||||||
|
function color.from_rgba(r, g, b, a)
|
||||||
|
return {
|
||||||
|
red = r,
|
||||||
|
green = g,
|
||||||
|
blue = b,
|
||||||
|
alpha = a or 1.0,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class snowcap.Font
|
||||||
|
---@field family snowcap.Font.Family?
|
||||||
|
---@field weight snowcap.Font.Weight?
|
||||||
|
---@field stretch snowcap.Font.Stretch?
|
||||||
|
---@field style snowcap.Font.Style?
|
||||||
|
|
||||||
|
---@class snowcap.Font.Family
|
||||||
|
---@field name string?
|
||||||
|
---@field serif {}?
|
||||||
|
---@field sans_serif {}?
|
||||||
|
---@field cursive {}?
|
||||||
|
---@field fantasy {}?
|
||||||
|
---@field monospace {}?
|
||||||
|
|
||||||
|
local font = {
|
||||||
|
family = {
|
||||||
|
---@type fun(name: string): snowcap.Font.Family
|
||||||
|
Name = function(name)
|
||||||
|
return { name = name }
|
||||||
|
end,
|
||||||
|
---@type snowcap.Font.Family
|
||||||
|
Serif = { serif = {} },
|
||||||
|
---@type snowcap.Font.Family
|
||||||
|
SansSerif = { sans_serif = {} },
|
||||||
|
---@type snowcap.Font.Family
|
||||||
|
Cursive = { cursive = {} },
|
||||||
|
---@type snowcap.Font.Family
|
||||||
|
Fantasy = { fantasy = {} },
|
||||||
|
---@type snowcap.Font.Family
|
||||||
|
Monospace = { monospace = {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
---@enum snowcap.Font.Weight
|
||||||
|
weight = {
|
||||||
|
THIN = 1,
|
||||||
|
EXTRA_LIGHT = 2,
|
||||||
|
LIGHT = 3,
|
||||||
|
NORMAL = 4,
|
||||||
|
MEDIUM = 5,
|
||||||
|
SEMIBOLD = 6,
|
||||||
|
BOLD = 7,
|
||||||
|
EXTRA_BOLD = 8,
|
||||||
|
BLACK = 9,
|
||||||
|
},
|
||||||
|
|
||||||
|
---@enum snowcap.Font.Stretch
|
||||||
|
stretch = {
|
||||||
|
ULTRA_CONDENSED = 1,
|
||||||
|
EXTRA_CONDENSED = 2,
|
||||||
|
CONDENSED = 3,
|
||||||
|
SEMI_CONDENSED = 4,
|
||||||
|
NORMAL = 5,
|
||||||
|
SEMI_EXPANDED = 6,
|
||||||
|
EXPANDED = 7,
|
||||||
|
EXTRA_EXPANDED = 8,
|
||||||
|
ULTRA_EXPANDED = 9,
|
||||||
|
},
|
||||||
|
|
||||||
|
---@enum snowcap.Font.Style
|
||||||
|
style = {
|
||||||
|
NORMAL = 1,
|
||||||
|
ITALIC = 2,
|
||||||
|
OBLIQUE = 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class snowcap.Padding
|
||||||
|
---@field top number?
|
||||||
|
---@field right number?
|
||||||
|
---@field bottom number?
|
||||||
|
---@field left number?
|
||||||
|
|
||||||
|
local widget = {
|
||||||
|
scrollable = scrollable,
|
||||||
|
length = length,
|
||||||
|
alignment = alignment,
|
||||||
|
color = color,
|
||||||
|
font = font,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param def snowcap.Text
|
||||||
|
---@return snowcap.widget.v0alpha1.Text
|
||||||
|
local function text_into_api(def)
|
||||||
|
---@type snowcap.widget.v0alpha1.Text
|
||||||
|
return {
|
||||||
|
text = def.text,
|
||||||
|
pixels = def.size,
|
||||||
|
width = def.width --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
height = def.height --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
vertical_alignment = def.valign,
|
||||||
|
horizontal_alignment = def.halign,
|
||||||
|
color = def.color --[[@as snowcap.widget.v0alpha1.Color]],
|
||||||
|
font = def.font --[[@as snowcap.widget.v0alpha1.Font]],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param def snowcap.Container
|
||||||
|
---@return snowcap.widget.v0alpha1.Container
|
||||||
|
local function container_into_api(def)
|
||||||
|
---@type snowcap.widget.v0alpha1.Container
|
||||||
|
return {
|
||||||
|
padding = def.padding --[[@as snowcap.widget.v0alpha1.Padding]],
|
||||||
|
width = def.width --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
height = def.height --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
max_width = def.max_width,
|
||||||
|
max_height = def.max_height,
|
||||||
|
vertical_alignment = def.valign,
|
||||||
|
horizontal_alignment = def.halign,
|
||||||
|
clip = def.clip,
|
||||||
|
child = widget.widget_def_into_api(def.child),
|
||||||
|
text_color = def.text_color --[[@as snowcap.widget.v0alpha1.Color]],
|
||||||
|
background_color = def.background_color --[[@as snowcap.widget.v0alpha1.Color]],
|
||||||
|
border_radius = def.border_radius,
|
||||||
|
border_thickness = def.border_thickness,
|
||||||
|
border_color = def.border_color --[[@as snowcap.widget.v0alpha1.Color]],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param def snowcap.Column
|
||||||
|
---@return snowcap.widget.v0alpha1.Column
|
||||||
|
local function column_into_api(def)
|
||||||
|
local children = {}
|
||||||
|
for _, child in ipairs(def.children) do
|
||||||
|
table.insert(children, widget.widget_def_into_api(child))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type snowcap.widget.v0alpha1.Column
|
||||||
|
return {
|
||||||
|
width = def.width --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
height = def.height --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
max_width = def.max_width,
|
||||||
|
padding = def.padding --[[@as snowcap.widget.v0alpha1.Padding]],
|
||||||
|
spacing = def.spacing,
|
||||||
|
clip = def.clip,
|
||||||
|
item_alignment = def.item_alignment,
|
||||||
|
children = children,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param def snowcap.Row
|
||||||
|
---@return snowcap.widget.v0alpha1.Row
|
||||||
|
local function row_into_api(def)
|
||||||
|
local children = {}
|
||||||
|
for _, child in ipairs(def.children) do
|
||||||
|
table.insert(children, widget.widget_def_into_api(child))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type snowcap.widget.v0alpha1.Row
|
||||||
|
return {
|
||||||
|
width = def.width --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
height = def.height --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
padding = def.padding --[[@as snowcap.widget.v0alpha1.Padding]],
|
||||||
|
spacing = def.spacing,
|
||||||
|
clip = def.clip,
|
||||||
|
item_alignment = def.item_alignment,
|
||||||
|
children = children,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param def snowcap.Scrollable
|
||||||
|
---@return snowcap.widget.v0alpha1.Scrollable
|
||||||
|
local function scrollable_into_api(def)
|
||||||
|
---@type snowcap.widget.v0alpha1.Scrollable
|
||||||
|
return {
|
||||||
|
width = def.width --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
height = def.height --[[@as snowcap.widget.v0alpha1.Length]],
|
||||||
|
direction = def.direction --[[@as snowcap.widget.v0alpha1.ScrollableDirection]],
|
||||||
|
child = widget.widget_def_into_api(def.child),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param def snowcap.WidgetDef
|
||||||
|
---@return snowcap.widget.v0alpha1.WidgetDef
|
||||||
|
function widget.widget_def_into_api(def)
|
||||||
|
if def.text then
|
||||||
|
def.text = text_into_api(def.text)
|
||||||
|
end
|
||||||
|
if def.container then
|
||||||
|
def.container = container_into_api(def.container)
|
||||||
|
end
|
||||||
|
if def.column then
|
||||||
|
def.column = column_into_api(def.column)
|
||||||
|
end
|
||||||
|
if def.row then
|
||||||
|
def.row = row_into_api(def.row)
|
||||||
|
end
|
||||||
|
if def.scrollable then
|
||||||
|
def.scrollable = scrollable_into_api(def.scrollable)
|
||||||
|
end
|
||||||
|
|
||||||
|
return def --[[@as snowcap.widget.v0alpha1.WidgetDef]]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param text snowcap.Text
|
||||||
|
---
|
||||||
|
---@return snowcap.WidgetDef
|
||||||
|
function widget.text(text)
|
||||||
|
return {
|
||||||
|
text = text,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param column snowcap.Column
|
||||||
|
---
|
||||||
|
---@return snowcap.WidgetDef
|
||||||
|
function widget.column(column)
|
||||||
|
return {
|
||||||
|
column = column,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param row snowcap.Row
|
||||||
|
---
|
||||||
|
---@return snowcap.WidgetDef
|
||||||
|
function widget.row(row)
|
||||||
|
return {
|
||||||
|
row = row,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param scrollable snowcap.Scrollable
|
||||||
|
---
|
||||||
|
---@return snowcap.WidgetDef
|
||||||
|
function widget.scrollable(scrollable)
|
||||||
|
return {
|
||||||
|
scrollable = scrollable,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param container snowcap.Container
|
||||||
|
---
|
||||||
|
---@return snowcap.WidgetDef
|
||||||
|
function widget.container(container)
|
||||||
|
return {
|
||||||
|
container = container,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return widget
|
51
snowcap/api/protobuf/google/protobuf/empty.proto
Normal file
51
snowcap/api/protobuf/google/protobuf/empty.proto
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Protocol Buffers - Google's data interchange format
|
||||||
|
// Copyright 2008 Google Inc. All rights reserved.
|
||||||
|
// https://developers.google.com/protocol-buffers/
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following disclaimer
|
||||||
|
// in the documentation and/or other materials provided with the
|
||||||
|
// distribution.
|
||||||
|
// * Neither the name of Google Inc. nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package google.protobuf;
|
||||||
|
|
||||||
|
option cc_enable_arenas = true;
|
||||||
|
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||||
|
option go_package = "google.golang.org/protobuf/types/known/emptypb";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
option java_outer_classname = "EmptyProto";
|
||||||
|
option java_package = "com.google.protobuf";
|
||||||
|
option objc_class_prefix = "GPB";
|
||||||
|
|
||||||
|
// A generic empty message that you can re-use to avoid defining duplicated
|
||||||
|
// empty messages in your APIs. A typical example is to use it as the request
|
||||||
|
// or the response type of an API method. For instance:
|
||||||
|
//
|
||||||
|
// service Foo {
|
||||||
|
// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
message Empty {}
|
36
snowcap/api/protobuf/snowcap/input/v0alpha1/input.proto
Normal file
36
snowcap/api/protobuf/snowcap/input/v0alpha1/input.proto
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package snowcap.input.v0alpha1;
|
||||||
|
|
||||||
|
// import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
message Modifiers {
|
||||||
|
optional bool shift = 1;
|
||||||
|
optional bool ctrl = 2;
|
||||||
|
optional bool alt = 3;
|
||||||
|
optional bool super = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyboardKeyRequest {
|
||||||
|
optional uint32 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyboardKeyResponse {
|
||||||
|
optional uint32 key = 1;
|
||||||
|
optional Modifiers modifiers = 2;
|
||||||
|
optional bool pressed = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PointerButtonRequest {
|
||||||
|
optional uint32 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PointerButtonResponse {
|
||||||
|
optional uint32 button = 1;
|
||||||
|
optional bool pressed = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
service InputService {
|
||||||
|
rpc KeyboardKey(KeyboardKeyRequest) returns (stream KeyboardKeyResponse);
|
||||||
|
rpc PointerButton(PointerButtonRequest) returns (stream PointerButtonResponse);
|
||||||
|
}
|
56
snowcap/api/protobuf/snowcap/layer/v0alpha1/layer.proto
Normal file
56
snowcap/api/protobuf/snowcap/layer/v0alpha1/layer.proto
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package snowcap.layer.v0alpha1;
|
||||||
|
|
||||||
|
import "snowcap/widget/v0alpha1/widget.proto";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
enum Anchor {
|
||||||
|
ANCHOR_UNSPECIFIED = 0;
|
||||||
|
ANCHOR_TOP = 1;
|
||||||
|
ANCHOR_BOTTOM = 2;
|
||||||
|
ANCHOR_LEFT = 3;
|
||||||
|
ANCHOR_RIGHT = 4;
|
||||||
|
ANCHOR_TOP_LEFT = 5;
|
||||||
|
ANCHOR_TOP_RIGHT = 6;
|
||||||
|
ANCHOR_BOTTOM_LEFT = 7;
|
||||||
|
ANCHOR_BOTTOM_RIGHT = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyboardInteractivity {
|
||||||
|
KEYBOARD_INTERACTIVITY_UNSPECIFIED = 0;
|
||||||
|
KEYBOARD_INTERACTIVITY_NONE = 1;
|
||||||
|
KEYBOARD_INTERACTIVITY_ON_DEMAND = 2;
|
||||||
|
KEYBOARD_INTERACTIVITY_EXCLUSIVE = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Layer {
|
||||||
|
LAYER_UNSPECIFIED = 0;
|
||||||
|
LAYER_BACKGROUND = 1;
|
||||||
|
LAYER_BOTTOM = 2;
|
||||||
|
LAYER_TOP = 3;
|
||||||
|
LAYER_OVERLAY = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NewLayerRequest {
|
||||||
|
optional snowcap.widget.v0alpha1.WidgetDef widget_def = 1;
|
||||||
|
optional uint32 width = 2;
|
||||||
|
optional uint32 height = 3;
|
||||||
|
optional Anchor anchor = 4;
|
||||||
|
optional KeyboardInteractivity keyboard_interactivity = 5;
|
||||||
|
optional int32 exclusive_zone = 6;
|
||||||
|
optional Layer layer = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NewLayerResponse {
|
||||||
|
optional uint32 layer_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CloseRequest {
|
||||||
|
optional uint32 layer_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
service LayerService {
|
||||||
|
rpc NewLayer(NewLayerRequest) returns (NewLayerResponse);
|
||||||
|
rpc Close(CloseRequest) returns (google.protobuf.Empty);
|
||||||
|
}
|
5
snowcap/api/protobuf/snowcap/v0alpha1/snowcap.proto
Normal file
5
snowcap/api/protobuf/snowcap/v0alpha1/snowcap.proto
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package snowcap.v0alpha1;
|
||||||
|
|
||||||
|
message Nothing {}
|
174
snowcap/api/protobuf/snowcap/widget/v0alpha1/widget.proto
Normal file
174
snowcap/api/protobuf/snowcap/widget/v0alpha1/widget.proto
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package snowcap.widget.v0alpha1;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
message Padding {
|
||||||
|
optional float top = 1;
|
||||||
|
optional float right = 2;
|
||||||
|
optional float bottom = 3;
|
||||||
|
optional float left = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Alignment {
|
||||||
|
ALIGNMENT_UNSPECIFIED = 0;
|
||||||
|
ALIGNMENT_START = 1;
|
||||||
|
ALIGNMENT_CENTER = 2;
|
||||||
|
ALIGNMENT_END = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Length {
|
||||||
|
oneof strategy {
|
||||||
|
google.protobuf.Empty fill = 1;
|
||||||
|
uint32 fill_portion = 2;
|
||||||
|
google.protobuf.Empty shrink = 3;
|
||||||
|
float fixed = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Color {
|
||||||
|
optional float red = 1;
|
||||||
|
optional float green = 2;
|
||||||
|
optional float blue = 3;
|
||||||
|
optional float alpha = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Font {
|
||||||
|
message Family {
|
||||||
|
oneof family {
|
||||||
|
string name = 1;
|
||||||
|
google.protobuf.Empty serif = 2;
|
||||||
|
google.protobuf.Empty sans_serif = 3;
|
||||||
|
google.protobuf.Empty cursive = 4;
|
||||||
|
google.protobuf.Empty fantasy = 5;
|
||||||
|
google.protobuf.Empty monospace = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Weight {
|
||||||
|
WEIGHT_UNSPECIFIED = 0;
|
||||||
|
WEIGHT_THIN = 1;
|
||||||
|
WEIGHT_EXTRA_LIGHT = 2;
|
||||||
|
WEIGHT_LIGHT = 3;
|
||||||
|
WEIGHT_NORMAL = 4;
|
||||||
|
WEIGHT_MEDIUM = 5;
|
||||||
|
WEIGHT_SEMIBOLD = 6;
|
||||||
|
WEIGHT_BOLD = 7;
|
||||||
|
WEIGHT_EXTRA_BOLD = 8;
|
||||||
|
WEIGHT_BLACK = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Stretch {
|
||||||
|
STRETCH_UNSPECIFIED = 0;
|
||||||
|
STRETCH_ULTRA_CONDENSED = 1;
|
||||||
|
STRETCH_EXTRA_CONDENSED = 2;
|
||||||
|
STRETCH_CONDENSED = 3;
|
||||||
|
STRETCH_SEMI_CONDENSED = 4;
|
||||||
|
STRETCH_NORMAL = 5;
|
||||||
|
STRETCH_SEMI_EXPANDED = 6;
|
||||||
|
STRETCH_EXPANDED = 7;
|
||||||
|
STRETCH_EXTRA_EXPANDED = 8;
|
||||||
|
STRETCH_ULTRA_EXPANDED = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Style {
|
||||||
|
STYLE_UNSPECIFIED = 0;
|
||||||
|
STYLE_NORMAL = 1;
|
||||||
|
STYLE_ITALIC = 2;
|
||||||
|
STYLE_OBLIQUE = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Family family = 1;
|
||||||
|
optional Weight weight = 2;
|
||||||
|
optional Stretch stretch = 3;
|
||||||
|
optional Style style = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message WidgetDef {
|
||||||
|
oneof widget {
|
||||||
|
Text text = 1;
|
||||||
|
Column column = 2;
|
||||||
|
Row row = 3;
|
||||||
|
Scrollable scrollable = 4;
|
||||||
|
Container container = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Text {
|
||||||
|
optional string text = 1;
|
||||||
|
optional float pixels = 2;
|
||||||
|
optional Length width = 3;
|
||||||
|
optional Length height = 4;
|
||||||
|
optional Alignment horizontal_alignment = 5;
|
||||||
|
optional Alignment vertical_alignment = 6;
|
||||||
|
optional Color color = 7;
|
||||||
|
optional Font font = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Column {
|
||||||
|
optional float spacing = 1;
|
||||||
|
optional Padding padding = 2;
|
||||||
|
optional Alignment item_alignment = 3;
|
||||||
|
optional Length width = 4;
|
||||||
|
optional Length height = 5;
|
||||||
|
optional float max_width = 6;
|
||||||
|
optional bool clip = 7;
|
||||||
|
repeated WidgetDef children = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Row {
|
||||||
|
optional float spacing = 1;
|
||||||
|
optional Padding padding = 2;
|
||||||
|
optional Alignment item_alignment = 3;
|
||||||
|
optional Length width = 4;
|
||||||
|
optional Length height = 5;
|
||||||
|
optional bool clip = 6;
|
||||||
|
repeated WidgetDef children = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ScrollableDirection {
|
||||||
|
optional ScrollableProperties vertical = 1;
|
||||||
|
optional ScrollableProperties horizontal = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ScrollableAlignment {
|
||||||
|
SCROLLABLE_ALIGNMENT_UNSPECIFIED = 0;
|
||||||
|
SCROLLABLE_ALIGNMENT_START = 1;
|
||||||
|
SCROLLABLE_ALIGNMENT_END = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ScrollableProperties {
|
||||||
|
optional float width = 1;
|
||||||
|
optional float margin = 2;
|
||||||
|
optional float scroller_width = 3;
|
||||||
|
optional ScrollableAlignment alignment = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Scrollable {
|
||||||
|
optional Length width = 1;
|
||||||
|
optional Length height = 2;
|
||||||
|
optional ScrollableDirection direction = 3;
|
||||||
|
optional WidgetDef child = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Container {
|
||||||
|
optional Padding padding = 1;
|
||||||
|
optional Length width = 2;
|
||||||
|
optional Length height = 3;
|
||||||
|
optional float max_width = 4;
|
||||||
|
optional float max_height = 5;
|
||||||
|
optional Alignment horizontal_alignment = 6;
|
||||||
|
optional Alignment vertical_alignment = 7;
|
||||||
|
optional bool clip = 8;
|
||||||
|
optional WidgetDef child = 9;
|
||||||
|
|
||||||
|
// styling
|
||||||
|
|
||||||
|
optional Color text_color = 10;
|
||||||
|
optional Color background_color = 11; // TODO: gradient
|
||||||
|
optional float border_radius = 12;
|
||||||
|
optional float border_thickness = 13;
|
||||||
|
optional Color border_color = 14;
|
||||||
|
}
|
24
snowcap/api/rust/Cargo.toml
Normal file
24
snowcap/api/rust/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "snowcap-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
snowcap-api-defs = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tokio-stream = { workspace = true }
|
||||||
|
tonic = { workspace = true }
|
||||||
|
tower = { version = "0.4.13", features = ["util"] }
|
||||||
|
futures = "0.3.30"
|
||||||
|
xdg = { workspace = true }
|
||||||
|
xkbcommon = { workspace = true }
|
||||||
|
from_variants = "1.0.2"
|
||||||
|
tracing = { workspace = true }
|
||||||
|
thiserror = "1.0.62"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
missing_docs = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
too_many_arguments = "allow"
|
||||||
|
type_complexity = "allow"
|
119
snowcap/api/rust/examples/default_config/main.rs
Normal file
119
snowcap/api/rust/examples/default_config/main.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
use snowcap_api::{
|
||||||
|
layer::{ExclusiveZone, KeyboardInteractivity, ZLayer},
|
||||||
|
widget::{
|
||||||
|
font::{Family, Font, Weight},
|
||||||
|
Alignment, Color, Column, Container, Length, Padding, Row, Text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let layer = snowcap_api::connect().await.unwrap();
|
||||||
|
|
||||||
|
let test_key_descs = [
|
||||||
|
("Super + Enter", "Open alacritty"),
|
||||||
|
("Super + M", "Toggle maximized"),
|
||||||
|
("Super + F", "Toggle fullscreen"),
|
||||||
|
("Super + Shift + Q", "Exit Pinnacle"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let widget = Container::new(Row::new_with_children([
|
||||||
|
Column::new_with_children(
|
||||||
|
test_key_descs
|
||||||
|
.iter()
|
||||||
|
.map(|(keys, _)| Text::new(keys).into()),
|
||||||
|
)
|
||||||
|
.width(Length::FillPortion(1))
|
||||||
|
.into(),
|
||||||
|
Column::new_with_children(
|
||||||
|
test_key_descs
|
||||||
|
.iter()
|
||||||
|
.map(|(_, desc)| {
|
||||||
|
Text::new(desc)
|
||||||
|
.horizontal_alignment(Alignment::End)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.font(
|
||||||
|
Font::new_with_family(Family::Name(
|
||||||
|
"JetBrainsMono Nerd Font".to_string(),
|
||||||
|
))
|
||||||
|
.weight(Weight::Semibold),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.chain([Row::new_with_children([
|
||||||
|
Text::new("first")
|
||||||
|
.horizontal_alignment(Alignment::End)
|
||||||
|
.into(),
|
||||||
|
Container::new(Text::new("alacritty").horizontal_alignment(Alignment::End))
|
||||||
|
.background_color(Color {
|
||||||
|
red: 0.5,
|
||||||
|
green: 0.0,
|
||||||
|
blue: 0.0,
|
||||||
|
alpha: 1.0,
|
||||||
|
})
|
||||||
|
.width(Length::Shrink)
|
||||||
|
.horizontal_alignment(Alignment::End)
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.into()]),
|
||||||
|
)
|
||||||
|
.width(Length::FillPortion(1))
|
||||||
|
.item_alignment(Alignment::End)
|
||||||
|
.into(),
|
||||||
|
]))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.padding(Padding {
|
||||||
|
top: 12.0,
|
||||||
|
right: 12.0,
|
||||||
|
bottom: 12.0,
|
||||||
|
left: 12.0,
|
||||||
|
})
|
||||||
|
.border_radius(64.0)
|
||||||
|
.border_thickness(6.0);
|
||||||
|
|
||||||
|
layer
|
||||||
|
.new_widget(
|
||||||
|
widget,
|
||||||
|
400,
|
||||||
|
500,
|
||||||
|
None,
|
||||||
|
KeyboardInteractivity::Exclusive,
|
||||||
|
ExclusiveZone::Respect,
|
||||||
|
ZLayer::Top,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.on_key_press(|handle, _key, _mods| {
|
||||||
|
dbg!(_key);
|
||||||
|
if _key == xkbcommon::xkb::Keysym::Escape {
|
||||||
|
println!("closing");
|
||||||
|
handle.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
snowcap_api::listen().await;
|
||||||
|
|
||||||
|
// let widget = layer.new_widget(...);
|
||||||
|
//
|
||||||
|
// widget.close();
|
||||||
|
|
||||||
|
// layer.new_widget(...)
|
||||||
|
// .on_key_press(|widget, key, mods| {
|
||||||
|
// if key == Key::Escape {
|
||||||
|
// widget.close();
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// OR
|
||||||
|
//
|
||||||
|
// let widget = layer.new_widget(...);
|
||||||
|
//
|
||||||
|
// widget.on_key_press(|key, mods| {
|
||||||
|
// if key == Key::Escape {
|
||||||
|
// widget.close();
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
}
|
24
snowcap/api/rust/src/input.rs
Normal file
24
snowcap/api/rust/src/input.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
//! Input types.
|
||||||
|
|
||||||
|
use snowcap_api_defs::snowcap::input;
|
||||||
|
|
||||||
|
/// Keyboard modifiers.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Modifiers {
|
||||||
|
pub shift: bool,
|
||||||
|
pub ctrl: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
pub logo: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<input::v0alpha1::Modifiers> for Modifiers {
|
||||||
|
fn from(value: input::v0alpha1::Modifiers) -> Self {
|
||||||
|
Self {
|
||||||
|
shift: value.shift(),
|
||||||
|
ctrl: value.ctrl(),
|
||||||
|
alt: value.alt(),
|
||||||
|
logo: value.super_(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
211
snowcap/api/rust/src/layer.rs
Normal file
211
snowcap/api/rust/src/layer.rs
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
//! Support for layer surface widgets using `wlr-layer-shell`.
|
||||||
|
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
|
use snowcap_api_defs::snowcap::{
|
||||||
|
input::v0alpha1::KeyboardKeyRequest,
|
||||||
|
layer::{
|
||||||
|
self,
|
||||||
|
v0alpha1::{CloseRequest, NewLayerRequest},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
use tracing::error;
|
||||||
|
use xkbcommon::xkb::Keysym;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
block_on_tokio,
|
||||||
|
input::Modifiers,
|
||||||
|
widget::{WidgetDef, WidgetId},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The Layer API.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Layer;
|
||||||
|
|
||||||
|
// TODO: change to bitflag
|
||||||
|
/// An anchor for a layer surface.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Anchor {
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomLeft,
|
||||||
|
BottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Anchor> for layer::v0alpha1::Anchor {
|
||||||
|
fn from(value: Anchor) -> Self {
|
||||||
|
match value {
|
||||||
|
Anchor::Top => layer::v0alpha1::Anchor::Top,
|
||||||
|
Anchor::Bottom => layer::v0alpha1::Anchor::Bottom,
|
||||||
|
Anchor::Left => layer::v0alpha1::Anchor::Left,
|
||||||
|
Anchor::Right => layer::v0alpha1::Anchor::Right,
|
||||||
|
Anchor::TopLeft => layer::v0alpha1::Anchor::TopLeft,
|
||||||
|
Anchor::TopRight => layer::v0alpha1::Anchor::TopRight,
|
||||||
|
Anchor::BottomLeft => layer::v0alpha1::Anchor::BottomLeft,
|
||||||
|
Anchor::BottomRight => layer::v0alpha1::Anchor::BottomRight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layer surface keyboard interactivity.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum KeyboardInteractivity {
|
||||||
|
/// This layer surface cannot get keyboard focus.
|
||||||
|
None,
|
||||||
|
/// This layer surface can get keyboard focus through the compositor's implementation.
|
||||||
|
OnDemand,
|
||||||
|
/// This layer surface will take exclusive keyboard focus.
|
||||||
|
Exclusive,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KeyboardInteractivity> for layer::v0alpha1::KeyboardInteractivity {
|
||||||
|
fn from(value: KeyboardInteractivity) -> Self {
|
||||||
|
match value {
|
||||||
|
KeyboardInteractivity::None => layer::v0alpha1::KeyboardInteractivity::None,
|
||||||
|
KeyboardInteractivity::OnDemand => layer::v0alpha1::KeyboardInteractivity::OnDemand,
|
||||||
|
KeyboardInteractivity::Exclusive => layer::v0alpha1::KeyboardInteractivity::Exclusive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layer surface behavior for exclusive zones.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ExclusiveZone {
|
||||||
|
/// This layer surface requests an exclusive zone of the given size.
|
||||||
|
Exclusive(NonZeroU32),
|
||||||
|
/// The layer surface does not request an exclusive zone but wants to be
|
||||||
|
/// positioned respecting any active exclusive zones.
|
||||||
|
Respect,
|
||||||
|
/// The layer surface does not request an exclusive zone and wants to be
|
||||||
|
/// positioned ignoring any active exclusive zones.
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExclusiveZone> for i32 {
|
||||||
|
fn from(value: ExclusiveZone) -> Self {
|
||||||
|
match value {
|
||||||
|
ExclusiveZone::Exclusive(size) => size.get() as i32,
|
||||||
|
ExclusiveZone::Respect => 0,
|
||||||
|
ExclusiveZone::Ignore => -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layer on which a layer surface will be drawn.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ZLayer {
|
||||||
|
Background,
|
||||||
|
Bottom,
|
||||||
|
Top,
|
||||||
|
Overlay,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ZLayer> for layer::v0alpha1::Layer {
|
||||||
|
fn from(value: ZLayer) -> Self {
|
||||||
|
match value {
|
||||||
|
ZLayer::Background => Self::Background,
|
||||||
|
ZLayer::Bottom => Self::Bottom,
|
||||||
|
ZLayer::Top => Self::Top,
|
||||||
|
ZLayer::Overlay => Self::Overlay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error type for [`Layer::new_widget`].
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum NewLayerError {
|
||||||
|
/// Snowcap returned a gRPC error status.
|
||||||
|
#[error("gRPC error: `{0}`")]
|
||||||
|
GrpcStatus(#[from] tonic::Status),
|
||||||
|
/// Snowcap did not return a layer id as expected.
|
||||||
|
#[error("snowcap did not return a layer id")]
|
||||||
|
NoLayerId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layer {
|
||||||
|
/// Create a new widget.
|
||||||
|
pub fn new_widget(
|
||||||
|
&self,
|
||||||
|
widget: impl Into<WidgetDef>,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
anchor: Option<Anchor>,
|
||||||
|
keyboard_interactivity: KeyboardInteractivity,
|
||||||
|
exclusive_zone: ExclusiveZone,
|
||||||
|
layer: ZLayer,
|
||||||
|
) -> Result<LayerHandle, NewLayerError> {
|
||||||
|
let response = block_on_tokio(crate::layer().new_layer(NewLayerRequest {
|
||||||
|
widget_def: Some(widget.into().into()),
|
||||||
|
width: Some(width),
|
||||||
|
height: Some(height),
|
||||||
|
anchor: anchor.map(|anchor| layer::v0alpha1::Anchor::from(anchor) as i32),
|
||||||
|
keyboard_interactivity: Some(layer::v0alpha1::KeyboardInteractivity::from(
|
||||||
|
keyboard_interactivity,
|
||||||
|
) as i32),
|
||||||
|
exclusive_zone: Some(exclusive_zone.into()),
|
||||||
|
layer: Some(layer::v0alpha1::Layer::from(layer) as i32),
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
let id = response
|
||||||
|
.into_inner()
|
||||||
|
.layer_id
|
||||||
|
.ok_or(NewLayerError::NoLayerId)?;
|
||||||
|
|
||||||
|
Ok(LayerHandle { id: id.into() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle to a layer surface widget.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct LayerHandle {
|
||||||
|
id: WidgetId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerHandle {
|
||||||
|
/// Close this layer widget.
|
||||||
|
pub fn close(&self) {
|
||||||
|
if let Err(status) = block_on_tokio(crate::layer().close(CloseRequest {
|
||||||
|
layer_id: Some(self.id.into_inner()),
|
||||||
|
})) {
|
||||||
|
error!("Failed to close {self:?}: {status}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do something on key press.
|
||||||
|
pub fn on_key_press(
|
||||||
|
&self,
|
||||||
|
mut on_press: impl FnMut(LayerHandle, Keysym, Modifiers) + Send + 'static,
|
||||||
|
) {
|
||||||
|
let mut stream = match block_on_tokio(crate::input().keyboard_key(KeyboardKeyRequest {
|
||||||
|
id: Some(self.id.into_inner()),
|
||||||
|
})) {
|
||||||
|
Ok(stream) => stream.into_inner(),
|
||||||
|
Err(status) => {
|
||||||
|
error!("Failed to set `on_key_press` handler: {status}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = *self;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(Ok(response)) = stream.next().await {
|
||||||
|
if !response.pressed() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = Keysym::new(response.key());
|
||||||
|
let mods = Modifiers::from(response.modifiers.unwrap_or_default());
|
||||||
|
|
||||||
|
on_press(handle, key, mods);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
89
snowcap/api/rust/src/lib.rs
Normal file
89
snowcap/api/rust/src/lib.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
//! Snowcap: A very, *very* WIP widget system built for [Pinnacle](https://github.com/pinnacle-comp/pinnacle).
|
||||||
|
//!
|
||||||
|
//! [AwesomeWM](https://awesomewm.org/) has a widget system, and Pinnacle is heavily inspired by
|
||||||
|
//! it, thus Snowcap was created.
|
||||||
|
//!
|
||||||
|
//! Snowcap used [Iced](https://iced.rs/) along with Smithay's [client toolkit](https://github.com/Smithay/client-toolkit)
|
||||||
|
//! to draw widgets on screen. The current, *very* early API is mostly a wrapper around Iced's
|
||||||
|
//! widget API and as such closely mirrors it.
|
||||||
|
//!
|
||||||
|
//! Once Snowcap matures a bit, you'll be able to use it in other compositors as well! Many parts
|
||||||
|
//! of Snowcap are designed to be compositor-agnostic. You'll just need a compositor that
|
||||||
|
//! implements the `wlr-layer-shell` protocol.
|
||||||
|
|
||||||
|
pub mod input;
|
||||||
|
pub mod layer;
|
||||||
|
pub mod snowcap;
|
||||||
|
pub mod widget;
|
||||||
|
|
||||||
|
use snowcap_api_defs::snowcap::{
|
||||||
|
input::v0alpha1::input_service_client::InputServiceClient,
|
||||||
|
layer::v0alpha1::layer_service_client::LayerServiceClient,
|
||||||
|
};
|
||||||
|
pub use xkbcommon;
|
||||||
|
|
||||||
|
use std::{path::PathBuf, sync::OnceLock, time::Duration};
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use layer::Layer;
|
||||||
|
use tonic::transport::{Channel, Endpoint, Uri};
|
||||||
|
use tower::service_fn;
|
||||||
|
|
||||||
|
static LAYER: OnceLock<LayerServiceClient<Channel>> = OnceLock::new();
|
||||||
|
static INPUT: OnceLock<InputServiceClient<Channel>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub(crate) fn layer() -> LayerServiceClient<Channel> {
|
||||||
|
LAYER
|
||||||
|
.get()
|
||||||
|
.expect("grpc connection was not initialized")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
pub(crate) fn input() -> InputServiceClient<Channel> {
|
||||||
|
INPUT
|
||||||
|
.get()
|
||||||
|
.expect("grpc connection was not initialized")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_dir() -> PathBuf {
|
||||||
|
xdg::BaseDirectories::with_prefix("snowcap")
|
||||||
|
.and_then(|xdg| xdg.get_runtime_directory().cloned())
|
||||||
|
.unwrap_or(PathBuf::from("/tmp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_name() -> String {
|
||||||
|
let wayland_suffix = std::env::var("WAYLAND_DISPLAY").unwrap_or("wayland-0".into());
|
||||||
|
format!("snowcap-grpc-{wayland_suffix}.sock")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to a running Snowcap instance.
|
||||||
|
///
|
||||||
|
/// Only one snowcap instance can be open per Wayland session.
|
||||||
|
/// This function will search for a Snowcap socket at
|
||||||
|
/// `$XDG_RUNTIME_DIR/$snowcap-grpc-$WAYLAND_DISPLAY.sock` and connect to it.
|
||||||
|
pub async fn connect() -> Result<Layer, Box<dyn std::error::Error>> {
|
||||||
|
let channel = Endpoint::try_from("http://[::]:50051")?
|
||||||
|
.connect_with_connector(service_fn(|_: Uri| {
|
||||||
|
tokio::net::UnixStream::connect(socket_dir().join(socket_name()))
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let _ = LAYER.set(LayerServiceClient::new(channel.clone()));
|
||||||
|
let _ = INPUT.set(InputServiceClient::new(channel.clone()));
|
||||||
|
|
||||||
|
Ok(Layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listen to Snowcap for events.
|
||||||
|
pub async fn listen() {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(Duration::from_secs(u64::MAX)).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn block_on_tokio<F: Future>(future: F) -> F::Output {
|
||||||
|
tokio::task::block_in_place(|| {
|
||||||
|
let handle = tokio::runtime::Handle::current();
|
||||||
|
handle.block_on(future)
|
||||||
|
})
|
||||||
|
}
|
4
snowcap/api/rust/src/snowcap.rs
Normal file
4
snowcap/api/rust/src/snowcap.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//! TODO:
|
||||||
|
|
||||||
|
/// TODO:
|
||||||
|
pub struct Snowcap {}
|
724
snowcap/api/rust/src/widget.rs
Normal file
724
snowcap/api/rust/src/widget.rs
Normal file
|
@ -0,0 +1,724 @@
|
||||||
|
//! Widget definitions.
|
||||||
|
|
||||||
|
#![allow(missing_docs)] // TODO:
|
||||||
|
|
||||||
|
pub mod font;
|
||||||
|
|
||||||
|
use font::Font;
|
||||||
|
use snowcap_api_defs::snowcap::widget;
|
||||||
|
|
||||||
|
/// A unique identifier for a widget.
|
||||||
|
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct WidgetId(u32);
|
||||||
|
|
||||||
|
impl WidgetId {
|
||||||
|
/// Get the raw u32.
|
||||||
|
pub fn into_inner(self) -> u32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for WidgetId {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget definition.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, from_variants::FromVariants)]
|
||||||
|
pub enum WidgetDef {
|
||||||
|
Text(Text),
|
||||||
|
Column(Column),
|
||||||
|
Row(Row),
|
||||||
|
Scrollable(Box<Scrollable>),
|
||||||
|
Container(Box<Container>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Scrollable> for WidgetDef {
|
||||||
|
fn from(value: Scrollable) -> Self {
|
||||||
|
Self::Scrollable(Box::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Container> for WidgetDef {
|
||||||
|
fn from(value: Container) -> Self {
|
||||||
|
Self::Container(Box::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WidgetDef> for widget::v0alpha1::WidgetDef {
|
||||||
|
fn from(value: WidgetDef) -> widget::v0alpha1::WidgetDef {
|
||||||
|
widget::v0alpha1::WidgetDef {
|
||||||
|
widget: Some(match value {
|
||||||
|
WidgetDef::Text(text) => widget::v0alpha1::widget_def::Widget::Text(text.into()),
|
||||||
|
WidgetDef::Column(column) => {
|
||||||
|
widget::v0alpha1::widget_def::Widget::Column(column.into())
|
||||||
|
}
|
||||||
|
WidgetDef::Row(row) => widget::v0alpha1::widget_def::Widget::Row(row.into()),
|
||||||
|
WidgetDef::Scrollable(scrollable) => {
|
||||||
|
widget::v0alpha1::widget_def::Widget::Scrollable(Box::new((*scrollable).into()))
|
||||||
|
}
|
||||||
|
WidgetDef::Container(container) => {
|
||||||
|
widget::v0alpha1::widget_def::Widget::Container(Box::new((*container).into()))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text widget definition.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Text {
|
||||||
|
pub text: String,
|
||||||
|
pub size: Option<f32>,
|
||||||
|
pub width: Option<Length>,
|
||||||
|
pub height: Option<Length>,
|
||||||
|
pub horizontal_alignment: Option<Alignment>,
|
||||||
|
pub vertical_alignment: Option<Alignment>,
|
||||||
|
pub color: Option<Color>,
|
||||||
|
pub font: Option<Font>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Text {
|
||||||
|
pub fn new(text: impl ToString) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(self, size: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
size: Some(size),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(self, width: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
width: Some(width),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(self, height: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
height: Some(height),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn horizontal_alignment(self, alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
horizontal_alignment: Some(alignment),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vertical_alignment(self, alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
vertical_alignment: Some(alignment),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color(self, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
color: Some(color),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn font(self, font: Font) -> Self {
|
||||||
|
Self {
|
||||||
|
font: Some(font),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Text> for widget::v0alpha1::Text {
|
||||||
|
fn from(value: Text) -> Self {
|
||||||
|
let mut text = widget::v0alpha1::Text {
|
||||||
|
text: Some(value.text),
|
||||||
|
pixels: value.size,
|
||||||
|
width: value.width.map(From::from),
|
||||||
|
height: value.height.map(From::from),
|
||||||
|
horizontal_alignment: None,
|
||||||
|
vertical_alignment: None,
|
||||||
|
color: value.color.map(From::from),
|
||||||
|
font: value.font.map(From::from),
|
||||||
|
};
|
||||||
|
if let Some(horizontal_alignment) = value.horizontal_alignment {
|
||||||
|
text.set_horizontal_alignment(horizontal_alignment.into());
|
||||||
|
}
|
||||||
|
if let Some(vertical_alignment) = value.vertical_alignment {
|
||||||
|
text.set_vertical_alignment(vertical_alignment.into());
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Color {
|
||||||
|
pub red: f32,
|
||||||
|
pub green: f32,
|
||||||
|
pub blue: f32,
|
||||||
|
pub alpha: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 4]> for Color {
|
||||||
|
fn from(value: [f32; 4]) -> Self {
|
||||||
|
Self {
|
||||||
|
red: value[0],
|
||||||
|
blue: value[1],
|
||||||
|
green: value[2],
|
||||||
|
alpha: value[3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 3]> for Color {
|
||||||
|
fn from(value: [f32; 3]) -> Self {
|
||||||
|
Self {
|
||||||
|
red: value[0],
|
||||||
|
blue: value[1],
|
||||||
|
green: value[2],
|
||||||
|
alpha: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Color> for widget::v0alpha1::Color {
|
||||||
|
fn from(value: Color) -> Self {
|
||||||
|
widget::v0alpha1::Color {
|
||||||
|
red: Some(value.red),
|
||||||
|
green: Some(value.blue),
|
||||||
|
blue: Some(value.green),
|
||||||
|
alpha: Some(value.alpha),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Column {
|
||||||
|
pub spacing: Option<f32>,
|
||||||
|
pub padding: Option<Padding>,
|
||||||
|
pub item_alignment: Option<Alignment>,
|
||||||
|
pub width: Option<Length>,
|
||||||
|
pub height: Option<Length>,
|
||||||
|
pub max_width: Option<f32>,
|
||||||
|
pub clip: Option<bool>,
|
||||||
|
pub children: Vec<WidgetDef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_children(children: impl IntoIterator<Item = WidgetDef>) -> Self {
|
||||||
|
Self {
|
||||||
|
children: children.into_iter().collect(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spacing(self, spacing: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
spacing: Some(spacing),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item_alignment(self, item_alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
item_alignment: Some(item_alignment),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn padding(self, padding: Padding) -> Self {
|
||||||
|
Self {
|
||||||
|
padding: Some(padding),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(self, width: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
width: Some(width),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(self, height: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
height: Some(height),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_width(self, max_width: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
max_width: Some(max_width),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clip(self, clip: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
clip: Some(clip),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self, child: impl Into<WidgetDef>) -> Self {
|
||||||
|
let mut children = self.children;
|
||||||
|
children.push(child.into());
|
||||||
|
Self { children, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Column> for widget::v0alpha1::Column {
|
||||||
|
fn from(value: Column) -> Self {
|
||||||
|
widget::v0alpha1::Column {
|
||||||
|
spacing: value.spacing,
|
||||||
|
padding: value.padding.map(From::from),
|
||||||
|
item_alignment: value
|
||||||
|
.item_alignment
|
||||||
|
.map(|it| widget::v0alpha1::Alignment::from(it) as i32),
|
||||||
|
width: value.width.map(From::from),
|
||||||
|
height: value.height.map(From::from),
|
||||||
|
max_width: value.max_width,
|
||||||
|
clip: value.clip,
|
||||||
|
children: value.children.into_iter().map(From::from).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Row {
|
||||||
|
pub spacing: Option<f32>,
|
||||||
|
pub padding: Option<Padding>,
|
||||||
|
pub item_alignment: Option<Alignment>,
|
||||||
|
pub width: Option<Length>,
|
||||||
|
pub height: Option<Length>,
|
||||||
|
pub clip: Option<bool>,
|
||||||
|
pub children: Vec<WidgetDef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Row {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_children(children: impl IntoIterator<Item = WidgetDef>) -> Self {
|
||||||
|
Self {
|
||||||
|
children: children.into_iter().collect(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spacing(self, spacing: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
spacing: Some(spacing),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item_alignment(self, item_alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
item_alignment: Some(item_alignment),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn padding(self, padding: Padding) -> Self {
|
||||||
|
Self {
|
||||||
|
padding: Some(padding),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(self, width: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
width: Some(width),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(self, height: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
height: Some(height),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clip(self, clip: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
clip: Some(clip),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self, child: impl Into<WidgetDef>) -> Self {
|
||||||
|
let mut children = self.children;
|
||||||
|
children.push(child.into());
|
||||||
|
Self { children, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Row> for widget::v0alpha1::Row {
|
||||||
|
fn from(value: Row) -> Self {
|
||||||
|
widget::v0alpha1::Row {
|
||||||
|
spacing: value.spacing,
|
||||||
|
padding: value.padding.map(From::from),
|
||||||
|
item_alignment: value
|
||||||
|
.item_alignment
|
||||||
|
.map(|it| widget::v0alpha1::Alignment::from(it) as i32),
|
||||||
|
width: value.width.map(From::from),
|
||||||
|
height: value.height.map(From::from),
|
||||||
|
clip: value.clip,
|
||||||
|
children: value.children.into_iter().map(From::from).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Padding {
|
||||||
|
pub top: f32,
|
||||||
|
pub right: f32,
|
||||||
|
pub bottom: f32,
|
||||||
|
pub left: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Padding> for widget::v0alpha1::Padding {
|
||||||
|
fn from(value: Padding) -> Self {
|
||||||
|
widget::v0alpha1::Padding {
|
||||||
|
top: Some(value.top),
|
||||||
|
right: Some(value.right),
|
||||||
|
bottom: Some(value.bottom),
|
||||||
|
left: Some(value.left),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||||
|
pub enum Alignment {
|
||||||
|
#[default]
|
||||||
|
Start,
|
||||||
|
Center,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Alignment> for widget::v0alpha1::Alignment {
|
||||||
|
fn from(value: Alignment) -> Self {
|
||||||
|
match value {
|
||||||
|
Alignment::Start => widget::v0alpha1::Alignment::Start,
|
||||||
|
Alignment::Center => widget::v0alpha1::Alignment::Center,
|
||||||
|
Alignment::End => widget::v0alpha1::Alignment::End,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub enum Length {
|
||||||
|
#[default]
|
||||||
|
Fill,
|
||||||
|
FillPortion(u16),
|
||||||
|
Shrink,
|
||||||
|
Fixed(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Length> for widget::v0alpha1::Length {
|
||||||
|
fn from(value: Length) -> Self {
|
||||||
|
widget::v0alpha1::Length {
|
||||||
|
strategy: Some(match value {
|
||||||
|
Length::Fill => widget::v0alpha1::length::Strategy::Fill(()),
|
||||||
|
Length::FillPortion(portion) => {
|
||||||
|
widget::v0alpha1::length::Strategy::FillPortion(portion as u32)
|
||||||
|
}
|
||||||
|
Length::Shrink => widget::v0alpha1::length::Strategy::Shrink(()),
|
||||||
|
Length::Fixed(size) => widget::v0alpha1::length::Strategy::Fixed(size),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum ScrollableDirection {
|
||||||
|
Vertical(ScrollableProperties),
|
||||||
|
Horizontal(ScrollableProperties),
|
||||||
|
Both {
|
||||||
|
vertical: ScrollableProperties,
|
||||||
|
horizontal: ScrollableProperties,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ScrollableDirection> for widget::v0alpha1::ScrollableDirection {
|
||||||
|
fn from(value: ScrollableDirection) -> Self {
|
||||||
|
match value {
|
||||||
|
ScrollableDirection::Vertical(props) => widget::v0alpha1::ScrollableDirection {
|
||||||
|
vertical: Some(props.into()),
|
||||||
|
horizontal: None,
|
||||||
|
},
|
||||||
|
ScrollableDirection::Horizontal(props) => widget::v0alpha1::ScrollableDirection {
|
||||||
|
vertical: None,
|
||||||
|
horizontal: Some(props.into()),
|
||||||
|
},
|
||||||
|
ScrollableDirection::Both {
|
||||||
|
vertical,
|
||||||
|
horizontal,
|
||||||
|
} => widget::v0alpha1::ScrollableDirection {
|
||||||
|
vertical: Some(vertical.into()),
|
||||||
|
horizontal: Some(horizontal.into()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||||
|
pub enum ScrollableAlignment {
|
||||||
|
#[default]
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ScrollableAlignment> for widget::v0alpha1::ScrollableAlignment {
|
||||||
|
fn from(value: ScrollableAlignment) -> Self {
|
||||||
|
match value {
|
||||||
|
ScrollableAlignment::Start => widget::v0alpha1::ScrollableAlignment::Start,
|
||||||
|
ScrollableAlignment::End => widget::v0alpha1::ScrollableAlignment::End,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct ScrollableProperties {
|
||||||
|
pub width: Option<f32>,
|
||||||
|
pub margin: Option<f32>,
|
||||||
|
pub scroller_width: Option<f32>,
|
||||||
|
pub alignment: Option<ScrollableAlignment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ScrollableProperties> for widget::v0alpha1::ScrollableProperties {
|
||||||
|
fn from(value: ScrollableProperties) -> Self {
|
||||||
|
widget::v0alpha1::ScrollableProperties {
|
||||||
|
width: value.width,
|
||||||
|
margin: value.margin,
|
||||||
|
scroller_width: value.scroller_width,
|
||||||
|
alignment: value
|
||||||
|
.alignment
|
||||||
|
.map(|it| widget::v0alpha1::ScrollableAlignment::from(it) as i32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Scrollable {
|
||||||
|
pub width: Option<Length>,
|
||||||
|
pub height: Option<Length>,
|
||||||
|
pub direction: Option<ScrollableDirection>,
|
||||||
|
pub child: WidgetDef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Scrollable> for widget::v0alpha1::Scrollable {
|
||||||
|
fn from(value: Scrollable) -> Self {
|
||||||
|
widget::v0alpha1::Scrollable {
|
||||||
|
width: value.width.map(From::from),
|
||||||
|
height: value.height.map(From::from),
|
||||||
|
direction: value.direction.map(From::from),
|
||||||
|
child: Some(Box::new(value.child.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scrollable {
|
||||||
|
pub fn new(child: impl Into<WidgetDef>) -> Self {
|
||||||
|
Self {
|
||||||
|
child: child.into(),
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
direction: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(self, width: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
width: Some(width),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(self, height: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
height: Some(height),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn direction(self, direction: ScrollableDirection) -> Self {
|
||||||
|
Self {
|
||||||
|
direction: Some(direction),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Container {
|
||||||
|
pub padding: Option<Padding>,
|
||||||
|
pub width: Option<Length>,
|
||||||
|
pub height: Option<Length>,
|
||||||
|
pub max_width: Option<f32>,
|
||||||
|
pub max_height: Option<f32>,
|
||||||
|
pub horizontal_alignment: Option<Alignment>,
|
||||||
|
pub vertical_alignment: Option<Alignment>,
|
||||||
|
pub clip: Option<bool>,
|
||||||
|
pub child: WidgetDef,
|
||||||
|
|
||||||
|
pub text_color: Option<Color>,
|
||||||
|
pub background_color: Option<Color>,
|
||||||
|
pub border_radius: Option<f32>,
|
||||||
|
pub border_thickness: Option<f32>,
|
||||||
|
pub border_color: Option<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container {
|
||||||
|
pub fn new(child: impl Into<WidgetDef>) -> Self {
|
||||||
|
Self {
|
||||||
|
child: child.into(),
|
||||||
|
padding: None,
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
max_width: None,
|
||||||
|
max_height: None,
|
||||||
|
horizontal_alignment: None,
|
||||||
|
vertical_alignment: None,
|
||||||
|
clip: None,
|
||||||
|
text_color: None,
|
||||||
|
background_color: None,
|
||||||
|
border_radius: None,
|
||||||
|
border_thickness: None,
|
||||||
|
border_color: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn padding(self, padding: Padding) -> Self {
|
||||||
|
Self {
|
||||||
|
padding: Some(padding),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(self, width: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
width: Some(width),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(self, height: Length) -> Self {
|
||||||
|
Self {
|
||||||
|
height: Some(height),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_width(self, max_width: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
max_width: Some(max_width),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_height(self, max_height: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
max_height: Some(max_height),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn horizontal_alignment(self, horizontal_alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
horizontal_alignment: Some(horizontal_alignment),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vertical_alignment(self, vertical_alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
vertical_alignment: Some(vertical_alignment),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clip(self, clip: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
clip: Some(clip),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_color(self, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
text_color: Some(color),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn background_color(self, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
background_color: Some(color),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_radius(self, radius: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
border_radius: Some(radius),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_thickness(self, thickness: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
border_thickness: Some(thickness),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_color(self, color: Color) -> Self {
|
||||||
|
Self {
|
||||||
|
border_color: Some(color),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Container> for widget::v0alpha1::Container {
|
||||||
|
fn from(value: Container) -> Self {
|
||||||
|
widget::v0alpha1::Container {
|
||||||
|
padding: value.padding.map(From::from),
|
||||||
|
width: value.width.map(From::from),
|
||||||
|
height: value.height.map(From::from),
|
||||||
|
max_width: value.max_width,
|
||||||
|
max_height: value.max_height,
|
||||||
|
horizontal_alignment: value
|
||||||
|
.horizontal_alignment
|
||||||
|
.map(|it| widget::v0alpha1::Alignment::from(it) as i32),
|
||||||
|
vertical_alignment: value
|
||||||
|
.vertical_alignment
|
||||||
|
.map(|it| widget::v0alpha1::Alignment::from(it) as i32),
|
||||||
|
clip: value.clip,
|
||||||
|
child: Some(Box::new(value.child.into())),
|
||||||
|
text_color: value.text_color.map(From::from),
|
||||||
|
background_color: value.background_color.map(From::from),
|
||||||
|
border_radius: value.border_radius,
|
||||||
|
border_thickness: value.border_thickness,
|
||||||
|
border_color: value.border_color.map(From::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
175
snowcap/api/rust/src/widget/font.rs
Normal file
175
snowcap/api/rust/src/widget/font.rs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
//! Font utilities and types.
|
||||||
|
|
||||||
|
use snowcap_api_defs::snowcap::widget;
|
||||||
|
|
||||||
|
/// A font specification.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
|
||||||
|
pub struct Font {
|
||||||
|
/// The font family.
|
||||||
|
pub family: Family,
|
||||||
|
/// The font weight.
|
||||||
|
pub weight: Weight,
|
||||||
|
/// The font stretch.
|
||||||
|
pub stretch: Stretch,
|
||||||
|
/// The font style.
|
||||||
|
pub style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Font {
|
||||||
|
/// Create a new, empty font specification.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new font specification with the given family.
|
||||||
|
pub fn new_with_family(family: Family) -> Self {
|
||||||
|
Self {
|
||||||
|
family,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this font's family.
|
||||||
|
pub fn family(self, family: Family) -> Self {
|
||||||
|
Self { family, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this font's weight.
|
||||||
|
pub fn weight(self, weight: Weight) -> Self {
|
||||||
|
Self { weight, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this font's stretch.
|
||||||
|
pub fn stretch(self, stretch: Stretch) -> Self {
|
||||||
|
Self { stretch, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this font's style.
|
||||||
|
pub fn style(self, style: Style) -> Self {
|
||||||
|
Self { style, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Font> for widget::v0alpha1::Font {
|
||||||
|
fn from(value: Font) -> Self {
|
||||||
|
Self {
|
||||||
|
family: Some(value.family.into()),
|
||||||
|
weight: Some(widget::v0alpha1::font::Weight::from(value.weight) as i32),
|
||||||
|
stretch: Some(widget::v0alpha1::font::Stretch::from(value.stretch) as i32),
|
||||||
|
style: Some(widget::v0alpha1::font::Style::from(value.style) as i32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A font family.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
|
||||||
|
pub enum Family {
|
||||||
|
/// A named font, like JetBrainsMono or FreeSerif.
|
||||||
|
Name(String),
|
||||||
|
Serif,
|
||||||
|
#[default]
|
||||||
|
SansSerif,
|
||||||
|
Cursive,
|
||||||
|
Fantasy,
|
||||||
|
Monospace,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Family> for widget::v0alpha1::font::Family {
|
||||||
|
fn from(value: Family) -> Self {
|
||||||
|
Self {
|
||||||
|
family: Some(match value {
|
||||||
|
Family::Name(name) => widget::v0alpha1::font::family::Family::Name(name),
|
||||||
|
Family::Serif => widget::v0alpha1::font::family::Family::Serif(()),
|
||||||
|
Family::SansSerif => widget::v0alpha1::font::family::Family::SansSerif(()),
|
||||||
|
Family::Cursive => widget::v0alpha1::font::family::Family::Cursive(()),
|
||||||
|
Family::Fantasy => widget::v0alpha1::font::family::Family::Fantasy(()),
|
||||||
|
Family::Monospace => widget::v0alpha1::font::family::Family::Monospace(()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A font weight.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||||
|
pub enum Weight {
|
||||||
|
Thin,
|
||||||
|
ExtraLight,
|
||||||
|
Light,
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Medium,
|
||||||
|
Semibold,
|
||||||
|
Bold,
|
||||||
|
ExtraBold,
|
||||||
|
Black,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Weight> for widget::v0alpha1::font::Weight {
|
||||||
|
fn from(value: Weight) -> Self {
|
||||||
|
match value {
|
||||||
|
Weight::Thin => widget::v0alpha1::font::Weight::Thin,
|
||||||
|
Weight::ExtraLight => widget::v0alpha1::font::Weight::ExtraLight,
|
||||||
|
Weight::Light => widget::v0alpha1::font::Weight::Light,
|
||||||
|
Weight::Normal => widget::v0alpha1::font::Weight::Normal,
|
||||||
|
Weight::Medium => widget::v0alpha1::font::Weight::Medium,
|
||||||
|
Weight::Semibold => widget::v0alpha1::font::Weight::Semibold,
|
||||||
|
Weight::Bold => widget::v0alpha1::font::Weight::Bold,
|
||||||
|
Weight::ExtraBold => widget::v0alpha1::font::Weight::ExtraBold,
|
||||||
|
Weight::Black => widget::v0alpha1::font::Weight::Black,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A font stretch.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||||
|
pub enum Stretch {
|
||||||
|
UltraCondensed,
|
||||||
|
ExtraCondensed,
|
||||||
|
Condensed,
|
||||||
|
SemiCondensed,
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
SemiExpanded,
|
||||||
|
Expanded,
|
||||||
|
ExtraExpanded,
|
||||||
|
UltraExpanded,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Stretch> for widget::v0alpha1::font::Stretch {
|
||||||
|
fn from(value: Stretch) -> Self {
|
||||||
|
match value {
|
||||||
|
Stretch::UltraCondensed => widget::v0alpha1::font::Stretch::UltraCondensed,
|
||||||
|
Stretch::ExtraCondensed => widget::v0alpha1::font::Stretch::ExtraCondensed,
|
||||||
|
Stretch::Condensed => widget::v0alpha1::font::Stretch::Condensed,
|
||||||
|
Stretch::SemiCondensed => widget::v0alpha1::font::Stretch::SemiCondensed,
|
||||||
|
Stretch::Normal => widget::v0alpha1::font::Stretch::Normal,
|
||||||
|
Stretch::SemiExpanded => widget::v0alpha1::font::Stretch::SemiExpanded,
|
||||||
|
Stretch::Expanded => widget::v0alpha1::font::Stretch::Expanded,
|
||||||
|
Stretch::ExtraExpanded => widget::v0alpha1::font::Stretch::ExtraExpanded,
|
||||||
|
Stretch::UltraExpanded => widget::v0alpha1::font::Stretch::UltraExpanded,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A font style.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
|
||||||
|
pub enum Style {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Italic,
|
||||||
|
Oblique,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Style> for widget::v0alpha1::font::Style {
|
||||||
|
fn from(value: Style) -> Self {
|
||||||
|
match value {
|
||||||
|
Style::Normal => widget::v0alpha1::font::Style::Normal,
|
||||||
|
Style::Italic => widget::v0alpha1::font::Style::Italic,
|
||||||
|
Style::Oblique => widget::v0alpha1::font::Style::Oblique,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
snowcap/justfile
Normal file
39
snowcap/justfile
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
set shell := ["bash", "-c"]
|
||||||
|
|
||||||
|
rootdir := justfile_directory()
|
||||||
|
xdg_data_dir := `echo "${XDG_DATA_HOME:-$HOME/.local/share}/snowcap"`
|
||||||
|
root_xdg_data_dir := "/usr/share/snowcap"
|
||||||
|
|
||||||
|
lua_version := "5.4"
|
||||||
|
|
||||||
|
list:
|
||||||
|
@just --list --unsorted
|
||||||
|
|
||||||
|
install: install-protos install-lua-lib
|
||||||
|
|
||||||
|
install-protos:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
proto_dir="{{xdg_data_dir}}/protobuf"
|
||||||
|
rm -rf "${proto_dir}"
|
||||||
|
mkdir -p "{{xdg_data_dir}}"
|
||||||
|
cp -r "{{rootdir}}/api/protobuf" "${proto_dir}"
|
||||||
|
|
||||||
|
install-lua-lib: gen-lua-pb-defs
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
cd "{{rootdir}}/api/lua"
|
||||||
|
luarocks build --local https://raw.githubusercontent.com/pinnacle-comp/lua-grpc-client/main/lua-grpc-client-dev-1.rockspec
|
||||||
|
luarocks build --local --lua-version "{{lua_version}}"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf "{{xdg_data_dir}}"
|
||||||
|
-luarocks remove --local snowcap-api
|
||||||
|
-luarocks remove --local lua-grpc-client
|
||||||
|
|
||||||
|
# Generate the protobuf definitions Lua file
|
||||||
|
gen-lua-pb-defs:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
cargo build --package lua-build
|
||||||
|
./target/debug/lua-build > "./api/lua/snowcap/grpc/defs.lua"
|
96
snowcap/resources/fonts/UFL.txt
Normal file
96
snowcap/resources/fonts/UFL.txt
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
-------------------------------
|
||||||
|
UBUNTU FONT LICENCE Version 1.0
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
This licence allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely. The fonts, including any derivative works, can be
|
||||||
|
bundled, embedded, and redistributed provided the terms of this licence
|
||||||
|
are met. The fonts and derivatives, however, cannot be released under
|
||||||
|
any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not require any document created using the fonts or their
|
||||||
|
derivatives to be published under this licence, as long as the primary
|
||||||
|
purpose of the document is not to be a vehicle for the distribution of
|
||||||
|
the fonts.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this licence and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components
|
||||||
|
as received under this licence.
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to
|
||||||
|
a new environment.
|
||||||
|
|
||||||
|
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||||
|
copyright ownership of the Font Software.
|
||||||
|
|
||||||
|
"Substantially Changed" refers to Modified Versions which can be easily
|
||||||
|
identified as dissimilar to the Font Software by users of the Font
|
||||||
|
Software comparing the Original Version with the Modified Version.
|
||||||
|
|
||||||
|
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 and with or without charging
|
||||||
|
a redistribution fee), making available to the public, and in some
|
||||||
|
countries other activities as well.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
This licence does not grant any rights under trademark law and all such
|
||||||
|
rights are reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of the Font Software, to propagate the Font Software, subject to
|
||||||
|
the below conditions:
|
||||||
|
|
||||||
|
1) Each copy of the Font Software must contain the above copyright
|
||||||
|
notice and this licence. These can be included either as stand-alone
|
||||||
|
text files, human-readable headers or in the appropriate machine-
|
||||||
|
readable metadata fields within text or binary files as long as those
|
||||||
|
fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
2) The font name complies with the following:
|
||||||
|
(a) The Original Version must retain its name, unmodified.
|
||||||
|
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||||
|
avoid use of the name of the Original Version or similar names entirely.
|
||||||
|
(c) Modified Versions which are not Substantially Changed must be
|
||||||
|
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||||
|
additional naming elements to distinguish the Modified Version from the
|
||||||
|
Original Version. The name of such Modified Versions must be the name of
|
||||||
|
the Original Version, with "derivative X" where X represents the name of
|
||||||
|
the new work, appended to that name.
|
||||||
|
|
||||||
|
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||||
|
Font Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except (i) as required by this licence, (ii) to
|
||||||
|
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||||
|
their explicit written permission.
|
||||||
|
|
||||||
|
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||||
|
be distributed entirely under this licence, and must not be distributed
|
||||||
|
under any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not affect any document created using the Font Software,
|
||||||
|
except any version of the Font Software extracted from a document
|
||||||
|
created using the Font Software may only be distributed under this
|
||||||
|
licence.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This licence becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||||
|
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||||
|
DEALINGS IN THE FONT SOFTWARE.
|
BIN
snowcap/resources/fonts/Ubuntu-Bold.ttf
Normal file
BIN
snowcap/resources/fonts/Ubuntu-Bold.ttf
Normal file
Binary file not shown.
BIN
snowcap/resources/fonts/Ubuntu-BoldItalic.ttf
Normal file
BIN
snowcap/resources/fonts/Ubuntu-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
snowcap/resources/fonts/Ubuntu-Italic.ttf
Normal file
BIN
snowcap/resources/fonts/Ubuntu-Italic.ttf
Normal file
Binary file not shown.
BIN
snowcap/resources/fonts/Ubuntu-Regular.ttf
Normal file
BIN
snowcap/resources/fonts/Ubuntu-Regular.ttf
Normal file
Binary file not shown.
12
snowcap/snowcap-api-defs/Cargo.toml
Normal file
12
snowcap/snowcap-api-defs/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "snowcap-api-defs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
prost = { workspace = true }
|
||||||
|
tonic = { workspace = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
tonic-build = { workspace = true }
|
23
snowcap/snowcap-api-defs/build.rs
Normal file
23
snowcap/snowcap-api-defs/build.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=../api/protobuf");
|
||||||
|
|
||||||
|
let mut proto_paths = Vec::new();
|
||||||
|
|
||||||
|
for entry in walkdir::WalkDir::new("../api/protobuf") {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
|
||||||
|
if entry.file_type().is_file() && entry.path().extension().is_some_and(|ext| ext == "proto")
|
||||||
|
{
|
||||||
|
proto_paths.push(entry.into_path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let descriptor_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("snowcap.bin");
|
||||||
|
|
||||||
|
tonic_build::configure()
|
||||||
|
.file_descriptor_set_path(descriptor_path)
|
||||||
|
.compile(&proto_paths, &["../api/protobuf"])
|
||||||
|
.unwrap();
|
||||||
|
}
|
25
snowcap/snowcap-api-defs/src/lib.rs
Normal file
25
snowcap/snowcap-api-defs/src/lib.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("snowcap");
|
||||||
|
|
||||||
|
pub mod snowcap {
|
||||||
|
pub mod v0alpha1 {
|
||||||
|
tonic::include_proto!("snowcap.v0alpha1");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod widget {
|
||||||
|
pub mod v0alpha1 {
|
||||||
|
tonic::include_proto!("snowcap.widget.v0alpha1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod layer {
|
||||||
|
pub mod v0alpha1 {
|
||||||
|
tonic::include_proto!("snowcap.layer.v0alpha1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod input {
|
||||||
|
pub mod v0alpha1 {
|
||||||
|
tonic::include_proto!("snowcap.input.v0alpha1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
215
snowcap/src/api.rs
Normal file
215
snowcap/src/api.rs
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
pub mod input;
|
||||||
|
|
||||||
|
use std::{num::NonZeroU32, pin::Pin};
|
||||||
|
|
||||||
|
use futures::Stream;
|
||||||
|
use smithay_client_toolkit::{reexports::calloop, shell::wlr_layer};
|
||||||
|
use snowcap_api_defs::snowcap::layer::{
|
||||||
|
self,
|
||||||
|
v0alpha1::{layer_service_server, CloseRequest, NewLayerRequest, NewLayerResponse},
|
||||||
|
};
|
||||||
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
layer::{ExclusiveZone, SnowcapLayer},
|
||||||
|
state::State,
|
||||||
|
widget::widget_def_to_fn,
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn run_unary_no_response<F>(
|
||||||
|
fn_sender: &StateFnSender,
|
||||||
|
with_state: F,
|
||||||
|
) -> Result<Response<()>, Status>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut State) + Send + 'static,
|
||||||
|
{
|
||||||
|
fn_sender
|
||||||
|
.send(Box::new(with_state))
|
||||||
|
.map_err(|_| Status::internal("failed to execute request"))?;
|
||||||
|
|
||||||
|
Ok(Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_unary<F, T>(fn_sender: &StateFnSender, with_state: F) -> Result<Response<T>, Status>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut State) -> Result<T, Status> + Send + 'static,
|
||||||
|
T: Send + 'static,
|
||||||
|
{
|
||||||
|
let (sender, receiver) = tokio::sync::oneshot::channel::<Result<T, Status>>();
|
||||||
|
|
||||||
|
let f = Box::new(|state: &mut State| {
|
||||||
|
// TODO: find a way to handle this error
|
||||||
|
if sender.send(with_state(state)).is_err() {
|
||||||
|
warn!("failed to send result of API call to config; receiver already dropped");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fn_sender
|
||||||
|
.send(f)
|
||||||
|
.map_err(|_| Status::internal("failed to execute request"))?;
|
||||||
|
|
||||||
|
let ret = receiver.await;
|
||||||
|
|
||||||
|
match ret {
|
||||||
|
Ok(it) => Ok(Response::new(it?)),
|
||||||
|
Err(err) => Err(Status::internal(format!(
|
||||||
|
"failed to transfer response for transport to client: {err}"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_server_streaming<F, T>(
|
||||||
|
fn_sender: &StateFnSender,
|
||||||
|
with_state: F,
|
||||||
|
) -> Result<Response<ResponseStream<T>>, Status>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut State, UnboundedSender<Result<T, Status>>) + Send + 'static,
|
||||||
|
T: Send + 'static,
|
||||||
|
{
|
||||||
|
let (sender, receiver) = unbounded_channel::<Result<T, Status>>();
|
||||||
|
|
||||||
|
let f = Box::new(|state: &mut State| {
|
||||||
|
with_state(state, sender);
|
||||||
|
});
|
||||||
|
|
||||||
|
fn_sender
|
||||||
|
.send(f)
|
||||||
|
.map_err(|_| Status::internal("failed to execute request"))?;
|
||||||
|
|
||||||
|
let receiver_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receiver);
|
||||||
|
Ok(Response::new(Box::pin(receiver_stream)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateFnSender = calloop::channel::Sender<Box<dyn FnOnce(&mut State) + Send>>;
|
||||||
|
|
||||||
|
type ResponseStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send>>;
|
||||||
|
|
||||||
|
pub struct SnowcapService {
|
||||||
|
_sender: StateFnSender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnowcapService {
|
||||||
|
pub fn new(sender: StateFnSender) -> Self {
|
||||||
|
Self { _sender: sender }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LayerService {
|
||||||
|
sender: StateFnSender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerService {
|
||||||
|
pub fn new(sender: StateFnSender) -> Self {
|
||||||
|
Self { sender }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl layer_service_server::LayerService for LayerService {
|
||||||
|
async fn new_layer(
|
||||||
|
&self,
|
||||||
|
request: Request<NewLayerRequest>,
|
||||||
|
) -> Result<Response<NewLayerResponse>, Status> {
|
||||||
|
let request = request.into_inner();
|
||||||
|
|
||||||
|
let anchor = request.anchor();
|
||||||
|
let exclusive_zone = request.exclusive_zone();
|
||||||
|
let keyboard_interactivity = request.keyboard_interactivity();
|
||||||
|
let layer = request.layer();
|
||||||
|
|
||||||
|
let Some(widget_def) = request.widget_def else {
|
||||||
|
return Err(Status::invalid_argument("no widget def"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let width = request.width.unwrap_or(600);
|
||||||
|
let height = request.height.unwrap_or(480);
|
||||||
|
|
||||||
|
let anchor = match anchor {
|
||||||
|
layer::v0alpha1::Anchor::Unspecified => wlr_layer::Anchor::empty(),
|
||||||
|
layer::v0alpha1::Anchor::Top => wlr_layer::Anchor::TOP,
|
||||||
|
layer::v0alpha1::Anchor::Bottom => wlr_layer::Anchor::BOTTOM,
|
||||||
|
layer::v0alpha1::Anchor::Left => wlr_layer::Anchor::LEFT,
|
||||||
|
layer::v0alpha1::Anchor::Right => wlr_layer::Anchor::RIGHT,
|
||||||
|
layer::v0alpha1::Anchor::TopLeft => wlr_layer::Anchor::TOP | wlr_layer::Anchor::LEFT,
|
||||||
|
layer::v0alpha1::Anchor::TopRight => wlr_layer::Anchor::TOP | wlr_layer::Anchor::RIGHT,
|
||||||
|
layer::v0alpha1::Anchor::BottomLeft => {
|
||||||
|
wlr_layer::Anchor::BOTTOM | wlr_layer::Anchor::LEFT
|
||||||
|
}
|
||||||
|
layer::v0alpha1::Anchor::BottomRight => {
|
||||||
|
wlr_layer::Anchor::BOTTOM | wlr_layer::Anchor::RIGHT
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let exclusive_zone = match exclusive_zone {
|
||||||
|
0 => ExclusiveZone::Respect,
|
||||||
|
x if x.is_positive() => ExclusiveZone::Exclusive(NonZeroU32::new(x as u32).unwrap()),
|
||||||
|
_ => ExclusiveZone::Ignore,
|
||||||
|
};
|
||||||
|
|
||||||
|
let keyboard_interactivity = match keyboard_interactivity {
|
||||||
|
layer::v0alpha1::KeyboardInteractivity::Unspecified
|
||||||
|
| layer::v0alpha1::KeyboardInteractivity::None => {
|
||||||
|
wlr_layer::KeyboardInteractivity::None
|
||||||
|
}
|
||||||
|
layer::v0alpha1::KeyboardInteractivity::OnDemand => {
|
||||||
|
wlr_layer::KeyboardInteractivity::OnDemand
|
||||||
|
}
|
||||||
|
layer::v0alpha1::KeyboardInteractivity::Exclusive => {
|
||||||
|
wlr_layer::KeyboardInteractivity::Exclusive
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let layer = match layer {
|
||||||
|
layer::v0alpha1::Layer::Unspecified => wlr_layer::Layer::Top,
|
||||||
|
layer::v0alpha1::Layer::Background => wlr_layer::Layer::Background,
|
||||||
|
layer::v0alpha1::Layer::Bottom => wlr_layer::Layer::Bottom,
|
||||||
|
layer::v0alpha1::Layer::Top => wlr_layer::Layer::Top,
|
||||||
|
layer::v0alpha1::Layer::Overlay => wlr_layer::Layer::Overlay,
|
||||||
|
};
|
||||||
|
|
||||||
|
run_unary(&self.sender, move |state| {
|
||||||
|
let Some((f, states)) = widget_def_to_fn(widget_def) else {
|
||||||
|
return Err(Status::invalid_argument("widget def was null"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let layer = SnowcapLayer::new(
|
||||||
|
state,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
layer,
|
||||||
|
anchor,
|
||||||
|
exclusive_zone,
|
||||||
|
keyboard_interactivity,
|
||||||
|
crate::widget::SnowcapWidgetProgram {
|
||||||
|
widgets: f,
|
||||||
|
widget_state: states,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let ret = Ok(NewLayerResponse {
|
||||||
|
layer_id: Some(layer.widget_id.into_inner()),
|
||||||
|
});
|
||||||
|
|
||||||
|
state.layers.push(layer);
|
||||||
|
|
||||||
|
ret
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self, request: Request<CloseRequest>) -> Result<Response<()>, Status> {
|
||||||
|
let request = request.into_inner();
|
||||||
|
|
||||||
|
let Some(id) = request.layer_id else {
|
||||||
|
return Err(Status::invalid_argument("layer id was null"));
|
||||||
|
};
|
||||||
|
|
||||||
|
run_unary_no_response(&self.sender, move |state| {
|
||||||
|
state
|
||||||
|
.layers
|
||||||
|
.retain(|sn_layer| sn_layer.widget_id.into_inner() != id);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
59
snowcap/src/api/input.rs
Normal file
59
snowcap/src/api/input.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use snowcap_api_defs::snowcap::input::v0alpha1::{
|
||||||
|
input_service_server, KeyboardKeyRequest, KeyboardKeyResponse, PointerButtonRequest,
|
||||||
|
PointerButtonResponse,
|
||||||
|
};
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
|
||||||
|
use crate::widget::WidgetId;
|
||||||
|
|
||||||
|
use super::{run_server_streaming, ResponseStream, StateFnSender};
|
||||||
|
|
||||||
|
pub struct InputService {
|
||||||
|
sender: StateFnSender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputService {
|
||||||
|
pub fn new(sender: StateFnSender) -> Self {
|
||||||
|
Self { sender }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl input_service_server::InputService for InputService {
|
||||||
|
type KeyboardKeyStream = ResponseStream<KeyboardKeyResponse>;
|
||||||
|
type PointerButtonStream = ResponseStream<PointerButtonResponse>;
|
||||||
|
|
||||||
|
async fn keyboard_key(
|
||||||
|
&self,
|
||||||
|
request: Request<KeyboardKeyRequest>,
|
||||||
|
) -> Result<Response<Self::KeyboardKeyStream>, Status> {
|
||||||
|
let request = request.into_inner();
|
||||||
|
|
||||||
|
let Some(id) = request.id else {
|
||||||
|
return Err(Status::invalid_argument("id was null"));
|
||||||
|
};
|
||||||
|
|
||||||
|
run_server_streaming(&self.sender, move |state, sender| {
|
||||||
|
if let Some(layer) = WidgetId::from(id).layer_for_mut(state) {
|
||||||
|
layer.keyboard_key_sender = Some(sender);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pointer_button(
|
||||||
|
&self,
|
||||||
|
request: Request<PointerButtonRequest>,
|
||||||
|
) -> Result<Response<Self::PointerButtonStream>, Status> {
|
||||||
|
let request = request.into_inner();
|
||||||
|
|
||||||
|
let Some(id) = request.id else {
|
||||||
|
return Err(Status::invalid_argument("id was null"));
|
||||||
|
};
|
||||||
|
|
||||||
|
run_server_streaming(&self.sender, move |state, sender| {
|
||||||
|
if let Some(layer) = WidgetId::from(id).layer_for_mut(state) {
|
||||||
|
layer.pointer_button_sender = Some(sender);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
24
snowcap/src/clipboard.rs
Normal file
24
snowcap/src/clipboard.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
/// A newtype wrapper over [`smithay_clipboard::Clipboard`].
|
||||||
|
pub struct WaylandClipboard(smithay_clipboard::Clipboard);
|
||||||
|
|
||||||
|
impl WaylandClipboard {
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// `display` must be a valid `*mut wl_display` pointer, and it must remain
|
||||||
|
/// valid for as long as `Clipboard` object is alive.
|
||||||
|
pub unsafe fn new(display: *mut c_void) -> Self {
|
||||||
|
Self(smithay_clipboard::Clipboard::new(display))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl iced_wgpu::core::Clipboard for WaylandClipboard {
|
||||||
|
fn read(&self, _kind: iced_wgpu::core::clipboard::Kind) -> Option<String> {
|
||||||
|
self.0.load().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, _kind: iced_wgpu::core::clipboard::Kind, contents: String) {
|
||||||
|
self.0.store(contents)
|
||||||
|
}
|
||||||
|
}
|
218
snowcap/src/handlers.rs
Normal file
218
snowcap/src/handlers.rs
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
pub mod keyboard;
|
||||||
|
pub mod pointer;
|
||||||
|
|
||||||
|
use iced_wgpu::graphics::Viewport;
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
compositor::CompositorHandler,
|
||||||
|
delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_seat,
|
||||||
|
output::{OutputHandler, OutputState},
|
||||||
|
reexports::client::{
|
||||||
|
protocol::{
|
||||||
|
wl_output::{self, WlOutput},
|
||||||
|
wl_seat::WlSeat,
|
||||||
|
wl_surface::WlSurface,
|
||||||
|
},
|
||||||
|
Connection, QueueHandle,
|
||||||
|
},
|
||||||
|
registry::{ProvidesRegistryState, RegistryState},
|
||||||
|
registry_handlers,
|
||||||
|
seat::{Capability, SeatHandler, SeatState},
|
||||||
|
shell::{
|
||||||
|
wlr_layer::{LayerShellHandler, LayerSurface, LayerSurfaceConfigure},
|
||||||
|
WaylandSurface,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
impl ProvidesRegistryState for State {
|
||||||
|
fn registry(&mut self) -> &mut RegistryState {
|
||||||
|
&mut self.registry_state
|
||||||
|
}
|
||||||
|
|
||||||
|
registry_handlers!(OutputState, SeatState);
|
||||||
|
}
|
||||||
|
delegate_registry!(State);
|
||||||
|
|
||||||
|
impl SeatHandler for State {
|
||||||
|
fn seat_state(&mut self) -> &mut SeatState {
|
||||||
|
&mut self.seat_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: WlSeat) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_capability(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
seat: WlSeat,
|
||||||
|
capability: Capability,
|
||||||
|
) {
|
||||||
|
if capability == Capability::Keyboard && self.keyboard.is_none() {
|
||||||
|
let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
|
||||||
|
self.keyboard = Some(keyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if capability == Capability::Pointer && self.pointer.is_none() {
|
||||||
|
let pointer = self.seat_state.get_pointer(qh, &seat).unwrap();
|
||||||
|
self.pointer = Some(pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_capability(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_seat: WlSeat,
|
||||||
|
capability: Capability,
|
||||||
|
) {
|
||||||
|
if capability == Capability::Keyboard {
|
||||||
|
if let Some(keyboard) = self.keyboard.take() {
|
||||||
|
keyboard.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if capability == Capability::Pointer {
|
||||||
|
if let Some(pointer) = self.pointer.take() {
|
||||||
|
pointer.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: WlSeat) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_seat!(State);
|
||||||
|
|
||||||
|
impl OutputHandler for State {
|
||||||
|
fn output_state(&mut self) -> &mut OutputState {
|
||||||
|
&mut self.output_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: WlOutput) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: WlOutput) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: WlOutput) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_output!(State);
|
||||||
|
|
||||||
|
impl LayerShellHandler for State {
|
||||||
|
fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, layer: &LayerSurface) {
|
||||||
|
self.layers.retain(|sn_layer| &sn_layer.layer != layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
layer: &LayerSurface,
|
||||||
|
_configure: LayerSurfaceConfigure,
|
||||||
|
_serial: u32,
|
||||||
|
) {
|
||||||
|
let layer = self.layers.iter_mut().find(|l| &l.layer == layer);
|
||||||
|
|
||||||
|
if let Some(layer) = layer {
|
||||||
|
layer.update_and_draw(
|
||||||
|
&self.wgpu.device,
|
||||||
|
&self.wgpu.queue,
|
||||||
|
&mut self.wgpu.renderer,
|
||||||
|
qh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_layer!(State);
|
||||||
|
|
||||||
|
impl CompositorHandler for State {
|
||||||
|
fn scale_factor_changed(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
surface: &WlSurface,
|
||||||
|
new_factor: i32,
|
||||||
|
) {
|
||||||
|
if let Some(layer) = self
|
||||||
|
.layers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|sn_layer| sn_layer.layer.wl_surface() == surface)
|
||||||
|
{
|
||||||
|
layer.viewport = Viewport::with_physical_size(
|
||||||
|
iced::Size::new(
|
||||||
|
layer.width * new_factor as u32,
|
||||||
|
layer.height * new_factor as u32,
|
||||||
|
),
|
||||||
|
new_factor as f64,
|
||||||
|
);
|
||||||
|
layer.set_scale(new_factor, &self.wgpu.device);
|
||||||
|
layer.update_and_draw(
|
||||||
|
&self.wgpu.device,
|
||||||
|
&self.wgpu.queue,
|
||||||
|
&mut self.wgpu.renderer,
|
||||||
|
qh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_changed(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_surface: &WlSurface,
|
||||||
|
_new_transform: wl_output::Transform,
|
||||||
|
) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
surface: &WlSurface,
|
||||||
|
_time: u32,
|
||||||
|
) {
|
||||||
|
let layer = self
|
||||||
|
.layers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|layer| layer.layer.wl_surface() == surface);
|
||||||
|
|
||||||
|
if let Some(layer) = layer {
|
||||||
|
layer.update_and_draw(
|
||||||
|
&self.wgpu.device,
|
||||||
|
&self.wgpu.queue,
|
||||||
|
&mut self.wgpu.renderer,
|
||||||
|
qh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_enter(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_surface: &WlSurface,
|
||||||
|
_output: &wl_output::WlOutput,
|
||||||
|
) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_leave(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_surface: &WlSurface,
|
||||||
|
_output: &wl_output::WlOutput,
|
||||||
|
) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_compositor!(State);
|
163
snowcap/src/handlers/keyboard.rs
Normal file
163
snowcap/src/handlers/keyboard.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
delegate_keyboard,
|
||||||
|
reexports::client::{
|
||||||
|
protocol::{wl_keyboard::WlKeyboard, wl_surface::WlSurface},
|
||||||
|
Connection, QueueHandle,
|
||||||
|
},
|
||||||
|
seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers},
|
||||||
|
shell::{wlr_layer::LayerSurface, WaylandSurface},
|
||||||
|
};
|
||||||
|
use snowcap_api_defs::snowcap::input::{self, v0alpha1::KeyboardKeyResponse};
|
||||||
|
|
||||||
|
use crate::{input::keyboard::keysym_to_iced_key_and_loc, state::State};
|
||||||
|
|
||||||
|
impl KeyboardHandler for State {
|
||||||
|
fn enter(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &WlKeyboard,
|
||||||
|
surface: &WlSurface,
|
||||||
|
_serial: u32,
|
||||||
|
_raw: &[u32],
|
||||||
|
_keysyms: &[Keysym],
|
||||||
|
) {
|
||||||
|
if let Some(layer) = self
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.find(|sn_layer| sn_layer.layer.wl_surface() == surface)
|
||||||
|
{
|
||||||
|
self.keyboard_focus = Some(KeyboardFocus::Layer(layer.layer.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leave(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &WlKeyboard,
|
||||||
|
surface: &WlSurface,
|
||||||
|
_serial: u32,
|
||||||
|
) {
|
||||||
|
if let Some(KeyboardFocus::Layer(layer)) = self.keyboard_focus.as_ref() {
|
||||||
|
if layer.wl_surface() == surface {
|
||||||
|
self.keyboard_focus = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press_key(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &WlKeyboard,
|
||||||
|
_serial: u32,
|
||||||
|
event: KeyEvent,
|
||||||
|
) {
|
||||||
|
let Some(KeyboardFocus::Layer(layer)) = self.keyboard_focus.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(snowcap_layer) = self.layers.iter_mut().find(|sn_l| &sn_l.layer == layer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (key, location) = keysym_to_iced_key_and_loc(event.keysym);
|
||||||
|
|
||||||
|
let mut modifiers = iced::keyboard::Modifiers::empty();
|
||||||
|
if self.keyboard_modifiers.ctrl {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::CTRL;
|
||||||
|
}
|
||||||
|
if self.keyboard_modifiers.alt {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::ALT;
|
||||||
|
}
|
||||||
|
if self.keyboard_modifiers.shift {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::SHIFT;
|
||||||
|
}
|
||||||
|
if self.keyboard_modifiers.logo {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::LOGO;
|
||||||
|
}
|
||||||
|
|
||||||
|
snowcap_layer.widgets.queue_event(iced::Event::Keyboard(
|
||||||
|
iced::keyboard::Event::KeyPressed {
|
||||||
|
key,
|
||||||
|
location,
|
||||||
|
modifiers,
|
||||||
|
text: None,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(sender) = snowcap_layer.keyboard_key_sender.as_ref() {
|
||||||
|
let api_modifiers = input::v0alpha1::Modifiers {
|
||||||
|
shift: Some(self.keyboard_modifiers.shift),
|
||||||
|
ctrl: Some(self.keyboard_modifiers.ctrl),
|
||||||
|
alt: Some(self.keyboard_modifiers.alt),
|
||||||
|
super_: Some(self.keyboard_modifiers.logo),
|
||||||
|
};
|
||||||
|
let _ = sender.send(Ok(KeyboardKeyResponse {
|
||||||
|
key: Some(event.keysym.raw()),
|
||||||
|
modifiers: Some(api_modifiers),
|
||||||
|
pressed: Some(true),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_key(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &WlKeyboard,
|
||||||
|
_serial: u32,
|
||||||
|
event: KeyEvent,
|
||||||
|
) {
|
||||||
|
let Some(KeyboardFocus::Layer(layer)) = self.keyboard_focus.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(snowcap_layer) = self.layers.iter_mut().find(|sn_l| &sn_l.layer == layer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (key, location) = keysym_to_iced_key_and_loc(event.keysym);
|
||||||
|
|
||||||
|
let mut modifiers = iced::keyboard::Modifiers::empty();
|
||||||
|
if self.keyboard_modifiers.ctrl {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::CTRL;
|
||||||
|
}
|
||||||
|
if self.keyboard_modifiers.alt {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::ALT;
|
||||||
|
}
|
||||||
|
if self.keyboard_modifiers.shift {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::SHIFT;
|
||||||
|
}
|
||||||
|
if self.keyboard_modifiers.logo {
|
||||||
|
modifiers |= iced::keyboard::Modifiers::LOGO;
|
||||||
|
}
|
||||||
|
|
||||||
|
snowcap_layer.widgets.queue_event(iced::Event::Keyboard(
|
||||||
|
iced::keyboard::Event::KeyReleased {
|
||||||
|
key,
|
||||||
|
location,
|
||||||
|
modifiers,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_modifiers(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_keyboard: &WlKeyboard,
|
||||||
|
_serial: u32,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
_layout: u32,
|
||||||
|
) {
|
||||||
|
// TODO: per wl_keyboard
|
||||||
|
self.keyboard_modifiers = modifiers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_keyboard!(State);
|
||||||
|
|
||||||
|
pub enum KeyboardFocus {
|
||||||
|
Layer(LayerSurface),
|
||||||
|
}
|
102
snowcap/src/handlers/pointer.rs
Normal file
102
snowcap/src/handlers/pointer.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
use iced::mouse::ScrollDelta;
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
delegate_pointer,
|
||||||
|
reexports::client::{
|
||||||
|
protocol::wl_pointer::{AxisSource, WlPointer},
|
||||||
|
Connection, QueueHandle,
|
||||||
|
},
|
||||||
|
seat::pointer::{PointerEvent, PointerEventKind, PointerHandler},
|
||||||
|
shell::WaylandSurface,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
impl PointerHandler for State {
|
||||||
|
fn pointer_frame(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_pointer: &WlPointer,
|
||||||
|
events: &[PointerEvent],
|
||||||
|
) {
|
||||||
|
for event in events {
|
||||||
|
let Some(layer) = self
|
||||||
|
.layers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|sn_layer| sn_layer.layer.wl_surface() == &event.surface)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let iced_event = match event.kind {
|
||||||
|
PointerEventKind::Enter { serial: _ } => {
|
||||||
|
layer.pointer_location = Some(event.position);
|
||||||
|
iced::Event::Mouse(iced::mouse::Event::CursorEntered)
|
||||||
|
}
|
||||||
|
PointerEventKind::Leave { serial: _ } => {
|
||||||
|
layer.pointer_location = None;
|
||||||
|
iced::Event::Mouse(iced::mouse::Event::CursorLeft)
|
||||||
|
}
|
||||||
|
PointerEventKind::Motion { time: _ } => {
|
||||||
|
layer.pointer_location = Some(event.position);
|
||||||
|
iced::Event::Mouse(iced::mouse::Event::CursorMoved {
|
||||||
|
position: iced::Point {
|
||||||
|
x: event.position.0 as f32,
|
||||||
|
y: event.position.1 as f32,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
PointerEventKind::Press {
|
||||||
|
time: _,
|
||||||
|
button,
|
||||||
|
serial: _,
|
||||||
|
} => iced::Event::Mouse(iced::mouse::Event::ButtonPressed(button_to_iced_button(
|
||||||
|
button,
|
||||||
|
))),
|
||||||
|
PointerEventKind::Release {
|
||||||
|
time: _,
|
||||||
|
button,
|
||||||
|
serial: _,
|
||||||
|
} => iced::Event::Mouse(iced::mouse::Event::ButtonReleased(button_to_iced_button(
|
||||||
|
button,
|
||||||
|
))),
|
||||||
|
PointerEventKind::Axis {
|
||||||
|
time: _,
|
||||||
|
horizontal,
|
||||||
|
vertical,
|
||||||
|
source,
|
||||||
|
} => {
|
||||||
|
// Values are negated because they're backwards otherwise
|
||||||
|
let delta = match source {
|
||||||
|
Some(AxisSource::Wheel | AxisSource::WheelTilt) => ScrollDelta::Lines {
|
||||||
|
x: -horizontal.discrete as f32,
|
||||||
|
y: -vertical.discrete as f32,
|
||||||
|
},
|
||||||
|
Some(AxisSource::Finger | AxisSource::Continuous) => ScrollDelta::Pixels {
|
||||||
|
x: -horizontal.absolute as f32,
|
||||||
|
y: -vertical.absolute as f32,
|
||||||
|
},
|
||||||
|
// TODO: continue here or default to lines? prolly should
|
||||||
|
// look at the protocol docs
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
iced::Event::Mouse(iced::mouse::Event::WheelScrolled { delta })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
layer.widgets.queue_event(iced_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_pointer!(State);
|
||||||
|
|
||||||
|
fn button_to_iced_button(button: u32) -> iced::mouse::Button {
|
||||||
|
match button {
|
||||||
|
0x110 => iced::mouse::Button::Left,
|
||||||
|
0x111 => iced::mouse::Button::Right,
|
||||||
|
0x112 => iced::mouse::Button::Middle,
|
||||||
|
0x115 => iced::mouse::Button::Forward,
|
||||||
|
0x116 => iced::mouse::Button::Back,
|
||||||
|
button => iced::mouse::Button::Other(button as u16),
|
||||||
|
}
|
||||||
|
}
|
1
snowcap/src/input.rs
Normal file
1
snowcap/src/input.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod keyboard;
|
491
snowcap/src/input/keyboard.rs
Normal file
491
snowcap/src/input/keyboard.rs
Normal file
|
@ -0,0 +1,491 @@
|
||||||
|
use iced::keyboard::{key::Named, Key, Location};
|
||||||
|
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||||
|
|
||||||
|
// All this stuff from cosmic's iced-sctk
|
||||||
|
|
||||||
|
fn keysym_to_iced_key(keysym: Keysym) -> Key {
|
||||||
|
let named = match keysym {
|
||||||
|
// TTY function keys
|
||||||
|
Keysym::BackSpace => Named::Backspace,
|
||||||
|
Keysym::Tab => Named::Tab,
|
||||||
|
// Keysym::Linefeed => Named::Linefeed,
|
||||||
|
Keysym::Clear => Named::Clear,
|
||||||
|
Keysym::Return => Named::Enter,
|
||||||
|
Keysym::Pause => Named::Pause,
|
||||||
|
Keysym::Scroll_Lock => Named::ScrollLock,
|
||||||
|
Keysym::Sys_Req => Named::PrintScreen,
|
||||||
|
Keysym::Escape => Named::Escape,
|
||||||
|
Keysym::Delete => Named::Delete,
|
||||||
|
|
||||||
|
// IME keys
|
||||||
|
Keysym::Multi_key => Named::Compose,
|
||||||
|
Keysym::Codeinput => Named::CodeInput,
|
||||||
|
Keysym::SingleCandidate => Named::SingleCandidate,
|
||||||
|
Keysym::MultipleCandidate => Named::AllCandidates,
|
||||||
|
Keysym::PreviousCandidate => Named::PreviousCandidate,
|
||||||
|
|
||||||
|
// Japanese key
|
||||||
|
Keysym::Kanji => Named::KanjiMode,
|
||||||
|
Keysym::Muhenkan => Named::NonConvert,
|
||||||
|
Keysym::Henkan_Mode => Named::Convert,
|
||||||
|
Keysym::Romaji => Named::Romaji,
|
||||||
|
Keysym::Hiragana => Named::Hiragana,
|
||||||
|
Keysym::Hiragana_Katakana => Named::HiraganaKatakana,
|
||||||
|
Keysym::Zenkaku => Named::Zenkaku,
|
||||||
|
Keysym::Hankaku => Named::Hankaku,
|
||||||
|
Keysym::Zenkaku_Hankaku => Named::ZenkakuHankaku,
|
||||||
|
// Keysym::Touroku => Named::Touroku,
|
||||||
|
// Keysym::Massyo => Named::Massyo,
|
||||||
|
Keysym::Kana_Lock => Named::KanaMode,
|
||||||
|
Keysym::Kana_Shift => Named::KanaMode,
|
||||||
|
Keysym::Eisu_Shift => Named::Alphanumeric,
|
||||||
|
Keysym::Eisu_toggle => Named::Alphanumeric,
|
||||||
|
// NOTE: The next three items are aliases for values we've already mapped.
|
||||||
|
// Keysym::Kanji_Bangou => Named::CodeInput,
|
||||||
|
// Keysym::Zen_Koho => Named::AllCandidates,
|
||||||
|
// Keysym::Mae_Koho => Named::PreviousCandidate,
|
||||||
|
|
||||||
|
// Cursor control & motion
|
||||||
|
Keysym::Home => Named::Home,
|
||||||
|
Keysym::Left => Named::ArrowLeft,
|
||||||
|
Keysym::Up => Named::ArrowUp,
|
||||||
|
Keysym::Right => Named::ArrowRight,
|
||||||
|
Keysym::Down => Named::ArrowDown,
|
||||||
|
// Keysym::Prior => Named::PageUp,
|
||||||
|
Keysym::Page_Up => Named::PageUp,
|
||||||
|
// Keysym::Next => Named::PageDown,
|
||||||
|
Keysym::Page_Down => Named::PageDown,
|
||||||
|
Keysym::End => Named::End,
|
||||||
|
// Keysym::Begin => Named::Begin,
|
||||||
|
|
||||||
|
// Misc. functions
|
||||||
|
Keysym::Select => Named::Select,
|
||||||
|
Keysym::Print => Named::PrintScreen,
|
||||||
|
Keysym::Execute => Named::Execute,
|
||||||
|
Keysym::Insert => Named::Insert,
|
||||||
|
Keysym::Undo => Named::Undo,
|
||||||
|
Keysym::Redo => Named::Redo,
|
||||||
|
Keysym::Menu => Named::ContextMenu,
|
||||||
|
Keysym::Find => Named::Find,
|
||||||
|
Keysym::Cancel => Named::Cancel,
|
||||||
|
Keysym::Help => Named::Help,
|
||||||
|
Keysym::Break => Named::Pause,
|
||||||
|
Keysym::Mode_switch => Named::ModeChange,
|
||||||
|
// Keysym::script_switch => Named::ModeChange,
|
||||||
|
Keysym::Num_Lock => Named::NumLock,
|
||||||
|
|
||||||
|
// Keypad keys
|
||||||
|
// Keysym::KP_Space => return Key::Character(" "),
|
||||||
|
Keysym::KP_Tab => Named::Tab,
|
||||||
|
Keysym::KP_Enter => Named::Enter,
|
||||||
|
Keysym::KP_F1 => Named::F1,
|
||||||
|
Keysym::KP_F2 => Named::F2,
|
||||||
|
Keysym::KP_F3 => Named::F3,
|
||||||
|
Keysym::KP_F4 => Named::F4,
|
||||||
|
Keysym::KP_Home => Named::Home,
|
||||||
|
Keysym::KP_Left => Named::ArrowLeft,
|
||||||
|
Keysym::KP_Up => Named::ArrowUp,
|
||||||
|
Keysym::KP_Right => Named::ArrowRight,
|
||||||
|
Keysym::KP_Down => Named::ArrowDown,
|
||||||
|
// Keysym::KP_Prior => Named::PageUp,
|
||||||
|
Keysym::KP_Page_Up => Named::PageUp,
|
||||||
|
// Keysym::KP_Next => Named::PageDown,
|
||||||
|
Keysym::KP_Page_Down => Named::PageDown,
|
||||||
|
Keysym::KP_End => Named::End,
|
||||||
|
// This is the key labeled "5" on the numpad when NumLock is off.
|
||||||
|
// Keysym::KP_Begin => Named::Begin,
|
||||||
|
Keysym::KP_Insert => Named::Insert,
|
||||||
|
Keysym::KP_Delete => Named::Delete,
|
||||||
|
// Keysym::KP_Equal => Named::Equal,
|
||||||
|
// Keysym::KP_Multiply => Named::Multiply,
|
||||||
|
// Keysym::KP_Add => Named::Add,
|
||||||
|
// Keysym::KP_Separator => Named::Separator,
|
||||||
|
// Keysym::KP_Subtract => Named::Subtract,
|
||||||
|
// Keysym::KP_Decimal => Named::Decimal,
|
||||||
|
// Keysym::KP_Divide => Named::Divide,
|
||||||
|
|
||||||
|
// Keysym::KP_0 => return Key::Character("0"),
|
||||||
|
// Keysym::KP_1 => return Key::Character("1"),
|
||||||
|
// Keysym::KP_2 => return Key::Character("2"),
|
||||||
|
// Keysym::KP_3 => return Key::Character("3"),
|
||||||
|
// Keysym::KP_4 => return Key::Character("4"),
|
||||||
|
// Keysym::KP_5 => return Key::Character("5"),
|
||||||
|
// Keysym::KP_6 => return Key::Character("6"),
|
||||||
|
// Keysym::KP_7 => return Key::Character("7"),
|
||||||
|
// Keysym::KP_8 => return Key::Character("8"),
|
||||||
|
// Keysym::KP_9 => return Key::Character("9"),
|
||||||
|
|
||||||
|
// Function keys
|
||||||
|
Keysym::F1 => Named::F1,
|
||||||
|
Keysym::F2 => Named::F2,
|
||||||
|
Keysym::F3 => Named::F3,
|
||||||
|
Keysym::F4 => Named::F4,
|
||||||
|
Keysym::F5 => Named::F5,
|
||||||
|
Keysym::F6 => Named::F6,
|
||||||
|
Keysym::F7 => Named::F7,
|
||||||
|
Keysym::F8 => Named::F8,
|
||||||
|
Keysym::F9 => Named::F9,
|
||||||
|
Keysym::F10 => Named::F10,
|
||||||
|
Keysym::F11 => Named::F11,
|
||||||
|
Keysym::F12 => Named::F12,
|
||||||
|
Keysym::F13 => Named::F13,
|
||||||
|
Keysym::F14 => Named::F14,
|
||||||
|
Keysym::F15 => Named::F15,
|
||||||
|
Keysym::F16 => Named::F16,
|
||||||
|
Keysym::F17 => Named::F17,
|
||||||
|
Keysym::F18 => Named::F18,
|
||||||
|
Keysym::F19 => Named::F19,
|
||||||
|
Keysym::F20 => Named::F20,
|
||||||
|
Keysym::F21 => Named::F21,
|
||||||
|
Keysym::F22 => Named::F22,
|
||||||
|
Keysym::F23 => Named::F23,
|
||||||
|
Keysym::F24 => Named::F24,
|
||||||
|
Keysym::F25 => Named::F25,
|
||||||
|
Keysym::F26 => Named::F26,
|
||||||
|
Keysym::F27 => Named::F27,
|
||||||
|
Keysym::F28 => Named::F28,
|
||||||
|
Keysym::F29 => Named::F29,
|
||||||
|
Keysym::F30 => Named::F30,
|
||||||
|
Keysym::F31 => Named::F31,
|
||||||
|
Keysym::F32 => Named::F32,
|
||||||
|
Keysym::F33 => Named::F33,
|
||||||
|
Keysym::F34 => Named::F34,
|
||||||
|
Keysym::F35 => Named::F35,
|
||||||
|
|
||||||
|
// Modifiers
|
||||||
|
Keysym::Shift_L => Named::Shift,
|
||||||
|
Keysym::Shift_R => Named::Shift,
|
||||||
|
Keysym::Control_L => Named::Control,
|
||||||
|
Keysym::Control_R => Named::Control,
|
||||||
|
Keysym::Caps_Lock => Named::CapsLock,
|
||||||
|
// Keysym::Shift_Lock => Named::ShiftLock,
|
||||||
|
|
||||||
|
// Keysym::Meta_L => Named::Meta,
|
||||||
|
// Keysym::Meta_R => Named::Meta,
|
||||||
|
Keysym::Alt_L => Named::Alt,
|
||||||
|
Keysym::Alt_R => Named::Alt,
|
||||||
|
Keysym::Super_L => Named::Super,
|
||||||
|
Keysym::Super_R => Named::Super,
|
||||||
|
Keysym::Hyper_L => Named::Hyper,
|
||||||
|
Keysym::Hyper_R => Named::Hyper,
|
||||||
|
|
||||||
|
// XKB function and modifier keys
|
||||||
|
// Keysym::ISO_Lock => Named::IsoLock,
|
||||||
|
// Keysym::ISO_Level2_Latch => Named::IsoLevel2Latch,
|
||||||
|
Keysym::ISO_Level3_Shift => Named::AltGraph,
|
||||||
|
Keysym::ISO_Level3_Latch => Named::AltGraph,
|
||||||
|
Keysym::ISO_Level3_Lock => Named::AltGraph,
|
||||||
|
// Keysym::ISO_Level5_Shift => Named::IsoLevel5Shift,
|
||||||
|
// Keysym::ISO_Level5_Latch => Named::IsoLevel5Latch,
|
||||||
|
// Keysym::ISO_Level5_Lock => Named::IsoLevel5Lock,
|
||||||
|
// Keysym::ISO_Group_Shift => Named::IsoGroupShift,
|
||||||
|
// Keysym::ISO_Group_Latch => Named::IsoGroupLatch,
|
||||||
|
// Keysym::ISO_Group_Lock => Named::IsoGroupLock,
|
||||||
|
Keysym::ISO_Next_Group => Named::GroupNext,
|
||||||
|
// Keysym::ISO_Next_Group_Lock => Named::GroupNextLock,
|
||||||
|
Keysym::ISO_Prev_Group => Named::GroupPrevious,
|
||||||
|
// Keysym::ISO_Prev_Group_Lock => Named::GroupPreviousLock,
|
||||||
|
Keysym::ISO_First_Group => Named::GroupFirst,
|
||||||
|
// Keysym::ISO_First_Group_Lock => Named::GroupFirstLock,
|
||||||
|
Keysym::ISO_Last_Group => Named::GroupLast,
|
||||||
|
// Keysym::ISO_Last_Group_Lock => Named::GroupLastLock,
|
||||||
|
//
|
||||||
|
Keysym::ISO_Left_Tab => Named::Tab,
|
||||||
|
// Keysym::ISO_Move_Line_Up => Named::IsoMoveLineUp,
|
||||||
|
// Keysym::ISO_Move_Line_Down => Named::IsoMoveLineDown,
|
||||||
|
// Keysym::ISO_Partial_Line_Up => Named::IsoPartialLineUp,
|
||||||
|
// Keysym::ISO_Partial_Line_Down => Named::IsoPartialLineDown,
|
||||||
|
// Keysym::ISO_Partial_Space_Left => Named::IsoPartialSpaceLeft,
|
||||||
|
// Keysym::ISO_Partial_Space_Right => Named::IsoPartialSpaceRight,
|
||||||
|
// Keysym::ISO_Set_Margin_Left => Named::IsoSetMarginLeft,
|
||||||
|
// Keysym::ISO_Set_Margin_Right => Named::IsoSetMarginRight,
|
||||||
|
// Keysym::ISO_Release_Margin_Left => Named::IsoReleaseMarginLeft,
|
||||||
|
// Keysym::ISO_Release_Margin_Right => Named::IsoReleaseMarginRight,
|
||||||
|
// Keysym::ISO_Release_Both_Margins => Named::IsoReleaseBothMargins,
|
||||||
|
// Keysym::ISO_Fast_Cursor_Left => Named::IsoFastCursorLeft,
|
||||||
|
// Keysym::ISO_Fast_Cursor_Right => Named::IsoFastCursorRight,
|
||||||
|
// Keysym::ISO_Fast_Cursor_Up => Named::IsoFastCursorUp,
|
||||||
|
// Keysym::ISO_Fast_Cursor_Down => Named::IsoFastCursorDown,
|
||||||
|
// Keysym::ISO_Continuous_Underline => Named::IsoContinuousUnderline,
|
||||||
|
// Keysym::ISO_Discontinuous_Underline => Named::IsoDiscontinuousUnderline,
|
||||||
|
// Keysym::ISO_Emphasize => Named::IsoEmphasize,
|
||||||
|
// Keysym::ISO_Center_Object => Named::IsoCenterObject,
|
||||||
|
Keysym::ISO_Enter => Named::Enter,
|
||||||
|
|
||||||
|
// dead_grave..dead_currency
|
||||||
|
|
||||||
|
// dead_lowline..dead_longsolidusoverlay
|
||||||
|
|
||||||
|
// dead_a..dead_capital_schwa
|
||||||
|
|
||||||
|
// dead_greek
|
||||||
|
|
||||||
|
// First_Virtual_Screen..Terminate_Server
|
||||||
|
|
||||||
|
// AccessX_Enable..AudibleBell_Enable
|
||||||
|
|
||||||
|
// Pointer_Left..Pointer_Drag5
|
||||||
|
|
||||||
|
// Pointer_EnableKeys..Pointer_DfltBtnPrev
|
||||||
|
|
||||||
|
// ch..C_H
|
||||||
|
|
||||||
|
// 3270 terminal keys
|
||||||
|
// Keysym::3270_Duplicate => Named::Duplicate,
|
||||||
|
// Keysym::3270_FieldMark => Named::FieldMark,
|
||||||
|
// Keysym::3270_Right2 => Named::Right2,
|
||||||
|
// Keysym::3270_Left2 => Named::Left2,
|
||||||
|
// Keysym::3270_BackTab => Named::BackTab,
|
||||||
|
Keysym::_3270_EraseEOF => Named::EraseEof,
|
||||||
|
// Keysym::3270_EraseInput => Named::EraseInput,
|
||||||
|
// Keysym::3270_Reset => Named::Reset,
|
||||||
|
// Keysym::3270_Quit => Named::Quit,
|
||||||
|
// Keysym::3270_PA1 => Named::Pa1,
|
||||||
|
// Keysym::3270_PA2 => Named::Pa2,
|
||||||
|
// Keysym::3270_PA3 => Named::Pa3,
|
||||||
|
// Keysym::3270_Test => Named::Test,
|
||||||
|
Keysym::_3270_Attn => Named::Attn,
|
||||||
|
// Keysym::3270_CursorBlink => Named::CursorBlink,
|
||||||
|
// Keysym::3270_AltCursor => Named::AltCursor,
|
||||||
|
// Keysym::3270_KeyClick => Named::KeyClick,
|
||||||
|
// Keysym::3270_Jump => Named::Jump,
|
||||||
|
// Keysym::3270_Ident => Named::Ident,
|
||||||
|
// Keysym::3270_Rule => Named::Rule,
|
||||||
|
// Keysym::3270_Copy => Named::Copy,
|
||||||
|
Keysym::_3270_Play => Named::Play,
|
||||||
|
// Keysym::3270_Setup => Named::Setup,
|
||||||
|
// Keysym::3270_Record => Named::Record,
|
||||||
|
// Keysym::3270_ChangeScreen => Named::ChangeScreen,
|
||||||
|
// Keysym::3270_DeleteWord => Named::DeleteWord,
|
||||||
|
Keysym::_3270_ExSelect => Named::ExSel,
|
||||||
|
Keysym::_3270_CursorSelect => Named::CrSel,
|
||||||
|
Keysym::_3270_PrintScreen => Named::PrintScreen,
|
||||||
|
Keysym::_3270_Enter => Named::Enter,
|
||||||
|
|
||||||
|
Keysym::space => Named::Space,
|
||||||
|
// exclam..Sinh_kunddaliya
|
||||||
|
|
||||||
|
// XFree86
|
||||||
|
// Keysym::XF86_ModeLock => Named::ModeLock,
|
||||||
|
|
||||||
|
// XFree86 - Backlight controls
|
||||||
|
Keysym::XF86_MonBrightnessUp => Named::BrightnessUp,
|
||||||
|
Keysym::XF86_MonBrightnessDown => Named::BrightnessDown,
|
||||||
|
// Keysym::XF86_KbdLightOnOff => Named::LightOnOff,
|
||||||
|
// Keysym::XF86_KbdBrightnessUp => Named::KeyboardBrightnessUp,
|
||||||
|
// Keysym::XF86_KbdBrightnessDown => Named::KeyboardBrightnessDown,
|
||||||
|
|
||||||
|
// XFree86 - "Internet"
|
||||||
|
Keysym::XF86_Standby => Named::Standby,
|
||||||
|
Keysym::XF86_AudioLowerVolume => Named::AudioVolumeDown,
|
||||||
|
Keysym::XF86_AudioRaiseVolume => Named::AudioVolumeUp,
|
||||||
|
Keysym::XF86_AudioPlay => Named::MediaPlay,
|
||||||
|
Keysym::XF86_AudioStop => Named::MediaStop,
|
||||||
|
Keysym::XF86_AudioPrev => Named::MediaTrackPrevious,
|
||||||
|
Keysym::XF86_AudioNext => Named::MediaTrackNext,
|
||||||
|
Keysym::XF86_HomePage => Named::BrowserHome,
|
||||||
|
Keysym::XF86_Mail => Named::LaunchMail,
|
||||||
|
// Keysym::XF86_Start => Named::Start,
|
||||||
|
Keysym::XF86_Search => Named::BrowserSearch,
|
||||||
|
Keysym::XF86_AudioRecord => Named::MediaRecord,
|
||||||
|
|
||||||
|
// XFree86 - PDA
|
||||||
|
Keysym::XF86_Calculator => Named::LaunchApplication2,
|
||||||
|
// Keysym::XF86_Memo => Named::Memo,
|
||||||
|
// Keysym::XF86_ToDoList => Named::ToDoList,
|
||||||
|
Keysym::XF86_Calendar => Named::LaunchCalendar,
|
||||||
|
Keysym::XF86_PowerDown => Named::Power,
|
||||||
|
// Keysym::XF86_ContrastAdjust => Named::AdjustContrast,
|
||||||
|
// Keysym::XF86_RockerUp => Named::RockerUp,
|
||||||
|
// Keysym::XF86_RockerDown => Named::RockerDown,
|
||||||
|
// Keysym::XF86_RockerEnter => Named::RockerEnter,
|
||||||
|
|
||||||
|
// XFree86 - More "Internet"
|
||||||
|
Keysym::XF86_Back => Named::BrowserBack,
|
||||||
|
Keysym::XF86_Forward => Named::BrowserForward,
|
||||||
|
// Keysym::XF86_Stop => Named::Stop,
|
||||||
|
Keysym::XF86_Refresh => Named::BrowserRefresh,
|
||||||
|
Keysym::XF86_PowerOff => Named::Power,
|
||||||
|
Keysym::XF86_WakeUp => Named::WakeUp,
|
||||||
|
Keysym::XF86_Eject => Named::Eject,
|
||||||
|
Keysym::XF86_ScreenSaver => Named::LaunchScreenSaver,
|
||||||
|
Keysym::XF86_WWW => Named::LaunchWebBrowser,
|
||||||
|
Keysym::XF86_Sleep => Named::Standby,
|
||||||
|
Keysym::XF86_Favorites => Named::BrowserFavorites,
|
||||||
|
Keysym::XF86_AudioPause => Named::MediaPause,
|
||||||
|
// Keysym::XF86_AudioMedia => Named::AudioMedia,
|
||||||
|
Keysym::XF86_MyComputer => Named::LaunchApplication1,
|
||||||
|
// Keysym::XF86_VendorHome => Named::VendorHome,
|
||||||
|
// Keysym::XF86_LightBulb => Named::LightBulb,
|
||||||
|
// Keysym::XF86_Shop => Named::BrowserShop,
|
||||||
|
// Keysym::XF86_History => Named::BrowserHistory,
|
||||||
|
// Keysym::XF86_OpenURL => Named::OpenUrl,
|
||||||
|
// Keysym::XF86_AddFavorite => Named::AddFavorite,
|
||||||
|
// Keysym::XF86_HotLinks => Named::HotLinks,
|
||||||
|
// Keysym::XF86_BrightnessAdjust => Named::BrightnessAdjust,
|
||||||
|
// Keysym::XF86_Finance => Named::BrowserFinance,
|
||||||
|
// Keysym::XF86_Community => Named::BrowserCommunity,
|
||||||
|
Keysym::XF86_AudioRewind => Named::MediaRewind,
|
||||||
|
// Keysym::XF86_BackForward => Key::???,
|
||||||
|
// XF86_Launch0..XF86_LaunchF
|
||||||
|
|
||||||
|
// XF86_ApplicationLeft..XF86_CD
|
||||||
|
Keysym::XF86_Calculater => Named::LaunchApplication2, // Nice typo, libxkbcommon :)
|
||||||
|
// XF86_Clear
|
||||||
|
Keysym::XF86_Close => Named::Close,
|
||||||
|
Keysym::XF86_Copy => Named::Copy,
|
||||||
|
Keysym::XF86_Cut => Named::Cut,
|
||||||
|
// XF86_Display..XF86_Documents
|
||||||
|
Keysym::XF86_Excel => Named::LaunchSpreadsheet,
|
||||||
|
// XF86_Explorer..XF86iTouch
|
||||||
|
Keysym::XF86_LogOff => Named::LogOff,
|
||||||
|
// XF86_Market..XF86_MenuPB
|
||||||
|
Keysym::XF86_MySites => Named::BrowserFavorites,
|
||||||
|
Keysym::XF86_New => Named::New,
|
||||||
|
// XF86_News..XF86_OfficeHome
|
||||||
|
Keysym::XF86_Open => Named::Open,
|
||||||
|
// XF86_Option
|
||||||
|
Keysym::XF86_Paste => Named::Paste,
|
||||||
|
Keysym::XF86_Phone => Named::LaunchPhone,
|
||||||
|
// XF86_Q
|
||||||
|
Keysym::XF86_Reply => Named::MailReply,
|
||||||
|
Keysym::XF86_Reload => Named::BrowserRefresh,
|
||||||
|
// XF86_RotateWindows..XF86_RotationKB
|
||||||
|
Keysym::XF86_Save => Named::Save,
|
||||||
|
// XF86_ScrollUp..XF86_ScrollClick
|
||||||
|
Keysym::XF86_Send => Named::MailSend,
|
||||||
|
Keysym::XF86_Spell => Named::SpellCheck,
|
||||||
|
Keysym::XF86_SplitScreen => Named::SplitScreenToggle,
|
||||||
|
// XF86_Support..XF86_User2KB
|
||||||
|
Keysym::XF86_Video => Named::LaunchMediaPlayer,
|
||||||
|
// XF86_WheelButton
|
||||||
|
Keysym::XF86_Word => Named::LaunchWordProcessor,
|
||||||
|
// XF86_Xfer
|
||||||
|
Keysym::XF86_ZoomIn => Named::ZoomIn,
|
||||||
|
Keysym::XF86_ZoomOut => Named::ZoomOut,
|
||||||
|
|
||||||
|
// XF86_Away..XF86_Messenger
|
||||||
|
Keysym::XF86_WebCam => Named::LaunchWebCam,
|
||||||
|
Keysym::XF86_MailForward => Named::MailForward,
|
||||||
|
// XF86_Pictures
|
||||||
|
Keysym::XF86_Music => Named::LaunchMusicPlayer,
|
||||||
|
|
||||||
|
// XF86_Battery..XF86_UWB
|
||||||
|
//
|
||||||
|
Keysym::XF86_AudioForward => Named::MediaFastForward,
|
||||||
|
// XF86_AudioRepeat
|
||||||
|
Keysym::XF86_AudioRandomPlay => Named::RandomToggle,
|
||||||
|
Keysym::XF86_Subtitle => Named::Subtitle,
|
||||||
|
Keysym::XF86_AudioCycleTrack => Named::MediaAudioTrack,
|
||||||
|
// XF86_CycleAngle..XF86_Blue
|
||||||
|
//
|
||||||
|
Keysym::XF86_Suspend => Named::Standby,
|
||||||
|
Keysym::XF86_Hibernate => Named::Hibernate,
|
||||||
|
// XF86_TouchpadToggle..XF86_TouchpadOff
|
||||||
|
//
|
||||||
|
Keysym::XF86_AudioMute => Named::AudioVolumeMute,
|
||||||
|
|
||||||
|
// XF86_Switch_VT_1..XF86_Switch_VT_12
|
||||||
|
|
||||||
|
// XF86_Ungrab..XF86_ClearGrab
|
||||||
|
Keysym::XF86_Next_VMode => Named::VideoModeNext,
|
||||||
|
// Keysym::XF86_Prev_VMode => Named::VideoModePrevious,
|
||||||
|
// XF86_LogWindowTree..XF86_LogGrabInfo
|
||||||
|
|
||||||
|
// SunFA_Grave..SunFA_Cedilla
|
||||||
|
|
||||||
|
// Keysym::SunF36 => Named::F36 | Named::F11,
|
||||||
|
// Keysym::SunF37 => Named::F37 | Named::F12,
|
||||||
|
|
||||||
|
// Keysym::SunSys_Req => Named::PrintScreen,
|
||||||
|
// The next couple of xkb (until SunStop) are already handled.
|
||||||
|
// SunPrint_Screen..SunPageDown
|
||||||
|
|
||||||
|
// SunUndo..SunFront
|
||||||
|
Keysym::SUN_Copy => Named::Copy,
|
||||||
|
Keysym::SUN_Open => Named::Open,
|
||||||
|
Keysym::SUN_Paste => Named::Paste,
|
||||||
|
Keysym::SUN_Cut => Named::Cut,
|
||||||
|
|
||||||
|
// SunPowerSwitch
|
||||||
|
Keysym::SUN_AudioLowerVolume => Named::AudioVolumeDown,
|
||||||
|
Keysym::SUN_AudioMute => Named::AudioVolumeMute,
|
||||||
|
Keysym::SUN_AudioRaiseVolume => Named::AudioVolumeUp,
|
||||||
|
// SUN_VideoDegauss
|
||||||
|
Keysym::SUN_VideoLowerBrightness => Named::BrightnessDown,
|
||||||
|
Keysym::SUN_VideoRaiseBrightness => Named::BrightnessUp,
|
||||||
|
// SunPowerSwitchShift
|
||||||
|
//
|
||||||
|
_ => return Key::Unidentified,
|
||||||
|
};
|
||||||
|
|
||||||
|
Key::Named(named)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keysym_location(keysym: Keysym) -> Location {
|
||||||
|
match keysym {
|
||||||
|
Keysym::Shift_L
|
||||||
|
| Keysym::Control_L
|
||||||
|
| Keysym::Meta_L
|
||||||
|
| Keysym::Alt_L
|
||||||
|
| Keysym::Super_L
|
||||||
|
| Keysym::Hyper_L => Location::Left,
|
||||||
|
Keysym::Shift_R
|
||||||
|
| Keysym::Control_R
|
||||||
|
| Keysym::Meta_R
|
||||||
|
| Keysym::Alt_R
|
||||||
|
| Keysym::Super_R
|
||||||
|
| Keysym::Hyper_R => Location::Right,
|
||||||
|
Keysym::KP_0
|
||||||
|
| Keysym::KP_1
|
||||||
|
| Keysym::KP_2
|
||||||
|
| Keysym::KP_3
|
||||||
|
| Keysym::KP_4
|
||||||
|
| Keysym::KP_5
|
||||||
|
| Keysym::KP_6
|
||||||
|
| Keysym::KP_7
|
||||||
|
| Keysym::KP_8
|
||||||
|
| Keysym::KP_9
|
||||||
|
| Keysym::KP_Space
|
||||||
|
| Keysym::KP_Tab
|
||||||
|
| Keysym::KP_Enter
|
||||||
|
| Keysym::KP_F1
|
||||||
|
| Keysym::KP_F2
|
||||||
|
| Keysym::KP_F3
|
||||||
|
| Keysym::KP_F4
|
||||||
|
| Keysym::KP_Home
|
||||||
|
| Keysym::KP_Left
|
||||||
|
| Keysym::KP_Up
|
||||||
|
| Keysym::KP_Right
|
||||||
|
| Keysym::KP_Down
|
||||||
|
| Keysym::KP_Page_Up
|
||||||
|
| Keysym::KP_Page_Down
|
||||||
|
| Keysym::KP_End
|
||||||
|
| Keysym::KP_Begin
|
||||||
|
| Keysym::KP_Insert
|
||||||
|
| Keysym::KP_Delete
|
||||||
|
| Keysym::KP_Equal
|
||||||
|
| Keysym::KP_Multiply
|
||||||
|
| Keysym::KP_Add
|
||||||
|
| Keysym::KP_Separator
|
||||||
|
| Keysym::KP_Subtract
|
||||||
|
| Keysym::KP_Decimal
|
||||||
|
| Keysym::KP_Divide => Location::Numpad,
|
||||||
|
_ => Location::Standard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keysym_to_iced_key_and_loc(keysym: Keysym) -> (Key, Location) {
|
||||||
|
let raw = keysym;
|
||||||
|
let mut key = keysym_to_iced_key(keysym);
|
||||||
|
if matches!(key, Key::Unidentified) {
|
||||||
|
let mut utf8 = xkbcommon::xkb::keysym_to_utf8(keysym);
|
||||||
|
utf8.pop();
|
||||||
|
if !utf8.is_empty() {
|
||||||
|
key = Key::Character(utf8.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let location = keysym_location(raw);
|
||||||
|
(key, location)
|
||||||
|
}
|
302
snowcap/src/layer.rs
Normal file
302
snowcap/src/layer.rs
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
use std::{num::NonZeroU32, ptr::NonNull};
|
||||||
|
|
||||||
|
use iced::{Color, Size, Theme};
|
||||||
|
use iced_futures::Runtime;
|
||||||
|
use iced_runtime::Debug;
|
||||||
|
use iced_wgpu::{graphics::Viewport, wgpu::SurfaceTargetUnsafe};
|
||||||
|
use raw_window_handle::{
|
||||||
|
RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
|
||||||
|
};
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
reexports::{
|
||||||
|
calloop,
|
||||||
|
client::{Proxy, QueueHandle},
|
||||||
|
},
|
||||||
|
shell::{
|
||||||
|
wlr_layer::{self, Anchor, LayerSurface},
|
||||||
|
WaylandSurface,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use snowcap_api_defs::snowcap::input::v0alpha1::{KeyboardKeyResponse, PointerButtonResponse};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use tonic::Status;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
clipboard::WaylandClipboard,
|
||||||
|
runtime::{CalloopSenderSink, CurrentTokioExecutor},
|
||||||
|
state::State,
|
||||||
|
widget::{SnowcapMessage, SnowcapWidgetProgram, WidgetId},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SnowcapLayer {
|
||||||
|
// SAFETY: Drop order: surface needs to be dropped before the layer
|
||||||
|
surface: iced_wgpu::wgpu::Surface<'static>,
|
||||||
|
pub layer: LayerSurface,
|
||||||
|
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub scale: i32,
|
||||||
|
pub viewport: Viewport,
|
||||||
|
|
||||||
|
pub widgets: iced_runtime::program::State<SnowcapWidgetProgram>,
|
||||||
|
pub clipboard: WaylandClipboard,
|
||||||
|
|
||||||
|
pub pointer_location: Option<(f64, f64)>,
|
||||||
|
|
||||||
|
pub runtime: Runtime<CurrentTokioExecutor, CalloopSenderSink<SnowcapMessage>, SnowcapMessage>,
|
||||||
|
|
||||||
|
pub widget_id: WidgetId,
|
||||||
|
|
||||||
|
pub keyboard_key_sender: Option<UnboundedSender<Result<KeyboardKeyResponse, Status>>>,
|
||||||
|
pub pointer_button_sender: Option<UnboundedSender<Result<PointerButtonResponse, Status>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ExclusiveZone {
|
||||||
|
/// This layer surface wants an exclusive zone of the given size.
|
||||||
|
Exclusive(NonZeroU32),
|
||||||
|
/// This layer surface does not have an exclusive zone but wants to be placed respecting any.
|
||||||
|
Respect,
|
||||||
|
/// This layer surface does not have an exclusive zone and wants to be placed ignoring any.
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnowcapLayer {
|
||||||
|
pub fn new(
|
||||||
|
state: &mut State,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
layer: wlr_layer::Layer,
|
||||||
|
anchor: Anchor,
|
||||||
|
exclusive_zone: ExclusiveZone,
|
||||||
|
keyboard_interactivity: wlr_layer::KeyboardInteractivity,
|
||||||
|
program: SnowcapWidgetProgram,
|
||||||
|
) -> Self {
|
||||||
|
let surface = state.compositor_state.create_surface(&state.queue_handle);
|
||||||
|
let layer = state.layer_shell_state.create_layer_surface(
|
||||||
|
&state.queue_handle,
|
||||||
|
surface,
|
||||||
|
layer,
|
||||||
|
Some("snowcap"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
layer.set_size(width, height);
|
||||||
|
layer.set_anchor(anchor);
|
||||||
|
layer.set_keyboard_interactivity(keyboard_interactivity);
|
||||||
|
layer.set_exclusive_zone(match exclusive_zone {
|
||||||
|
ExclusiveZone::Exclusive(size) => size.get() as i32,
|
||||||
|
ExclusiveZone::Respect => 0,
|
||||||
|
ExclusiveZone::Ignore => -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.commit();
|
||||||
|
|
||||||
|
let raw_display_handle = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(
|
||||||
|
NonNull::new(state.conn.backend().display_ptr() as *mut _).unwrap(),
|
||||||
|
));
|
||||||
|
let raw_window_handle = RawWindowHandle::Wayland(WaylandWindowHandle::new(
|
||||||
|
NonNull::new(layer.wl_surface().id().as_ptr() as *mut _).unwrap(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let wgpu_surface = unsafe {
|
||||||
|
state
|
||||||
|
.wgpu
|
||||||
|
.instance
|
||||||
|
.create_surface_unsafe(SurfaceTargetUnsafe::RawHandle {
|
||||||
|
raw_display_handle,
|
||||||
|
raw_window_handle,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let surface_config = iced_wgpu::wgpu::SurfaceConfiguration {
|
||||||
|
usage: iced_wgpu::wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: iced_wgpu::wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
present_mode: iced_wgpu::wgpu::PresentMode::Mailbox,
|
||||||
|
desired_maximum_frame_latency: 1,
|
||||||
|
alpha_mode: iced_wgpu::wgpu::CompositeAlphaMode::PreMultiplied,
|
||||||
|
view_formats: vec![iced_wgpu::wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||||
|
};
|
||||||
|
|
||||||
|
wgpu_surface.configure(&state.wgpu.device, &surface_config);
|
||||||
|
|
||||||
|
let widgets = iced_runtime::program::State::new(
|
||||||
|
program,
|
||||||
|
[width as f32, height as f32].into(),
|
||||||
|
&mut state.wgpu.renderer,
|
||||||
|
&mut iced_runtime::Debug::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let clipboard =
|
||||||
|
unsafe { WaylandClipboard::new(state.conn.backend().display_ptr() as *mut _) };
|
||||||
|
|
||||||
|
let (sender, recv) = calloop::channel::channel::<SnowcapMessage>();
|
||||||
|
let runtime = Runtime::new(CurrentTokioExecutor, CalloopSenderSink::new(sender));
|
||||||
|
|
||||||
|
let layer_clone = layer.clone();
|
||||||
|
state
|
||||||
|
.loop_handle
|
||||||
|
.insert_source(recv, move |event, _, state| match event {
|
||||||
|
calloop::channel::Event::Msg(message) => {
|
||||||
|
let Some(layer) = state
|
||||||
|
.layers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|sn_layer| sn_layer.layer == layer_clone)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match message {
|
||||||
|
SnowcapMessage::Close => {
|
||||||
|
state
|
||||||
|
.layers
|
||||||
|
.retain(|sn_layer| sn_layer.layer != layer_clone);
|
||||||
|
}
|
||||||
|
msg => {
|
||||||
|
layer.widgets.queue_message(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calloop::channel::Event::Closed => (),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// runtime.track(
|
||||||
|
// iced::keyboard::on_key_press(|key, _mods| {
|
||||||
|
// if matches!(
|
||||||
|
// key,
|
||||||
|
// iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape)
|
||||||
|
// ) {
|
||||||
|
// Some(SnowcapMessage::Close)
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .into_recipes(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
let next_id = state.widget_id_counter.next_and_increment();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surface: wgpu_surface,
|
||||||
|
layer,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
scale: 1,
|
||||||
|
viewport: Viewport::with_physical_size(Size::new(width, height), 1.0),
|
||||||
|
widgets,
|
||||||
|
clipboard,
|
||||||
|
pointer_location: None,
|
||||||
|
runtime,
|
||||||
|
widget_id: next_id,
|
||||||
|
keyboard_key_sender: None,
|
||||||
|
pointer_button_sender: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(
|
||||||
|
&self,
|
||||||
|
device: &iced_wgpu::wgpu::Device,
|
||||||
|
queue: &iced_wgpu::wgpu::Queue,
|
||||||
|
renderer: &mut iced_wgpu::Renderer,
|
||||||
|
_qh: &QueueHandle<State>,
|
||||||
|
) {
|
||||||
|
let Ok(frame) = self.surface.get_current_texture() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encoder =
|
||||||
|
device.create_command_encoder(&iced_wgpu::wgpu::CommandEncoderDescriptor::default());
|
||||||
|
|
||||||
|
let view = frame
|
||||||
|
.texture
|
||||||
|
.create_view(&iced_wgpu::wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
{
|
||||||
|
renderer.with_primitives(|backend, primitives| {
|
||||||
|
backend.present::<String>(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&mut encoder,
|
||||||
|
Some(iced::Color::TRANSPARENT),
|
||||||
|
iced_wgpu::wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
&view,
|
||||||
|
primitives,
|
||||||
|
&self.viewport,
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.submit(Some(encoder.finish()));
|
||||||
|
|
||||||
|
self.layer.wl_surface().damage_buffer(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.width as i32 * self.scale,
|
||||||
|
self.height as i32 * self.scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
// self.layer
|
||||||
|
// .wl_surface()
|
||||||
|
// .frame(qh, self.layer.wl_surface().clone());
|
||||||
|
|
||||||
|
// Does a commit
|
||||||
|
frame.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_and_draw(
|
||||||
|
&mut self,
|
||||||
|
device: &iced_wgpu::wgpu::Device,
|
||||||
|
queue: &iced_wgpu::wgpu::Queue,
|
||||||
|
renderer: &mut iced_wgpu::Renderer,
|
||||||
|
qh: &QueueHandle<State>,
|
||||||
|
) {
|
||||||
|
let cursor = match self.pointer_location {
|
||||||
|
Some((x, y)) => iced::mouse::Cursor::Available(iced::Point {
|
||||||
|
x: x as f32,
|
||||||
|
y: y as f32,
|
||||||
|
}),
|
||||||
|
None => iced::mouse::Cursor::Unavailable,
|
||||||
|
};
|
||||||
|
// TODO: the command bit
|
||||||
|
let (events, _command) = self.widgets.update(
|
||||||
|
self.viewport.logical_size(),
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
&Theme::CatppuccinFrappe,
|
||||||
|
&iced_wgpu::core::renderer::Style {
|
||||||
|
text_color: Color::WHITE,
|
||||||
|
},
|
||||||
|
&mut self.clipboard,
|
||||||
|
&mut Debug::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for event in events {
|
||||||
|
self.runtime.broadcast(event, iced::event::Status::Ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.draw(device, queue, renderer, qh);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_scale(&mut self, scale: i32, device: &iced_wgpu::wgpu::Device) {
|
||||||
|
self.scale = scale;
|
||||||
|
self.layer.wl_surface().set_buffer_scale(scale);
|
||||||
|
|
||||||
|
let surface_config = iced_wgpu::wgpu::SurfaceConfiguration {
|
||||||
|
usage: iced_wgpu::wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: iced_wgpu::wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
width: self.width * scale as u32,
|
||||||
|
height: self.height * scale as u32,
|
||||||
|
present_mode: iced_wgpu::wgpu::PresentMode::Mailbox,
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
alpha_mode: iced_wgpu::wgpu::CompositeAlphaMode::PreMultiplied,
|
||||||
|
view_formats: vec![iced_wgpu::wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||||
|
};
|
||||||
|
|
||||||
|
self.surface.configure(device, &surface_config);
|
||||||
|
}
|
||||||
|
}
|
89
snowcap/src/lib.rs
Normal file
89
snowcap/src/lib.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
pub mod api;
|
||||||
|
pub mod clipboard;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod input;
|
||||||
|
pub mod layer;
|
||||||
|
pub mod runtime;
|
||||||
|
pub mod server;
|
||||||
|
pub mod state;
|
||||||
|
pub mod util;
|
||||||
|
pub mod wgpu;
|
||||||
|
pub mod widget;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use server::socket_dir;
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
reexports::calloop::{self, EventLoop},
|
||||||
|
shell::WaylandSurface,
|
||||||
|
};
|
||||||
|
use state::State;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
/// A signal for Rust integrations to stop Snowcap.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StopSignal(calloop::ping::Ping);
|
||||||
|
|
||||||
|
impl StopSignal {
|
||||||
|
/// Send the stop signal to Snowcap.
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.0.ping();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(stop_signal_sender: Option<tokio::sync::oneshot::Sender<StopSignal>>) {
|
||||||
|
info!("Snowcap starting up");
|
||||||
|
|
||||||
|
let mut event_loop = EventLoop::<State>::try_new().unwrap();
|
||||||
|
|
||||||
|
let mut state = State::new(event_loop.handle(), event_loop.get_signal()).unwrap();
|
||||||
|
|
||||||
|
state.start_grpc_server(socket_dir()).unwrap();
|
||||||
|
|
||||||
|
if let Some(sender) = stop_signal_sender {
|
||||||
|
let (ping, ping_source) = calloop::ping::make_ping().unwrap();
|
||||||
|
let loop_signal = event_loop.get_signal();
|
||||||
|
|
||||||
|
event_loop
|
||||||
|
.handle()
|
||||||
|
.insert_source(ping_source, move |_, _, _| {
|
||||||
|
loop_signal.stop();
|
||||||
|
loop_signal.wakeup();
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sender.send(StopSignal(ping)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
event_loop
|
||||||
|
.run(Duration::from_secs(1), &mut state, |state| {
|
||||||
|
let keyboard_focus_is_dead =
|
||||||
|
state
|
||||||
|
.keyboard_focus
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|focus| match focus {
|
||||||
|
handlers::keyboard::KeyboardFocus::Layer(layer) => {
|
||||||
|
!state.layers.iter().any(|sn_layer| &sn_layer.layer == layer)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if keyboard_focus_is_dead {
|
||||||
|
state.keyboard_focus = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for layer in state.layers.iter_mut() {
|
||||||
|
if !layer.widgets.is_queue_empty() {
|
||||||
|
layer
|
||||||
|
.layer
|
||||||
|
.wl_surface()
|
||||||
|
.frame(&state.queue_handle, layer.layer.wl_surface().clone());
|
||||||
|
layer.layer.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_on_tokio<F: Future>(future: F) -> F::Output {
|
||||||
|
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(future))
|
||||||
|
}
|
17
snowcap/src/main.rs
Normal file
17
snowcap/src/main.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("snowcap=info"));
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.compact()
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(|| snowcap::start(None))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
60
snowcap/src/runtime.rs
Normal file
60
snowcap/src/runtime.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use smithay_client_toolkit::reexports::calloop;
|
||||||
|
|
||||||
|
pub struct CurrentTokioExecutor;
|
||||||
|
|
||||||
|
impl iced_futures::Executor for CurrentTokioExecutor {
|
||||||
|
fn new() -> Result<Self, futures::io::Error>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn(
|
||||||
|
&self,
|
||||||
|
future: impl futures::prelude::Future<Output = ()> + iced_futures::MaybeSend + 'static,
|
||||||
|
) {
|
||||||
|
tokio::runtime::Handle::current().spawn(future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CalloopSenderSink<T>(calloop::channel::Sender<T>);
|
||||||
|
|
||||||
|
impl<T> Clone for CalloopSenderSink<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CalloopSenderSink<T> {
|
||||||
|
pub fn new(sender: calloop::channel::Sender<T>) -> Self {
|
||||||
|
Self(sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> futures::Sink<T> for CalloopSenderSink<T> {
|
||||||
|
type Error = futures::channel::mpsc::SendError;
|
||||||
|
|
||||||
|
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_send(self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
|
||||||
|
let _ = self.0.send(item);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
92
snowcap/src/server.rs
Normal file
92
snowcap/src/server.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use smithay_client_toolkit::reexports::calloop;
|
||||||
|
use snowcap_api_defs::snowcap::{
|
||||||
|
input::v0alpha1::input_service_server::InputServiceServer,
|
||||||
|
layer::v0alpha1::layer_service_server::LayerServiceServer,
|
||||||
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{input::InputService, LayerService},
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn socket_dir() -> PathBuf {
|
||||||
|
xdg::BaseDirectories::with_prefix("snowcap")
|
||||||
|
.and_then(|xdg| xdg.get_runtime_directory().cloned())
|
||||||
|
.unwrap_or(PathBuf::from("/tmp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_name() -> String {
|
||||||
|
let wayland_suffix = std::env::var("WAYLAND_DISPLAY").unwrap_or("wayland-0".into());
|
||||||
|
format!("snowcap-grpc-{wayland_suffix}.sock")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GrpcServerState {
|
||||||
|
_join_handle: JoinHandle<()>,
|
||||||
|
socket_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for GrpcServerState {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_file(&self.socket_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn start_grpc_server(&mut self, socket_dir: impl AsRef<Path>) -> anyhow::Result<()> {
|
||||||
|
let socket_dir = socket_dir.as_ref();
|
||||||
|
std::fs::create_dir_all(socket_dir)?;
|
||||||
|
|
||||||
|
let socket_path = socket_dir.join(socket_name());
|
||||||
|
|
||||||
|
if let Ok(true) = socket_path.try_exists() {
|
||||||
|
std::fs::remove_file(&socket_path)
|
||||||
|
.context(format!("failed to remove old socket at {socket_path:?}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (grpc_sender, grpc_recv) =
|
||||||
|
calloop::channel::channel::<Box<dyn FnOnce(&mut State) + Send>>();
|
||||||
|
|
||||||
|
self.loop_handle
|
||||||
|
.insert_source(grpc_recv, |msg, _, state| match msg {
|
||||||
|
calloop::channel::Event::Msg(f) => f(state),
|
||||||
|
calloop::channel::Event::Closed => error!("grpc receiver was closed"),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// let snowcap_service = SnowcapService::new(grpc_sender.clone());
|
||||||
|
let layer_service = LayerService::new(grpc_sender.clone());
|
||||||
|
let input_service = InputService::new(grpc_sender.clone());
|
||||||
|
|
||||||
|
let refl_service = tonic_reflection::server::Builder::configure()
|
||||||
|
.register_encoded_file_descriptor_set(snowcap_api_defs::FILE_DESCRIPTOR_SET)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let uds = tokio::net::UnixListener::bind(&socket_path)?;
|
||||||
|
let uds_stream = tokio_stream::wrappers::UnixListenerStream::new(uds);
|
||||||
|
|
||||||
|
let grpc_server = tonic::transport::Server::builder()
|
||||||
|
.add_service(refl_service)
|
||||||
|
.add_service(LayerServiceServer::new(layer_service))
|
||||||
|
.add_service(InputServiceServer::new(input_service));
|
||||||
|
|
||||||
|
let join_handle = tokio::spawn(async move {
|
||||||
|
if let Err(err) = grpc_server.serve_with_incoming(uds_stream).await {
|
||||||
|
error!("gRPC server error: {err}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("Started gRPC server at {socket_path:?}");
|
||||||
|
|
||||||
|
self.grpc_server_state = Some(GrpcServerState {
|
||||||
|
_join_handle: join_handle,
|
||||||
|
socket_path,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
104
snowcap/src/state.rs
Normal file
104
snowcap/src/state.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
compositor::CompositorState,
|
||||||
|
output::OutputState,
|
||||||
|
reexports::{
|
||||||
|
calloop::{LoopHandle, LoopSignal},
|
||||||
|
calloop_wayland_source::WaylandSource,
|
||||||
|
client::{
|
||||||
|
globals::registry_queue_init,
|
||||||
|
protocol::{wl_keyboard::WlKeyboard, wl_pointer::WlPointer},
|
||||||
|
Connection, QueueHandle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registry::RegistryState,
|
||||||
|
seat::{keyboard::Modifiers, SeatState},
|
||||||
|
shell::wlr_layer::LayerShell,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
handlers::keyboard::KeyboardFocus,
|
||||||
|
layer::SnowcapLayer,
|
||||||
|
server::GrpcServerState,
|
||||||
|
wgpu::{setup_wgpu, Wgpu},
|
||||||
|
widget::WidgetIdCounter,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub loop_handle: LoopHandle<'static, State>,
|
||||||
|
pub loop_signal: LoopSignal,
|
||||||
|
pub conn: Connection,
|
||||||
|
|
||||||
|
pub registry_state: RegistryState,
|
||||||
|
pub seat_state: SeatState,
|
||||||
|
pub output_state: OutputState,
|
||||||
|
pub compositor_state: CompositorState,
|
||||||
|
pub layer_shell_state: LayerShell,
|
||||||
|
|
||||||
|
pub grpc_server_state: Option<GrpcServerState>,
|
||||||
|
|
||||||
|
pub queue_handle: QueueHandle<State>,
|
||||||
|
|
||||||
|
pub wgpu: Wgpu,
|
||||||
|
|
||||||
|
pub layers: Vec<SnowcapLayer>,
|
||||||
|
|
||||||
|
// TODO: per wl_keyboard
|
||||||
|
pub keyboard_focus: Option<KeyboardFocus>,
|
||||||
|
pub keyboard_modifiers: Modifiers,
|
||||||
|
pub keyboard: Option<WlKeyboard>, // TODO: multiple
|
||||||
|
|
||||||
|
pub pointer: Option<WlPointer>, // TODO: multiple
|
||||||
|
|
||||||
|
pub widget_id_counter: WidgetIdCounter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn new(
|
||||||
|
loop_handle: LoopHandle<'static, State>,
|
||||||
|
loop_signal: LoopSignal,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let conn =
|
||||||
|
Connection::connect_to_env().context("failed to establish wayland connection")?;
|
||||||
|
|
||||||
|
let (globals, event_queue) =
|
||||||
|
registry_queue_init::<State>(&conn).context("failed to init registry queue")?;
|
||||||
|
let queue_handle = event_queue.handle();
|
||||||
|
|
||||||
|
let layer_shell_state = LayerShell::bind(&globals, &queue_handle).unwrap();
|
||||||
|
|
||||||
|
let seat_state = SeatState::new(&globals, &queue_handle);
|
||||||
|
|
||||||
|
let registry_state = RegistryState::new(&globals);
|
||||||
|
|
||||||
|
let output_state = OutputState::new(&globals, &queue_handle);
|
||||||
|
|
||||||
|
let compositor_state = CompositorState::bind(&globals, &queue_handle).unwrap();
|
||||||
|
|
||||||
|
WaylandSource::new(conn.clone(), event_queue)
|
||||||
|
.insert(loop_handle.clone())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let state = State {
|
||||||
|
loop_handle,
|
||||||
|
loop_signal,
|
||||||
|
conn: conn.clone(),
|
||||||
|
registry_state,
|
||||||
|
seat_state,
|
||||||
|
output_state,
|
||||||
|
compositor_state,
|
||||||
|
layer_shell_state,
|
||||||
|
grpc_server_state: None,
|
||||||
|
queue_handle,
|
||||||
|
wgpu: setup_wgpu()?,
|
||||||
|
layers: Vec::new(),
|
||||||
|
keyboard_focus: None,
|
||||||
|
keyboard_modifiers: smithay_client_toolkit::seat::keyboard::Modifiers::default(),
|
||||||
|
keyboard: None,
|
||||||
|
pointer: None,
|
||||||
|
widget_id_counter: WidgetIdCounter::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
}
|
1
snowcap/src/util.rs
Normal file
1
snowcap/src/util.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod convert;
|
205
snowcap/src/util/convert.rs
Normal file
205
snowcap/src/util/convert.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
//! Utilities for converting to and from API types
|
||||||
|
|
||||||
|
use snowcap_api_defs::snowcap::widget;
|
||||||
|
|
||||||
|
pub trait FromApi {
|
||||||
|
type ApiType;
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::Length {
|
||||||
|
type ApiType = widget::v0alpha1::Length;
|
||||||
|
|
||||||
|
fn from_api(length: Self::ApiType) -> Self {
|
||||||
|
use widget::v0alpha1::length::Strategy;
|
||||||
|
match length.strategy.unwrap_or(Strategy::Fill(())) {
|
||||||
|
Strategy::Fill(_) => iced::Length::Fill,
|
||||||
|
Strategy::FillPortion(portion) => iced::Length::FillPortion(portion as u16),
|
||||||
|
Strategy::Shrink(_) => iced::Length::Shrink,
|
||||||
|
Strategy::Fixed(size) => iced::Length::Fixed(size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::Alignment {
|
||||||
|
type ApiType = widget::v0alpha1::Alignment;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
match api_type {
|
||||||
|
widget::v0alpha1::Alignment::Unspecified => iced::Alignment::Start,
|
||||||
|
widget::v0alpha1::Alignment::Start => iced::Alignment::Start,
|
||||||
|
widget::v0alpha1::Alignment::Center => iced::Alignment::Center,
|
||||||
|
widget::v0alpha1::Alignment::End => iced::Alignment::End,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::widget::scrollable::Alignment {
|
||||||
|
type ApiType = widget::v0alpha1::ScrollableAlignment;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
match api_type {
|
||||||
|
widget::v0alpha1::ScrollableAlignment::Unspecified => Self::default(),
|
||||||
|
widget::v0alpha1::ScrollableAlignment::Start => {
|
||||||
|
iced::widget::scrollable::Alignment::Start
|
||||||
|
}
|
||||||
|
widget::v0alpha1::ScrollableAlignment::End => iced::widget::scrollable::Alignment::End,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::widget::scrollable::Properties {
|
||||||
|
type ApiType = widget::v0alpha1::ScrollableProperties;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
let mut properties = iced::widget::scrollable::Properties::new();
|
||||||
|
let alignment = api_type.alignment();
|
||||||
|
properties = properties.alignment(iced::widget::scrollable::Alignment::from_api(alignment));
|
||||||
|
if let Some(width) = api_type.width {
|
||||||
|
properties = properties.width(width);
|
||||||
|
}
|
||||||
|
if let Some(margin) = api_type.margin {
|
||||||
|
properties = properties.margin(margin);
|
||||||
|
}
|
||||||
|
if let Some(scroller_width) = api_type.scroller_width {
|
||||||
|
properties = properties.scroller_width(scroller_width);
|
||||||
|
}
|
||||||
|
properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::widget::scrollable::Direction {
|
||||||
|
type ApiType = widget::v0alpha1::ScrollableDirection;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
use iced::widget::scrollable::Properties;
|
||||||
|
match (api_type.vertical, api_type.horizontal) {
|
||||||
|
(Some(vertical), Some(horizontal)) => Self::Both {
|
||||||
|
vertical: Properties::from_api(vertical),
|
||||||
|
horizontal: Properties::from_api(horizontal),
|
||||||
|
},
|
||||||
|
(Some(vertical), None) => Self::Vertical(Properties::from_api(vertical)),
|
||||||
|
(None, Some(horizontal)) => Self::Horizontal(Properties::from_api(horizontal)),
|
||||||
|
(None, None) => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::Padding {
|
||||||
|
type ApiType = widget::v0alpha1::Padding;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
iced::Padding {
|
||||||
|
top: api_type.top(),
|
||||||
|
right: api_type.right(),
|
||||||
|
bottom: api_type.bottom(),
|
||||||
|
left: api_type.left(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::Color {
|
||||||
|
type ApiType = widget::v0alpha1::Color;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
iced::Color {
|
||||||
|
r: api_type.red().clamp(0.0, 1.0),
|
||||||
|
g: api_type.green().clamp(0.0, 1.0),
|
||||||
|
b: api_type.blue().clamp(0.0, 1.0),
|
||||||
|
a: api_type.alpha.unwrap_or(1.0).clamp(0.0, 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::font::Family {
|
||||||
|
type ApiType = widget::v0alpha1::font::Family;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
match api_type.family {
|
||||||
|
Some(family) => match family {
|
||||||
|
widget::v0alpha1::font::family::Family::Name(name) => {
|
||||||
|
iced::font::Family::Name(name.leak()) // why does this take &'static str
|
||||||
|
}
|
||||||
|
widget::v0alpha1::font::family::Family::Serif(_) => iced::font::Family::Serif,
|
||||||
|
widget::v0alpha1::font::family::Family::SansSerif(_) => {
|
||||||
|
iced::font::Family::SansSerif
|
||||||
|
}
|
||||||
|
widget::v0alpha1::font::family::Family::Cursive(_) => iced::font::Family::Cursive,
|
||||||
|
widget::v0alpha1::font::family::Family::Fantasy(_) => iced::font::Family::Fantasy,
|
||||||
|
widget::v0alpha1::font::family::Family::Monospace(_) => {
|
||||||
|
iced::font::Family::Monospace
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::font::Weight {
|
||||||
|
type ApiType = widget::v0alpha1::font::Weight;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
match api_type {
|
||||||
|
widget::v0alpha1::font::Weight::Unspecified => Default::default(),
|
||||||
|
widget::v0alpha1::font::Weight::Thin => iced::font::Weight::Thin,
|
||||||
|
widget::v0alpha1::font::Weight::ExtraLight => iced::font::Weight::ExtraLight,
|
||||||
|
widget::v0alpha1::font::Weight::Light => iced::font::Weight::Light,
|
||||||
|
widget::v0alpha1::font::Weight::Normal => iced::font::Weight::Normal,
|
||||||
|
widget::v0alpha1::font::Weight::Medium => iced::font::Weight::Medium,
|
||||||
|
widget::v0alpha1::font::Weight::Semibold => iced::font::Weight::Semibold,
|
||||||
|
widget::v0alpha1::font::Weight::Bold => iced::font::Weight::Bold,
|
||||||
|
widget::v0alpha1::font::Weight::ExtraBold => iced::font::Weight::ExtraBold,
|
||||||
|
widget::v0alpha1::font::Weight::Black => iced::font::Weight::Black,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::font::Stretch {
|
||||||
|
type ApiType = widget::v0alpha1::font::Stretch;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
match api_type {
|
||||||
|
widget::v0alpha1::font::Stretch::Unspecified => Default::default(),
|
||||||
|
widget::v0alpha1::font::Stretch::UltraCondensed => iced::font::Stretch::UltraCondensed,
|
||||||
|
widget::v0alpha1::font::Stretch::ExtraCondensed => iced::font::Stretch::ExtraCondensed,
|
||||||
|
widget::v0alpha1::font::Stretch::Condensed => iced::font::Stretch::Condensed,
|
||||||
|
widget::v0alpha1::font::Stretch::SemiCondensed => iced::font::Stretch::SemiCondensed,
|
||||||
|
widget::v0alpha1::font::Stretch::Normal => iced::font::Stretch::Normal,
|
||||||
|
widget::v0alpha1::font::Stretch::SemiExpanded => iced::font::Stretch::SemiExpanded,
|
||||||
|
widget::v0alpha1::font::Stretch::Expanded => iced::font::Stretch::Expanded,
|
||||||
|
widget::v0alpha1::font::Stretch::ExtraExpanded => iced::font::Stretch::ExtraExpanded,
|
||||||
|
widget::v0alpha1::font::Stretch::UltraExpanded => iced::font::Stretch::UltraExpanded,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::font::Style {
|
||||||
|
type ApiType = widget::v0alpha1::font::Style;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
match api_type {
|
||||||
|
widget::v0alpha1::font::Style::Unspecified => Default::default(),
|
||||||
|
widget::v0alpha1::font::Style::Normal => iced::font::Style::Normal,
|
||||||
|
widget::v0alpha1::font::Style::Italic => iced::font::Style::Italic,
|
||||||
|
widget::v0alpha1::font::Style::Oblique => iced::font::Style::Oblique,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi for iced::Font {
|
||||||
|
type ApiType = widget::v0alpha1::Font;
|
||||||
|
|
||||||
|
fn from_api(api_type: Self::ApiType) -> Self {
|
||||||
|
let weight = FromApi::from_api(api_type.weight());
|
||||||
|
let stretch = FromApi::from_api(api_type.stretch());
|
||||||
|
let style = FromApi::from_api(api_type.style());
|
||||||
|
let family = api_type.family.map(FromApi::from_api).unwrap_or_default();
|
||||||
|
|
||||||
|
iced::Font {
|
||||||
|
family,
|
||||||
|
weight,
|
||||||
|
stretch,
|
||||||
|
style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
snowcap/src/wgpu.rs
Normal file
72
snowcap/src/wgpu.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use iced_wgpu::graphics::backend::Text;
|
||||||
|
use iced_wgpu::{
|
||||||
|
wgpu::{self, Backends},
|
||||||
|
Backend,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::block_on_tokio;
|
||||||
|
|
||||||
|
const UBUNTU_REGULAR: &[u8] = include_bytes!("../resources/fonts/Ubuntu-Regular.ttf");
|
||||||
|
const UBUNTU_BOLD: &[u8] = include_bytes!("../resources/fonts/Ubuntu-Bold.ttf");
|
||||||
|
const UBUNTU_ITALIC: &[u8] = include_bytes!("../resources/fonts/Ubuntu-Italic.ttf");
|
||||||
|
const UBUNTU_BOLD_ITALIC: &[u8] = include_bytes!("../resources/fonts/Ubuntu-BoldItalic.ttf");
|
||||||
|
|
||||||
|
pub struct Wgpu {
|
||||||
|
pub instance: Arc<wgpu::Instance>,
|
||||||
|
pub adapter: Arc<wgpu::Adapter>,
|
||||||
|
pub device: Arc<wgpu::Device>,
|
||||||
|
pub queue: Arc<wgpu::Queue>,
|
||||||
|
pub renderer: iced_wgpu::Renderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_wgpu() -> anyhow::Result<Wgpu> {
|
||||||
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::VULKAN,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let adapter = block_on_tokio(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::default(),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
compatible_surface: None,
|
||||||
|
}))
|
||||||
|
.context("no adapter")?;
|
||||||
|
|
||||||
|
let (device, queue) = block_on_tokio(adapter.request_device(
|
||||||
|
&wgpu::DeviceDescriptor {
|
||||||
|
label: None,
|
||||||
|
required_features: wgpu::Features::empty(), // TODO:
|
||||||
|
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let mut backend = Backend::new(
|
||||||
|
&device,
|
||||||
|
&queue,
|
||||||
|
iced_wgpu::Settings {
|
||||||
|
present_mode: wgpu::PresentMode::Mailbox,
|
||||||
|
internal_backend: Backends::VULKAN,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
);
|
||||||
|
|
||||||
|
backend.load_font(UBUNTU_REGULAR.into());
|
||||||
|
backend.load_font(UBUNTU_BOLD.into());
|
||||||
|
backend.load_font(UBUNTU_ITALIC.into());
|
||||||
|
backend.load_font(UBUNTU_BOLD_ITALIC.into());
|
||||||
|
|
||||||
|
let renderer = iced_wgpu::Renderer::new(backend, Default::default(), iced::Pixels(16.0));
|
||||||
|
|
||||||
|
Ok(Wgpu {
|
||||||
|
instance: Arc::new(instance),
|
||||||
|
adapter: Arc::new(adapter),
|
||||||
|
device: Arc::new(device),
|
||||||
|
queue: Arc::new(queue),
|
||||||
|
renderer,
|
||||||
|
})
|
||||||
|
}
|
454
snowcap/src/widget.rs
Normal file
454
snowcap/src/widget.rs
Normal file
|
@ -0,0 +1,454 @@
|
||||||
|
use std::{any::Any, collections::HashMap};
|
||||||
|
|
||||||
|
use iced::{
|
||||||
|
widget::{Column, Container, Row, Scrollable},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
use iced_runtime::Program;
|
||||||
|
use iced_wgpu::core::Element;
|
||||||
|
use snowcap_api_defs::snowcap::widget::{
|
||||||
|
self,
|
||||||
|
v0alpha1::{widget_def, WidgetDef},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{layer::SnowcapLayer, state::State, util::convert::FromApi};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct WidgetId(u32);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct WidgetIdCounter(WidgetId);
|
||||||
|
|
||||||
|
impl WidgetIdCounter {
|
||||||
|
pub fn next_and_increment(&mut self) -> WidgetId {
|
||||||
|
let ret = self.0;
|
||||||
|
self.0 .0 += 1;
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetId {
|
||||||
|
pub fn into_inner(self) -> u32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layer_for_mut<'a>(&self, state: &'a mut State) -> Option<&'a mut SnowcapLayer> {
|
||||||
|
state
|
||||||
|
.layers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|sn_layer| &sn_layer.widget_id == self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for WidgetId {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SnowcapWidgetProgram {
|
||||||
|
pub widgets: WidgetFn,
|
||||||
|
pub widget_state: HashMap<u32, Box<dyn Any + Send>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type WidgetFn = Box<
|
||||||
|
dyn for<'a> Fn(
|
||||||
|
&'a HashMap<u32, Box<dyn Any + Send>>,
|
||||||
|
) -> Element<'a, SnowcapMessage, iced::Theme, iced_wgpu::Renderer>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SnowcapMessage {
|
||||||
|
Noop,
|
||||||
|
Close,
|
||||||
|
Update(u32, Box<dyn Any + Send>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Program for SnowcapWidgetProgram {
|
||||||
|
type Renderer = iced_wgpu::Renderer;
|
||||||
|
|
||||||
|
type Theme = iced::Theme;
|
||||||
|
|
||||||
|
type Message = SnowcapMessage;
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||||
|
match message {
|
||||||
|
SnowcapMessage::Noop => (),
|
||||||
|
SnowcapMessage::Close => (),
|
||||||
|
SnowcapMessage::Update(id, data) => {
|
||||||
|
self.widget_state.insert(id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> {
|
||||||
|
(self.widgets)(&self.widget_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget_def_to_fn(def: WidgetDef) -> Option<(WidgetFn, HashMap<u32, Box<dyn Any + Send>>)> {
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
let mut current_id = 0;
|
||||||
|
|
||||||
|
let f = widget_def_to_fn_inner(def, &mut current_id, &mut states);
|
||||||
|
|
||||||
|
f.map(|f| (f, states))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn widget_def_to_fn_inner(
|
||||||
|
def: WidgetDef,
|
||||||
|
current_id: &mut u32,
|
||||||
|
_states: &mut HashMap<u32, Box<dyn Any + Send>>,
|
||||||
|
) -> Option<WidgetFn> {
|
||||||
|
let def = def.widget?;
|
||||||
|
match def {
|
||||||
|
widget_def::Widget::Text(text_def) => {
|
||||||
|
let horizontal_alignment = text_def.horizontal_alignment();
|
||||||
|
let vertical_alignment = text_def.vertical_alignment();
|
||||||
|
|
||||||
|
let widget::v0alpha1::Text {
|
||||||
|
text,
|
||||||
|
pixels,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
horizontal_alignment: _,
|
||||||
|
vertical_alignment: _,
|
||||||
|
color,
|
||||||
|
font,
|
||||||
|
} = text_def;
|
||||||
|
|
||||||
|
let f: WidgetFn = Box::new(move |_states| {
|
||||||
|
let mut text = iced::widget::Text::new(text.clone().unwrap_or_default());
|
||||||
|
if let Some(pixels) = pixels {
|
||||||
|
text = text.size(pixels);
|
||||||
|
}
|
||||||
|
if let Some(width) = width.clone() {
|
||||||
|
text = text.width(iced::Length::from_api(width));
|
||||||
|
}
|
||||||
|
if let Some(height) = height.clone() {
|
||||||
|
text = text.height(iced::Length::from_api(height));
|
||||||
|
}
|
||||||
|
if let Some(color) = color.clone() {
|
||||||
|
text = text.style(iced::theme::Text::Color(iced::Color::from_api(color)));
|
||||||
|
}
|
||||||
|
|
||||||
|
match horizontal_alignment {
|
||||||
|
widget::v0alpha1::Alignment::Unspecified => (),
|
||||||
|
widget::v0alpha1::Alignment::Start => {
|
||||||
|
text = text.horizontal_alignment(iced::alignment::Horizontal::Left)
|
||||||
|
}
|
||||||
|
widget::v0alpha1::Alignment::Center => {
|
||||||
|
text = text.horizontal_alignment(iced::alignment::Horizontal::Center)
|
||||||
|
}
|
||||||
|
widget::v0alpha1::Alignment::End => {
|
||||||
|
text = text.horizontal_alignment(iced::alignment::Horizontal::Right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match vertical_alignment {
|
||||||
|
widget::v0alpha1::Alignment::Unspecified => (),
|
||||||
|
widget::v0alpha1::Alignment::Start => {
|
||||||
|
text = text.vertical_alignment(iced::alignment::Vertical::Top)
|
||||||
|
}
|
||||||
|
widget::v0alpha1::Alignment::Center => {
|
||||||
|
text = text.vertical_alignment(iced::alignment::Vertical::Center)
|
||||||
|
}
|
||||||
|
widget::v0alpha1::Alignment::End => {
|
||||||
|
text = text.vertical_alignment(iced::alignment::Vertical::Bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(font) = font.clone() {
|
||||||
|
text = text.font(iced::Font::from_api(font));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.into()
|
||||||
|
});
|
||||||
|
Some(f)
|
||||||
|
}
|
||||||
|
widget_def::Widget::Column(widget::v0alpha1::Column {
|
||||||
|
spacing,
|
||||||
|
padding,
|
||||||
|
item_alignment,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
max_width,
|
||||||
|
clip,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
let children_widget_fns = children
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|def| {
|
||||||
|
*current_id += 1;
|
||||||
|
widget_def_to_fn_inner(def, current_id, _states)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let f: WidgetFn = Box::new(move |states| {
|
||||||
|
let mut column = Column::new();
|
||||||
|
|
||||||
|
if let Some(spacing) = spacing {
|
||||||
|
column = column.spacing(spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = width.clone() {
|
||||||
|
column = column.width(iced::Length::from_api(width));
|
||||||
|
}
|
||||||
|
if let Some(height) = height.clone() {
|
||||||
|
column = column.height(iced::Length::from_api(height));
|
||||||
|
}
|
||||||
|
if let Some(max_width) = max_width {
|
||||||
|
column = column.max_width(max_width);
|
||||||
|
}
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
column = column.clip(clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(padding) = padding.clone() {
|
||||||
|
column = column.padding(iced::Padding::from_api(padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(alignment) = item_alignment {
|
||||||
|
column = column.align_items(match alignment {
|
||||||
|
// FIXME: actual conversion logic
|
||||||
|
1 => iced::Alignment::Start,
|
||||||
|
2 => iced::Alignment::Center,
|
||||||
|
3 => iced::Alignment::End,
|
||||||
|
_ => iced::Alignment::Start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in children_widget_fns.iter() {
|
||||||
|
column = column.push(child(states));
|
||||||
|
}
|
||||||
|
|
||||||
|
column.into()
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(f)
|
||||||
|
}
|
||||||
|
widget_def::Widget::Row(widget::v0alpha1::Row {
|
||||||
|
spacing,
|
||||||
|
padding,
|
||||||
|
item_alignment,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
clip,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
let children_widget_fns = children
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|def| {
|
||||||
|
*current_id += 1;
|
||||||
|
widget_def_to_fn_inner(def, current_id, _states)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let f: WidgetFn = Box::new(move |states| {
|
||||||
|
let mut row = Row::new();
|
||||||
|
|
||||||
|
if let Some(spacing) = spacing {
|
||||||
|
row = row.spacing(spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = width.clone() {
|
||||||
|
row = row.width(iced::Length::from_api(width));
|
||||||
|
}
|
||||||
|
if let Some(height) = height.clone() {
|
||||||
|
row = row.height(iced::Length::from_api(height));
|
||||||
|
}
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
row = row.clip(clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(widget::v0alpha1::Padding {
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
|
}) = padding
|
||||||
|
{
|
||||||
|
row = row.padding([
|
||||||
|
top.unwrap_or_default(),
|
||||||
|
right.unwrap_or_default(),
|
||||||
|
bottom.unwrap_or_default(),
|
||||||
|
left.unwrap_or_default(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(alignment) = item_alignment {
|
||||||
|
row = row.align_items(match alignment {
|
||||||
|
// FIXME: actual conversion logic
|
||||||
|
1 => iced::Alignment::Start,
|
||||||
|
2 => iced::Alignment::Center,
|
||||||
|
3 => iced::Alignment::End,
|
||||||
|
_ => iced::Alignment::Start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in children_widget_fns.iter() {
|
||||||
|
row = row.push(child(states));
|
||||||
|
}
|
||||||
|
|
||||||
|
row.into()
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(f)
|
||||||
|
}
|
||||||
|
widget_def::Widget::Scrollable(scrollable_def) => {
|
||||||
|
let widget::v0alpha1::Scrollable {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
direction,
|
||||||
|
child,
|
||||||
|
} = *scrollable_def;
|
||||||
|
|
||||||
|
let child_widget_fn = child.and_then(|def| {
|
||||||
|
*current_id += 1;
|
||||||
|
widget_def_to_fn_inner(*def, current_id, _states)
|
||||||
|
});
|
||||||
|
|
||||||
|
let f: WidgetFn = Box::new(move |states| {
|
||||||
|
let mut scrollable = Scrollable::new(
|
||||||
|
child_widget_fn
|
||||||
|
.as_ref()
|
||||||
|
.map(|child| child(states))
|
||||||
|
.unwrap_or_else(|| iced::widget::Text::new("NULL").into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(width) = width.clone() {
|
||||||
|
scrollable = scrollable.width(iced::Length::from_api(width));
|
||||||
|
}
|
||||||
|
if let Some(height) = height.clone() {
|
||||||
|
scrollable = scrollable.height(iced::Length::from_api(height));
|
||||||
|
}
|
||||||
|
if let Some(direction) = direction.clone() {
|
||||||
|
scrollable = scrollable
|
||||||
|
.direction(iced::widget::scrollable::Direction::from_api(direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollable.into()
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(f)
|
||||||
|
}
|
||||||
|
widget_def::Widget::Container(container_def) => {
|
||||||
|
let horizontal_alignment = container_def.horizontal_alignment();
|
||||||
|
let vertical_alignment = container_def.vertical_alignment();
|
||||||
|
|
||||||
|
let widget::v0alpha1::Container {
|
||||||
|
padding,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
max_width,
|
||||||
|
max_height,
|
||||||
|
horizontal_alignment: _,
|
||||||
|
vertical_alignment: _,
|
||||||
|
clip,
|
||||||
|
child,
|
||||||
|
|
||||||
|
text_color,
|
||||||
|
background_color,
|
||||||
|
border_radius,
|
||||||
|
border_thickness,
|
||||||
|
border_color,
|
||||||
|
} = *container_def;
|
||||||
|
|
||||||
|
let child_widget_fn = child.and_then(|def| {
|
||||||
|
*current_id += 1;
|
||||||
|
widget_def_to_fn_inner(*def, current_id, _states)
|
||||||
|
});
|
||||||
|
|
||||||
|
let f: WidgetFn = Box::new(move |states| {
|
||||||
|
let mut container = Container::new(
|
||||||
|
child_widget_fn
|
||||||
|
.as_ref()
|
||||||
|
.map(|child| child(states))
|
||||||
|
.unwrap_or_else(|| iced::widget::Text::new("NULL").into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(width) = width.clone() {
|
||||||
|
container = container.width(iced::Length::from_api(width));
|
||||||
|
}
|
||||||
|
if let Some(height) = height.clone() {
|
||||||
|
container = container.height(iced::Length::from_api(height));
|
||||||
|
}
|
||||||
|
if let Some(max_width) = max_width {
|
||||||
|
container = container.max_width(max_width);
|
||||||
|
}
|
||||||
|
if let Some(max_height) = max_height {
|
||||||
|
container = container.max_height(max_height);
|
||||||
|
}
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
container = container.clip(clip);
|
||||||
|
}
|
||||||
|
if let Some(padding) = padding.clone() {
|
||||||
|
container = container.padding(iced::Padding::from_api(padding));
|
||||||
|
}
|
||||||
|
container = container.align_x(match horizontal_alignment {
|
||||||
|
widget::v0alpha1::Alignment::Unspecified => iced::alignment::Horizontal::Left,
|
||||||
|
widget::v0alpha1::Alignment::Start => iced::alignment::Horizontal::Left,
|
||||||
|
widget::v0alpha1::Alignment::Center => iced::alignment::Horizontal::Center,
|
||||||
|
widget::v0alpha1::Alignment::End => iced::alignment::Horizontal::Right,
|
||||||
|
});
|
||||||
|
container = container.align_y(match vertical_alignment {
|
||||||
|
widget::v0alpha1::Alignment::Unspecified => iced::alignment::Vertical::Top,
|
||||||
|
widget::v0alpha1::Alignment::Start => iced::alignment::Vertical::Top,
|
||||||
|
widget::v0alpha1::Alignment::Center => iced::alignment::Vertical::Center,
|
||||||
|
widget::v0alpha1::Alignment::End => iced::alignment::Vertical::Bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
let text_color_clone = text_color.clone();
|
||||||
|
let background_color_clone = background_color.clone();
|
||||||
|
let border_color_clone = border_color.clone();
|
||||||
|
|
||||||
|
let style = move |theme: &iced::Theme| {
|
||||||
|
use iced::widget::container::Appearance;
|
||||||
|
|
||||||
|
let palette = theme.extended_palette();
|
||||||
|
|
||||||
|
let mut appearance = Appearance {
|
||||||
|
text_color: None,
|
||||||
|
background: Some(palette.background.weak.color.into()),
|
||||||
|
border: iced::Border {
|
||||||
|
color: palette.background.base.color,
|
||||||
|
width: 0.0,
|
||||||
|
radius: 2.0.into(),
|
||||||
|
},
|
||||||
|
shadow: iced::Shadow::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(text_color) = text_color_clone.clone() {
|
||||||
|
appearance.text_color = Some(iced::Color::from_api(text_color));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(background_color) = background_color_clone.clone() {
|
||||||
|
appearance.background =
|
||||||
|
Some(iced::Color::from_api(background_color).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(border_color) = border_color_clone.clone() {
|
||||||
|
appearance.border.color = iced::Color::from_api(border_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(border_radius) = border_radius {
|
||||||
|
appearance.border.radius = border_radius.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(border_thickness) = border_thickness {
|
||||||
|
appearance.border.width = border_thickness;
|
||||||
|
}
|
||||||
|
|
||||||
|
appearance
|
||||||
|
};
|
||||||
|
|
||||||
|
container = container.style(iced::theme::Container::Custom(Box::new(style)));
|
||||||
|
|
||||||
|
container.into()
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue