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