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:
Ottatop 2024-12-15 21:41:45 -06:00
parent 975da0d14e
commit 83f968c3c9
62 changed files with 15182 additions and 4 deletions

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "snowcap"]
path = snowcap
url = https://github.com/pinnacle-comp/snowcap

@ -1 +0,0 @@
Subproject commit 3dc265976aa1e715db483e1c69d1c84ce897e4a0

4188
snowcap/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

51
snowcap/Cargo.toml Normal file
View 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
View 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
View 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.

View 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,
}

View file

@ -0,0 +1,2 @@
indent_type = "Spaces"
column_width = 100

View 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"

View 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();
}

View 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)
);
}

View 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",
},
}

View 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

View 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

View 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,
}

View 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

View 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

File diff suppressed because it is too large Load diff

View 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

View 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

View 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

View 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

View 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 {}

View 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);
}

View 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);
}

View file

@ -0,0 +1,5 @@
syntax = "proto2";
package snowcap.v0alpha1;
message Nothing {}

View 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;
}

View 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"

View 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();
// }
// })
//
}

View 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_(),
}
}
}

View 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);
}
});
}
}

View 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)
})
}

View file

@ -0,0 +1,4 @@
//! TODO:
/// TODO:
pub struct Snowcap {}

View 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),
}
}
}

View 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
View 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"

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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 }

View 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();
}

View 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
View 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
View 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
View 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
View 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);

View 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),
}

View 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
View file

@ -0,0 +1 @@
pub mod keyboard;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
pub mod convert;

205
snowcap/src/util/convert.rs Normal file
View 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
View 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
View 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)
}
}
}