rename to cursor-mover-webapp
This commit is contained in:
parent
201f0e5f0a
commit
cc52e3e781
35 changed files with 7 additions and 7 deletions
7
crates/cursor-mover-webapp/.gitignore
vendored
Normal file
7
crates/cursor-mover-webapp/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target
|
||||
.DS_Store
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
265
crates/cursor-mover-webapp/AGENTS.md
Normal file
265
crates/cursor-mover-webapp/AGENTS.md
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
You are an expert [0.7 Dioxus](https://dioxuslabs.com/learn/0.7) assistant. Dioxus 0.7 changes every api in dioxus. Only use this up to date documentation. `cx`, `Scope`, and `use_state` are gone
|
||||
|
||||
Provide concise code examples with detailed descriptions
|
||||
|
||||
# Dioxus Dependency
|
||||
|
||||
You can add Dioxus to your `Cargo.toml` like this:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dioxus = { version = "0.7.1" }
|
||||
|
||||
[features]
|
||||
default = ["web", "webview", "server"]
|
||||
web = ["dioxus/web"]
|
||||
webview = ["dioxus/desktop"]
|
||||
server = ["dioxus/server"]
|
||||
```
|
||||
|
||||
# Launching your application
|
||||
|
||||
You need to create a main function that sets up the Dioxus runtime and mounts your root component.
|
||||
|
||||
```rust
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(App);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
rsx! { "Hello, Dioxus!" }
|
||||
}
|
||||
```
|
||||
|
||||
Then serve with `dx serve`:
|
||||
|
||||
```sh
|
||||
curl -sSL http://dioxus.dev/install.sh | sh
|
||||
dx serve
|
||||
```
|
||||
|
||||
# UI with RSX
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
div {
|
||||
class: "container", // Attribute
|
||||
color: "red", // Inline styles
|
||||
width: if condition { "100%" }, // Conditional attributes
|
||||
"Hello, Dioxus!"
|
||||
}
|
||||
// Prefer loops over iterators
|
||||
for i in 0..5 {
|
||||
div { "{i}" } // use elements or components directly in loops
|
||||
}
|
||||
if condition {
|
||||
div { "Condition is true!" } // use elements or components directly in conditionals
|
||||
}
|
||||
|
||||
{children} // Expressions are wrapped in brace
|
||||
{(0..5).map(|i| rsx! { span { "Item {i}" } })} // Iterators must be wrapped in braces
|
||||
}
|
||||
```
|
||||
|
||||
# Assets
|
||||
|
||||
The asset macro can be used to link to local files to use in your project. All links start with `/` and are relative to the root of your project.
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
img {
|
||||
src: asset!("/assets/image.png"),
|
||||
alt: "An image",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Styles
|
||||
|
||||
The `document::Stylesheet` component will inject the stylesheet into the `<head>` of the document
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
document::Stylesheet {
|
||||
href: asset!("/assets/styles.css"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Components
|
||||
|
||||
Components are the building blocks of apps
|
||||
|
||||
* Component are functions annotated with the `#[component]` macro.
|
||||
* The function name must start with a capital letter or contain an underscore.
|
||||
* A component re-renders only under two conditions:
|
||||
1. Its props change (as determined by `PartialEq`).
|
||||
2. An internal reactive state it depends on is updated.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn Input(mut value: Signal<String>) -> Element {
|
||||
rsx! {
|
||||
input {
|
||||
value,
|
||||
oninput: move |e| {
|
||||
*value.write() = e.value();
|
||||
},
|
||||
onkeydown: move |e| {
|
||||
if e.key() == Key::Enter {
|
||||
value.write().clear();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each component accepts function arguments (props)
|
||||
|
||||
* Props must be owned values, not references. Use `String` and `Vec<T>` instead of `&str` or `&[T]`.
|
||||
* Props must implement `PartialEq` and `Clone`.
|
||||
* To make props reactive and copy, you can wrap the type in `ReadOnlySignal`. Any reactive state like memos and resources that read `ReadOnlySignal` props will automatically re-run when the prop changes.
|
||||
|
||||
# State
|
||||
|
||||
A signal is a wrapper around a value that automatically tracks where it's read and written. Changing a signal's value causes code that relies on the signal to rerun.
|
||||
|
||||
## Local State
|
||||
|
||||
The `use_signal` hook creates state that is local to a single component. You can call the signal like a function (e.g. `my_signal()`) to clone the value, or use `.read()` to get a reference. `.write()` gets a mutable reference to the value.
|
||||
|
||||
Use `use_memo` to create a memoized value that recalculates when its dependencies change. Memos are useful for expensive calculations that you don't want to repeat unnecessarily.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn Counter() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut doubled = use_memo(move || count() * 2); // doubled will re-run when count changes because it reads the signal
|
||||
|
||||
rsx! {
|
||||
h1 { "Count: {count}" } // Counter will re-render when count changes because it reads the signal
|
||||
h2 { "Doubled: {doubled}" }
|
||||
button {
|
||||
onclick: move |_| *count.write() += 1, // Writing to the signal rerenders Counter
|
||||
"Increment"
|
||||
}
|
||||
button {
|
||||
onclick: move |_| count.with_mut(|count| *count += 1), // use with_mut to mutate the signal
|
||||
"Increment with with_mut"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Context API
|
||||
|
||||
The Context API allows you to share state down the component tree. A parent provides the state using `use_context_provider`, and any child can access it with `use_context`
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let mut theme = use_signal(|| "light".to_string());
|
||||
use_context_provider(|| theme); // Provide a type to children
|
||||
rsx! { Child {} }
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Child() -> Element {
|
||||
let theme = use_context::<Signal<String>>(); // Consume the same type
|
||||
rsx! {
|
||||
div {
|
||||
"Current theme: {theme}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Async
|
||||
|
||||
For state that depends on an asynchronous operation (like a network request), Dioxus provides a hook called `use_resource`. This hook manages the lifecycle of the async task and provides the result to your component.
|
||||
|
||||
* The `use_resource` hook takes an `async` closure. It re-runs this closure whenever any signals it depends on (reads) are updated
|
||||
* The `Resource` object returned can be in several states when read:
|
||||
1. `None` if the resource is still loading
|
||||
2. `Some(value)` if the resource has successfully loaded
|
||||
|
||||
```rust
|
||||
let mut dog = use_resource(move || async move {
|
||||
// api request
|
||||
});
|
||||
|
||||
match dog() {
|
||||
Some(dog_info) => rsx! { Dog { dog_info } },
|
||||
None => rsx! { "Loading..." },
|
||||
}
|
||||
```
|
||||
|
||||
# Routing
|
||||
|
||||
All possible routes are defined in a single Rust `enum` that derives `Routable`. Each variant represents a route and is annotated with `#[route("/path")]`. Dynamic Segments can capture parts of the URL path as parameters by using `:name` in the route string. These become fields in the enum variant.
|
||||
|
||||
The `Router<Route> {}` component is the entry point that manages rendering the correct component for the current URL.
|
||||
|
||||
You can use the `#[layout(NavBar)]` to create a layout shared between pages and place an `Outlet<Route> {}` inside your layout component. The child routes will be rendered in the outlet.
|
||||
|
||||
```rust
|
||||
#[derive(Routable, Clone, PartialEq)]
|
||||
enum Route {
|
||||
#[layout(NavBar)] // This will use NavBar as the layout for all routes
|
||||
#[route("/")]
|
||||
Home {},
|
||||
#[route("/blog/:id")] // Dynamic segment
|
||||
BlogPost { id: i32 },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NavBar() -> Element {
|
||||
rsx! {
|
||||
a { href: "/", "Home" }
|
||||
Outlet<Route> {} // Renders Home or BlogPost
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
rsx! { Router::<Route> {} }
|
||||
}
|
||||
```
|
||||
|
||||
```toml
|
||||
dioxus = { version = "0.7.1", features = ["router"] }
|
||||
```
|
||||
|
||||
# Fullstack
|
||||
|
||||
Fullstack enables server rendering and ipc calls. It uses Cargo features (`server` and a client feature like `web`) to split the code into a server and client binaries.
|
||||
|
||||
```toml
|
||||
dioxus = { version = "0.7.1", features = ["fullstack"] }
|
||||
```
|
||||
|
||||
## Server Functions
|
||||
|
||||
Use the `#[post]` / `#[get]` macros to define an `async` function that will only run on the server. On the server, this macro generates an API endpoint. On the client, it generates a function that makes an HTTP request to that endpoint.
|
||||
|
||||
```rust
|
||||
#[post("/api/double/:path/&query")]
|
||||
async fn double_server(number: i32, path: String, query: i32) -> Result<i32, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Ok(number * 2)
|
||||
}
|
||||
```
|
||||
|
||||
## Hydration
|
||||
|
||||
Hydration is the process of making a server-rendered HTML page interactive on the client. The server sends the initial HTML, and then the client-side runs, attaches event listeners, and takes control of future rendering.
|
||||
|
||||
### Errors
|
||||
The initial UI rendered by the component on the client must be identical to the UI rendered on the server.
|
||||
|
||||
* Use the `use_server_future` hook instead of `use_resource`. It runs the future on the server, serializes the result, and sends it to the client, ensuring the client has the data immediately for its first render.
|
||||
* Any code that relies on browser-specific APIs (like accessing `localStorage`) must be run *after* hydration. Place this code inside a `use_effect` hook.
|
||||
49
crates/cursor-mover-webapp/Cargo.toml
Normal file
49
crates/cursor-mover-webapp/Cargo.toml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
[package]
|
||||
name = "cursor-mover-webapp"
|
||||
version = "0.1.0"
|
||||
authors = ["Mona Mayrhofer <mona.mayrhofer@proton.me>"]
|
||||
edition = "2024"
|
||||
repository = "https://github.com/mona-mayrhofer/cursor-mover-app"
|
||||
categories = ["tools"]
|
||||
keywords = ["tools", "cursor", "remote"]
|
||||
license = "EUPL-1.2"
|
||||
description = "A web application for controlling your cursor and keyboard via a smartphone browser."
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { version = "0.7.3", features = ["router", "fullstack", "logger"] }
|
||||
dioxus-html = { version = "0.7.3", features = ["serialize"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
wasmtimer = "0.4.3"
|
||||
|
||||
|
||||
wayland-client = { version = "0.31.12", optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.10", features = ["client"], optional = true }
|
||||
tokio = {version = "1.49.0", optional = true}
|
||||
rustix = { version = "1.1.4", optional = true, features = ["time"], default-features = false }
|
||||
wayland-protocols-misc = { version = "0.3.10", features = ["client"], optional = true }
|
||||
wayland-protocols = { version = "0.32.10", features = ["client", "staging"], optional = true }
|
||||
memfile = {version = "0.3.2", optional = true}
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
# The feature that are only required for the web = ["dioxus/web"] build target should be optional and only enabled in the web = ["dioxus/web"] feature
|
||||
web = ["dioxus/web"]
|
||||
# The feature that are only required for the desktop = ["dioxus/desktop"] build target should be optional and only enabled in the desktop = ["dioxus/desktop"] feature
|
||||
#desktop = ["dioxus/desktop"]
|
||||
# The feature that are only required for the mobile = ["dioxus/mobile"] build target should be optional and only enabled in the mobile = ["dioxus/mobile"] feature
|
||||
#mobile = ["dioxus/mobile"]
|
||||
# The feature that are only required for the server = ["dioxus/server"] build target should be optional and only enabled in the server = ["dioxus/server"] feature
|
||||
server = ["dioxus/server",
|
||||
"dep:wayland-client",
|
||||
"dep:wayland-protocols-wlr",
|
||||
"dep:wayland-protocols-misc",
|
||||
"dep:wayland-protocols",
|
||||
"dep:tokio",
|
||||
"dep:rustix",
|
||||
"dep:memfile"
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
27
crates/cursor-mover-webapp/Dioxus.toml
Normal file
27
crates/cursor-mover-webapp/Dioxus.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[application]
|
||||
name = "cursor-move-webapp"
|
||||
default_platform = "web"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "cursor-move-webapp"
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# Additional CSS style files
|
||||
style = []
|
||||
|
||||
# Additional JavaScript files
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
|
||||
[web.watcher]
|
||||
reload_html = true
|
||||
watch_path = ["src", "public"]
|
||||
34
crates/cursor-mover-webapp/README.md
Normal file
34
crates/cursor-mover-webapp/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Development
|
||||
|
||||
Your new jumpstart project includes basic organization with an organized `assets` folder and a `components` folder.
|
||||
If you chose to develop with the router feature, you will also have a `views` folder.
|
||||
|
||||
```
|
||||
project/
|
||||
├─ assets/ # Any assets that are used by the app should be placed here
|
||||
├─ src/
|
||||
│ ├─ main.rs # The entrypoint for the app. It also defines the routes for the app.
|
||||
│ ├─ components/
|
||||
│ │ ├─ mod.rs # Defines the components module
|
||||
│ │ ├─ hero.rs # The Hero component for use in the home page
|
||||
│ │ ├─ echo.rs # The echo component uses server functions to communicate with the server
|
||||
│ ├─ views/ # The views each route will render in the app.
|
||||
│ │ ├─ mod.rs # Defines the module for the views route and re-exports the components for each route
|
||||
│ │ ├─ blog.rs # The component that will render at the /blog/:id route
|
||||
│ │ ├─ home.rs # The component that will render at the / route
|
||||
├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project
|
||||
```
|
||||
|
||||
### Serving Your App
|
||||
|
||||
Run the following command in the root of your project to start developing with the default platform:
|
||||
|
||||
```bash
|
||||
dx serve --platform web
|
||||
```
|
||||
|
||||
To run for a different platform, use the `--platform platform` flag. E.g.
|
||||
```bash
|
||||
dx serve --platform desktop
|
||||
```
|
||||
|
||||
BIN
crates/cursor-mover-webapp/assets/favicon.ico
Normal file
BIN
crates/cursor-mover-webapp/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
.controls {
|
||||
flex-grow: 1;
|
||||
}
|
||||
13
crates/cursor-mover-webapp/assets/styling/home.module.css
Normal file
13
crates/cursor-mover-webapp/assets/styling/home.module.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.keyboard-area {
|
||||
height: 48px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
15
crates/cursor-mover-webapp/assets/styling/main.css
Normal file
15
crates/cursor-mover-webapp/assets/styling/main.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
html,
|
||||
body,
|
||||
#main {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100dvh;
|
||||
width: 100dvw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#main {
|
||||
background-color: #214;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.mouse-area {
|
||||
background: radial-gradient(#0004 15%, transparent 20%), white;
|
||||
background-size: 15px 15px;
|
||||
|
||||
touch-action: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
16
crates/cursor-mover-webapp/assets/styling/navbar.module.css
Normal file
16
crates/cursor-mover-webapp/assets/styling/navbar.module.css
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
.navbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
color: #ffffff;
|
||||
margin-right: 20px;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.navbar a:hover {
|
||||
cursor: pointer;
|
||||
color: #91a4d2;
|
||||
}
|
||||
8
crates/cursor-mover-webapp/clippy.toml
Normal file
8
crates/cursor-mover-webapp/clippy.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
await-holding-invalid-types = [
|
||||
"generational_box::GenerationalRef",
|
||||
{ path = "generational_box::GenerationalRef", reason = "Reads should not be held over an await point. This will cause any writes to fail while the await is pending since the read borrow is still active." },
|
||||
"generational_box::GenerationalRefMut",
|
||||
{ path = "generational_box::GenerationalRefMut", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
|
||||
"dioxus_signals::WriteLock",
|
||||
{ path = "dioxus_signals::WriteLock", reason = "Write should not be held over an await point. This will cause any reads or writes to fail while the await is pending since the write borrow is still active." },
|
||||
]
|
||||
22
crates/cursor-mover-webapp/index.html
Normal file
22
crates/cursor-mover-webapp/index.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<script>
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/{base_path}/assets/sw.js");
|
||||
}
|
||||
</script>
|
||||
<link rel="manifest" href="/assets/manifest.json" />
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="worker-src 'self'"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
crates/cursor-mover-webapp/public/assets/logo_192.png
Normal file
BIN
crates/cursor-mover-webapp/public/assets/logo_192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
crates/cursor-mover-webapp/public/assets/logo_512.png
Normal file
BIN
crates/cursor-mover-webapp/public/assets/logo_512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
34
crates/cursor-mover-webapp/public/assets/manifest.json
Normal file
34
crates/cursor-mover-webapp/public/assets/manifest.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "Cursor Mover",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo_192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo_512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "logo_512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "any",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"id": "/",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-control-overlay", "standalone"],
|
||||
"scope": "/",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff",
|
||||
"short_name": "Cursr Mover",
|
||||
"description": "A web control your cursor and keyboard via a smartphone.",
|
||||
"dir": "ltr",
|
||||
"lang": "en",
|
||||
"orientation": "portrait"
|
||||
}
|
||||
198
crates/cursor-mover-webapp/public/assets/sw.js
Normal file
198
crates/cursor-mover-webapp/public/assets/sw.js
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"use strict";
|
||||
|
||||
//console.log('WORKER: executing.');
|
||||
|
||||
/* A version number is useful when updating the worker logic,
|
||||
allowing you to remove outdated cache entries during the update.
|
||||
*/
|
||||
var version = "v1.0.0::";
|
||||
|
||||
/* These resources will be downloaded and cached by the service worker
|
||||
during the installation process. If any resource fails to be downloaded,
|
||||
then the service worker won't be installed either.
|
||||
*/
|
||||
var offlineFundamentals = [
|
||||
// add here the files you want to cache
|
||||
//"favicon.ico",
|
||||
];
|
||||
|
||||
/* The install event fires when the service worker is first installed.
|
||||
You can use this event to prepare the service worker to be able to serve
|
||||
files while visitors are offline.
|
||||
*/
|
||||
self.addEventListener("install", function (event) {
|
||||
//console.log('WORKER: install event in progress.');
|
||||
/* Using event.waitUntil(p) blocks the installation process on the provided
|
||||
promise. If the promise is rejected, the service worker won't be installed.
|
||||
*/
|
||||
event.waitUntil(
|
||||
/* The caches built-in is a promise-based API that helps you cache responses,
|
||||
as well as finding and deleting them.
|
||||
*/
|
||||
caches
|
||||
/* You can open a cache by name, and this method returns a promise. We use
|
||||
a versioned cache name here so that we can remove old cache entries in
|
||||
one fell swoop later, when phasing out an older service worker.
|
||||
*/
|
||||
.open(version + "fundamentals")
|
||||
.then(function (cache) {
|
||||
/* After the cache is opened, we can fill it with the offline fundamentals.
|
||||
The method below will add all resources in `offlineFundamentals` to the
|
||||
cache, after making requests for them.
|
||||
*/
|
||||
return cache.addAll(offlineFundamentals);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: install completed');
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
/* The fetch event fires whenever a page controlled by this service worker requests
|
||||
a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it
|
||||
comprehends even the request for the HTML page on first load, as well as JS and
|
||||
CSS resources, fonts, any images, etc.
|
||||
*/
|
||||
self.addEventListener("fetch", function (event) {
|
||||
//console.log('WORKER: fetch event in progress.');
|
||||
|
||||
/* We should only cache GET requests, and deal with the rest of method in the
|
||||
client-side, by handling failed POST,PUT,PATCH,etc. requests.
|
||||
*/
|
||||
if (event.request.method !== "GET") {
|
||||
/* If we don't block the event as shown below, then the request will go to
|
||||
the network as usual.
|
||||
*/
|
||||
//console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
|
||||
return;
|
||||
}
|
||||
/* Similar to event.waitUntil in that it blocks the fetch event on a promise.
|
||||
Fulfillment result will be used as the response, and rejection will end in a
|
||||
HTTP response indicating failure.
|
||||
*/
|
||||
event.respondWith(
|
||||
caches
|
||||
/* This method returns a promise that resolves to a cache entry matching
|
||||
the request. Once the promise is settled, we can then provide a response
|
||||
to the fetch request.
|
||||
*/
|
||||
.match(event.request)
|
||||
.then(function (cached) {
|
||||
/* Even if the response is in our cache, we go to the network as well.
|
||||
This pattern is known for producing "eventually fresh" responses,
|
||||
where we return cached responses immediately, and meanwhile pull
|
||||
a network response and store that in the cache.
|
||||
|
||||
Read more:
|
||||
https://ponyfoo.com/articles/progressive-networking-serviceworker
|
||||
*/
|
||||
var networked = fetch(event.request)
|
||||
// We handle the network request with success and failure scenarios.
|
||||
.then(fetchedFromNetwork, unableToResolve)
|
||||
// We should catch errors on the fetchedFromNetwork handler as well.
|
||||
.catch(unableToResolve);
|
||||
|
||||
/* We return the cached response immediately if there is one, and fall
|
||||
back to waiting on the network as usual.
|
||||
*/
|
||||
//console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
|
||||
return cached || networked;
|
||||
|
||||
function fetchedFromNetwork(response) {
|
||||
/* We copy the response before replying to the network request.
|
||||
This is the response that will be stored on the ServiceWorker cache.
|
||||
*/
|
||||
var cacheCopy = response.clone();
|
||||
|
||||
//console.log('WORKER: fetch response from network.', event.request.url);
|
||||
|
||||
caches
|
||||
// We open a cache to store the response for this request.
|
||||
.open(version + "pages")
|
||||
.then(function add(cache) {
|
||||
/* We store the response for this request. It'll later become
|
||||
available to caches.match(event.request) calls, when looking
|
||||
for cached responses.
|
||||
*/
|
||||
cache.put(event.request, cacheCopy);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: fetch response stored in cache.', event.request.url);
|
||||
});
|
||||
|
||||
// Return the response so that the promise is settled in fulfillment.
|
||||
return response;
|
||||
}
|
||||
|
||||
/* When this method is called, it means we were unable to produce a response
|
||||
from either the cache or the network. This is our opportunity to produce
|
||||
a meaningful response even when all else fails. It's the last chance, so
|
||||
you probably want to display a "Service Unavailable" view or a generic
|
||||
error response.
|
||||
*/
|
||||
function unableToResolve() {
|
||||
/* There's a couple of things we can do here.
|
||||
- Test the Accept header and then return one of the `offlineFundamentals`
|
||||
e.g: `return caches.match('/some/cached/image.png')`
|
||||
- You should also consider the origin. It's easier to decide what
|
||||
"unavailable" means for requests against your origins than for requests
|
||||
against a third party, such as an ad provider.
|
||||
- Generate a Response programmatically, as shown below, and return that.
|
||||
*/
|
||||
|
||||
//console.log('WORKER: fetch request failed in both cache and network.');
|
||||
|
||||
/* Here we're creating a response programmatically. The first parameter is the
|
||||
response body, and the second one defines the options for the response.
|
||||
*/
|
||||
return new Response("<h1>Service Unavailable</h1>", {
|
||||
status: 503,
|
||||
statusText: "Service Unavailable",
|
||||
headers: new Headers({
|
||||
"Content-Type": "text/html",
|
||||
}),
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
/* The activate event fires after a service worker has been successfully installed.
|
||||
It is most useful when phasing out an older version of a service worker, as at
|
||||
this point you know that the new worker was installed correctly. In this example,
|
||||
we delete old caches that don't match the version in the worker we just finished
|
||||
installing.
|
||||
*/
|
||||
self.addEventListener("activate", function (event) {
|
||||
/* Just like with the install event, event.waitUntil blocks activate on a promise.
|
||||
Activation will fail unless the promise is fulfilled.
|
||||
*/
|
||||
//console.log('WORKER: activate event in progress.');
|
||||
|
||||
event.waitUntil(
|
||||
caches
|
||||
/* This method returns a promise which will resolve to an array of available
|
||||
cache keys.
|
||||
*/
|
||||
.keys()
|
||||
.then(function (keys) {
|
||||
// We return a promise that settles when all outdated caches are deleted.
|
||||
return Promise.all(
|
||||
keys
|
||||
.filter(function (key) {
|
||||
// Filter by keys that don't start with the latest version prefix.
|
||||
return !key.startsWith(version);
|
||||
})
|
||||
.map(function (key) {
|
||||
/* Return a promise that's fulfilled
|
||||
when each outdated cache is deleted.
|
||||
*/
|
||||
return caches.delete(key);
|
||||
}),
|
||||
);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: activate completed.');
|
||||
}),
|
||||
);
|
||||
});
|
||||
128
crates/cursor-mover-webapp/src/components/controls.rs
Normal file
128
crates/cursor-mover-webapp/src/components/controls.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use dioxus::{
|
||||
fullstack::{CborEncoding, WebSocketOptions, Websocket, extract::State, use_websocket},
|
||||
html::input_data::MouseButton,
|
||||
logger::tracing,
|
||||
prelude::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::components::{KeyboardArea, MouseArea};
|
||||
|
||||
#[component]
|
||||
pub fn Controls() -> Element {
|
||||
#[css_module("/assets/styling/controls.module.css")]
|
||||
struct Styles;
|
||||
|
||||
let mut socket = use_websocket(move || remote_control(WebSocketOptions::new()));
|
||||
|
||||
use_future(move || async move {
|
||||
loop {
|
||||
// Wait for the socket to connect
|
||||
_ = socket.connect().await;
|
||||
|
||||
// Loop poll with recv. Throws an error when the connection closes, making it possible
|
||||
// to run code before the socket re-connects when the name input changes
|
||||
while let Ok(message) = socket.recv().await {
|
||||
tracing::info!("Received message: {:?}", message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let event_handler = use_callback(move |evt| {
|
||||
spawn(async move {
|
||||
_ = socket.send(evt).await;
|
||||
});
|
||||
});
|
||||
|
||||
match *socket.status().read() {
|
||||
dioxus_fullstack::WebsocketState::Connecting => {
|
||||
rsx! {
|
||||
div {
|
||||
"Connecting..."
|
||||
}
|
||||
}
|
||||
},
|
||||
dioxus_fullstack::WebsocketState::Open => {
|
||||
rsx! {
|
||||
div {
|
||||
class: Styles::controls,
|
||||
MouseArea { onevent: event_handler }
|
||||
KeyboardArea { onevent: event_handler }
|
||||
}
|
||||
}
|
||||
},
|
||||
dioxus_fullstack::WebsocketState::Closing => {
|
||||
rsx! {
|
||||
div {
|
||||
"Closing..."
|
||||
}
|
||||
}
|
||||
},
|
||||
dioxus_fullstack::WebsocketState::Closed => {
|
||||
rsx! {
|
||||
div {
|
||||
"Closed..."
|
||||
}
|
||||
}
|
||||
},
|
||||
dioxus_fullstack::WebsocketState::FailedToConnect => {
|
||||
rsx! {
|
||||
div {
|
||||
"Failed to connect..."
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ClientEvent {
|
||||
MouseMove { dx: f64, dy: f64 },
|
||||
MouseScroll { dx: f64, dy: f64 },
|
||||
Click { button: MouseButton },
|
||||
KeyPressEvent { key: String },
|
||||
TextInputStartEvent,
|
||||
TextInputEvent { text: String },
|
||||
TextInputDoneEvent { text: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ServerEvent {
|
||||
Ping,
|
||||
}
|
||||
|
||||
#[expect(clippy::unused_async)]
|
||||
#[get("/api/remote_control_wss", mouse_service: State<crate::server::input_proxy_service::InputProxyService>)]
|
||||
async fn remote_control(
|
||||
options: WebSocketOptions
|
||||
) -> Result<Websocket<ClientEvent, ServerEvent, CborEncoding>> {
|
||||
Ok(options.on_upgrade(move |mut socket| async move {
|
||||
_ = socket.send(ServerEvent::Ping).await;
|
||||
|
||||
while let Ok(event) = socket.recv().await {
|
||||
match event {
|
||||
ClientEvent::MouseMove { dx, dy } => {
|
||||
mouse_service.move_mouse(dx, dy).await;
|
||||
},
|
||||
ClientEvent::MouseScroll { dx, dy } => {
|
||||
mouse_service.mouse_scroll(dx, dy).await;
|
||||
},
|
||||
ClientEvent::Click { button } => {
|
||||
mouse_service.click(button).await;
|
||||
},
|
||||
ClientEvent::KeyPressEvent { key } => {
|
||||
mouse_service.key_press_event(key).await;
|
||||
},
|
||||
ClientEvent::TextInputEvent { text } => {
|
||||
mouse_service.text_input(text).await;
|
||||
},
|
||||
ClientEvent::TextInputStartEvent => {
|
||||
mouse_service.text_input_start().await;
|
||||
},
|
||||
ClientEvent::TextInputDoneEvent { text } => {
|
||||
mouse_service.text_input_end(text).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
57
crates/cursor-mover-webapp/src/components/keyboard_area.rs
Normal file
57
crates/cursor-mover-webapp/src/components/keyboard_area.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::controls::ClientEvent;
|
||||
|
||||
#[component]
|
||||
pub fn KeyboardArea(onevent: EventHandler<ClientEvent>) -> Element {
|
||||
#[css_module("/assets/styling/keyboard_area.module.css")]
|
||||
struct Styles;
|
||||
|
||||
let mut input_state = use_signal(String::new);
|
||||
let input_handler = use_callback(move |evt: Event<FormData>| {
|
||||
let v = evt.value();
|
||||
input_state.set(v.clone());
|
||||
|
||||
onevent.call(ClientEvent::TextInputEvent { text: v });
|
||||
});
|
||||
let key_press_handler = use_callback(move |evt: Event<KeyboardData>| {
|
||||
if input_state.read().is_empty() {
|
||||
match evt.key() {
|
||||
Key::Character(_) => {},
|
||||
_ => {
|
||||
onevent.call(ClientEvent::KeyPressEvent {
|
||||
key: evt.key().to_string(),
|
||||
});
|
||||
},
|
||||
}
|
||||
} else if evt.key() == Key::Enter {
|
||||
onevent.call(ClientEvent::TextInputDoneEvent {
|
||||
text: input_state.replace(String::new()),
|
||||
});
|
||||
}
|
||||
});
|
||||
let input_focus_handler = use_callback(move |_: Event<FocusData>| {
|
||||
input_state.set(String::new());
|
||||
onevent.call(ClientEvent::TextInputStartEvent);
|
||||
});
|
||||
|
||||
let input_blur_handler = use_callback(move |_: Event<FocusData>| {
|
||||
onevent.call(ClientEvent::TextInputDoneEvent {
|
||||
text: input_state.replace(String::new()),
|
||||
});
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: Styles::keyboard_area,
|
||||
|
||||
input {
|
||||
oninput: input_handler,
|
||||
value: input_state,
|
||||
onkeydown: key_press_handler,
|
||||
onfocus: input_focus_handler,
|
||||
onblur: input_blur_handler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
crates/cursor-mover-webapp/src/components/mod.rs
Normal file
6
crates/cursor-mover-webapp/src/components/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
mod controls;
|
||||
mod keyboard_area;
|
||||
mod mouse_area;
|
||||
pub use controls::Controls;
|
||||
pub use keyboard_area::KeyboardArea;
|
||||
pub use mouse_area::MouseArea;
|
||||
169
crates/cursor-mover-webapp/src/components/mouse_area.rs
Normal file
169
crates/cursor-mover-webapp/src/components/mouse_area.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use std::{collections::HashMap, ops::Sub, time::Duration};
|
||||
|
||||
use dioxus::{
|
||||
html::{
|
||||
geometry::{ElementSpace, euclid::Point2D},
|
||||
input_data::MouseButton,
|
||||
},
|
||||
logger::tracing,
|
||||
prelude::*,
|
||||
};
|
||||
use dioxus_html::geometry::euclid::Vector2D;
|
||||
|
||||
use crate::{components::controls::ClientEvent, utils::mouse_filter_buffer::MouseFilterBuffer};
|
||||
|
||||
const FLING_START_THRESHOLD_VELOCITY: f64 = 500.0;
|
||||
const FLING_STOP_THRESHOLD_VELOCITY: f64 = 100.0;
|
||||
const FLING_DAMPENING: f64 = 0.98;
|
||||
|
||||
const CURSOR_SPEED_MULTIPLIER: f64 = 1.5;
|
||||
|
||||
pub struct PointerRegistryData {
|
||||
initial_position: Point2D<f64, ElementSpace>,
|
||||
|
||||
last_positions: MouseFilterBuffer,
|
||||
}
|
||||
|
||||
pub struct FlingerData {
|
||||
velocity: Vector2D<f64, ElementSpace>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PointerRegistry {
|
||||
pointers: HashMap<i32, PointerRegistryData>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FlingerRegistry {
|
||||
flinger: Option<FlingerData>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn MouseArea(onevent: EventHandler<ClientEvent>) -> Element {
|
||||
#[css_module("/assets/styling/mouse_area.module.css")]
|
||||
struct Styles;
|
||||
|
||||
let mut registry = use_signal::<PointerRegistry>(PointerRegistry::default);
|
||||
let mut flingers = use_signal::<FlingerRegistry>(FlingerRegistry::default);
|
||||
|
||||
let pointer_move_handler = use_callback(move |evt: Event<PointerData>| {
|
||||
let mut registry = registry.write();
|
||||
if let Some(data) = registry.pointers.get_mut(&evt.pointer_id()) {
|
||||
evt.prevent_default();
|
||||
let point = evt.element_coordinates();
|
||||
let last_position = data.last_positions.back();
|
||||
let delta = point - last_position.position;
|
||||
|
||||
data.last_positions
|
||||
.push(point, wasmtimer::std::SystemTime::now());
|
||||
|
||||
if registry.pointers.len() == 1 {
|
||||
onevent.call(ClientEvent::MouseMove {
|
||||
dx: delta.x * CURSOR_SPEED_MULTIPLIER,
|
||||
dy: delta.y * CURSOR_SPEED_MULTIPLIER,
|
||||
});
|
||||
} else if registry.pointers.len() == 2 {
|
||||
onevent.call(ClientEvent::MouseScroll {
|
||||
dx: -delta.x,
|
||||
dy: -delta.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let pointer_down_handler = use_callback(move |evt: Event<PointerData>| {
|
||||
//If any pointer is down, we cancel the flingers
|
||||
flingers.write().flinger.take();
|
||||
|
||||
let point = evt.element_coordinates();
|
||||
|
||||
registry.write().pointers.insert(
|
||||
evt.pointer_id(),
|
||||
PointerRegistryData {
|
||||
last_positions: MouseFilterBuffer::new(
|
||||
10,
|
||||
point,
|
||||
wasmtimer::std::SystemTime::now(),
|
||||
),
|
||||
initial_position: point,
|
||||
},
|
||||
);
|
||||
});
|
||||
let pointer_up_handler = use_callback(move |evt: Event<PointerData>| {
|
||||
let point = evt.element_coordinates();
|
||||
let mut registry = registry.write();
|
||||
let data = registry.pointers.remove(&evt.pointer_id());
|
||||
if let Some(data) = data {
|
||||
let distance_moved = data.initial_position - point;
|
||||
let release_velocity = data.last_positions.average_velocity_since(
|
||||
wasmtimer::std::SystemTime::now().sub(Duration::from_millis(100)),
|
||||
);
|
||||
tracing::info!("Release Velocity: {:?}", release_velocity.length());
|
||||
|
||||
if distance_moved.length() <= 1.0 {
|
||||
match registry.pointers.len() {
|
||||
0 => {
|
||||
onevent.call(ClientEvent::Click {
|
||||
button: MouseButton::Primary,
|
||||
});
|
||||
},
|
||||
1 => {
|
||||
onevent.call(ClientEvent::Click {
|
||||
button: MouseButton::Secondary,
|
||||
});
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
} else if release_velocity.length() > FLING_START_THRESHOLD_VELOCITY {
|
||||
//We only fling if there are no other pointers
|
||||
if registry.pointers.is_empty() {
|
||||
flingers.write().flinger = Some(FlingerData {
|
||||
velocity: release_velocity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
use_future(move || async move {
|
||||
let mut last_frame_time = wasmtimer::std::SystemTime::now();
|
||||
|
||||
loop {
|
||||
wasmtimer::tokio::sleep(Duration::from_millis(16)).await;
|
||||
let new_frame_time = wasmtimer::std::SystemTime::now();
|
||||
let delta_seconds = new_frame_time
|
||||
.duration_since(last_frame_time)
|
||||
.unwrap()
|
||||
.as_secs_f64();
|
||||
last_frame_time = new_frame_time;
|
||||
|
||||
let mut flinger = flingers.write();
|
||||
let new_flinger = flinger.flinger.as_ref().and_then(|flinger| {
|
||||
if flinger.velocity.length() < FLING_STOP_THRESHOLD_VELOCITY {
|
||||
None
|
||||
} else {
|
||||
onevent.call(ClientEvent::MouseMove {
|
||||
dx: flinger.velocity.x * delta_seconds * CURSOR_SPEED_MULTIPLIER,
|
||||
dy: flinger.velocity.y * delta_seconds * CURSOR_SPEED_MULTIPLIER,
|
||||
});
|
||||
|
||||
//tracing::info!("Fling: {:?}", flinger.velocity);
|
||||
Some(FlingerData {
|
||||
velocity: flinger.velocity * FLING_DAMPENING,
|
||||
})
|
||||
}
|
||||
});
|
||||
flinger.flinger = new_flinger;
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: Styles::mouse_area,
|
||||
|
||||
onpointermove: pointer_move_handler,
|
||||
onpointerdown: pointer_down_handler,
|
||||
onpointerup: pointer_up_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
53
crates/cursor-mover-webapp/src/main.rs
Normal file
53
crates/cursor-mover-webapp/src/main.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#![expect(clippy::volatile_composites)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use views::{Home, Navbar};
|
||||
|
||||
mod components;
|
||||
mod utils;
|
||||
mod views;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod server;
|
||||
|
||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||
#[rustfmt::skip]
|
||||
enum Route {
|
||||
#[layout(Navbar)]
|
||||
#[route("/")]
|
||||
Home {},
|
||||
// #[route("/blog/:id")]
|
||||
// Blog { id: i32 },
|
||||
}
|
||||
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::launch(App);
|
||||
|
||||
// When using `Lazy` items, or axum `Extension`s, we need to initialize them in `dioxus::serve`
|
||||
// before launching our app.
|
||||
#[cfg(feature = "server")]
|
||||
dioxus::serve(|| async move {
|
||||
use crate::server::input_proxy_service::InputProxyService;
|
||||
use dioxus::server::axum::Extension;
|
||||
|
||||
let router = dioxus::server::router(App);
|
||||
let router = router.layer(Extension(InputProxyService::start()));
|
||||
|
||||
Ok(router)
|
||||
});
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
document::Meta { name: "viewport", content: "width=device-width, initial-scale=1.0" }
|
||||
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
566
crates/cursor-mover-webapp/src/server/input_proxy_service.rs
Normal file
566
crates/cursor-mover-webapp/src/server/input_proxy_service.rs
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
use std::{io::Write, sync::Arc, time::Duration};
|
||||
|
||||
use dioxus::{
|
||||
fullstack::{FullstackContext, extract::FromRef},
|
||||
logger::tracing,
|
||||
};
|
||||
use dioxus_html::input_data::MouseButton;
|
||||
use memfile::MemFile;
|
||||
use rustix::time::{ClockId, clock_gettime};
|
||||
use tokio::sync::Mutex;
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
|
||||
protocol::{
|
||||
wl_pointer::{Axis, ButtonState},
|
||||
wl_registry::{Event, WlRegistry},
|
||||
wl_seat::{self, WlSeat},
|
||||
},
|
||||
};
|
||||
use wayland_protocols_misc::{
|
||||
zwp_input_method_v2::client::{
|
||||
zwp_input_method_manager_v2::ZwpInputMethodManagerV2, zwp_input_method_v2::ZwpInputMethodV2,
|
||||
},
|
||||
zwp_virtual_keyboard_v1::client::{
|
||||
zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1,
|
||||
zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1,
|
||||
},
|
||||
};
|
||||
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
||||
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
|
||||
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
|
||||
};
|
||||
|
||||
use crate::server::keymap::{self, web_key_to_linux_keycode};
|
||||
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
|
||||
const BUTTON_LEFT: u32 = 0x110;
|
||||
const BUTTON_RIGHT: u32 = 0x111;
|
||||
const BUTTON_MIDDLE: u32 = 0x112;
|
||||
|
||||
// https://wayland.app/protocols/wayland#wl_keyboard:enum:keymap_format
|
||||
#[expect(unused)]
|
||||
const NO_KEYMAP: u32 = 0;
|
||||
const XKB_V1: u32 = 1;
|
||||
|
||||
pub fn get_wayland_timestamp() -> u32 {
|
||||
let ts = clock_gettime(ClockId::Monotonic);
|
||||
u32::try_from(
|
||||
Duration::new(
|
||||
u64::try_from(ts.tv_sec).unwrap(),
|
||||
u32::try_from(ts.tv_nsec).unwrap(),
|
||||
)
|
||||
.as_millis(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InputProxyService {
|
||||
input_proxy_service_state: Arc<Mutex<InputProxy>>,
|
||||
}
|
||||
|
||||
impl InputProxyService {
|
||||
pub fn start() -> Self {
|
||||
Self {
|
||||
input_proxy_service_state: Arc::new(Mutex::new(InputProxy::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn move_mouse(
|
||||
&self,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
) {
|
||||
let guard = self.input_proxy_service_state.lock().await;
|
||||
if let Some(pointer) = &guard.state.virtual_pointer() {
|
||||
let time = get_wayland_timestamp();
|
||||
pointer.motion(time, dx, dy);
|
||||
pointer.frame();
|
||||
guard.event_queue.flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mouse_scroll(
|
||||
&self,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
) {
|
||||
let guard = self.input_proxy_service_state.lock().await;
|
||||
if let Some(pointer) = &guard.state.virtual_pointer() {
|
||||
let time = get_wayland_timestamp();
|
||||
pointer.axis(time, Axis::HorizontalScroll, dx);
|
||||
pointer.axis(time, Axis::VerticalScroll, dy);
|
||||
pointer.frame();
|
||||
guard.event_queue.flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn click(
|
||||
&self,
|
||||
button: MouseButton,
|
||||
) {
|
||||
let guard = self.input_proxy_service_state.lock().await;
|
||||
if let Some(pointer) = &guard.state.virtual_pointer() {
|
||||
tracing::info!("Do click");
|
||||
let time = get_wayland_timestamp();
|
||||
|
||||
let button = match button {
|
||||
MouseButton::Primary => BUTTON_LEFT,
|
||||
MouseButton::Secondary => BUTTON_RIGHT,
|
||||
MouseButton::Auxiliary => BUTTON_MIDDLE,
|
||||
MouseButton::Fourth | MouseButton::Fifth | MouseButton::Unknown => {
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
pointer.button(time, button, ButtonState::Pressed);
|
||||
pointer.frame();
|
||||
pointer.button(time, button, ButtonState::Released);
|
||||
pointer.frame();
|
||||
guard.event_queue.flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn key_press_event(
|
||||
&self,
|
||||
key: String,
|
||||
) {
|
||||
let guard = self.input_proxy_service_state.lock().await;
|
||||
|
||||
if let Some(keyboard) = guard.state.virtual_keyboard() {
|
||||
let time = get_wayland_timestamp();
|
||||
let key = web_key_to_linux_keycode(key.as_str()).unwrap();
|
||||
keyboard.key(time, key, 1);
|
||||
keyboard.key(time, key, 0);
|
||||
|
||||
guard.event_queue.flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn text_input_start(&self) {
|
||||
let mut guard = self.input_proxy_service_state.lock().await;
|
||||
|
||||
if let Some(ime) = guard.state.input_method_mut() {
|
||||
ime.input_method_state += 1;
|
||||
|
||||
tracing::info!("Text Input Start");
|
||||
ime.input_method.delete_surrounding_text(4, 4);
|
||||
ime.input_method.commit(ime.input_method_state);
|
||||
|
||||
guard.event_queue.flush().unwrap();
|
||||
}
|
||||
}
|
||||
pub async fn text_input(
|
||||
&self,
|
||||
key: String,
|
||||
) {
|
||||
let mut guard = self.input_proxy_service_state.lock().await;
|
||||
|
||||
if let Some(ime) = guard.state.input_method_mut() {
|
||||
ime.input_method_state += 1;
|
||||
|
||||
tracing::info!("Text Input {key}");
|
||||
ime.input_method.set_preedit_string(key, 0, 0);
|
||||
ime.input_method.commit(ime.input_method_state);
|
||||
guard.event_queue.flush().unwrap();
|
||||
}
|
||||
}
|
||||
pub async fn text_input_end(
|
||||
&self,
|
||||
text: String,
|
||||
) {
|
||||
let mut guard = self.input_proxy_service_state.lock().await;
|
||||
|
||||
if let Some(ime) = guard.state.input_method_mut() {
|
||||
ime.input_method_state += 1;
|
||||
|
||||
tracing::info!("Text Input End");
|
||||
ime.input_method.commit_string(text);
|
||||
ime.input_method.commit(ime.input_method_state);
|
||||
guard.event_queue.flush().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<FullstackContext> for InputProxyService {
|
||||
fn from_ref(state: &FullstackContext) -> Self {
|
||||
state.extension::<Self>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputProxy {
|
||||
state: InputProxyServiceState,
|
||||
event_queue: EventQueue<InputProxyServiceState>,
|
||||
}
|
||||
|
||||
impl InputProxy {
|
||||
pub fn new() -> Self {
|
||||
let connection = Connection::connect_to_env().unwrap();
|
||||
let display = connection.display();
|
||||
|
||||
let mut event_queue = connection.new_event_queue();
|
||||
let queue_handle = event_queue.handle();
|
||||
let _ = display.get_registry(&queue_handle, ());
|
||||
|
||||
let mut input_proxy_service_state = InputProxyServiceState::new(queue_handle);
|
||||
event_queue
|
||||
.roundtrip(&mut input_proxy_service_state)
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
state: input_proxy_service_state,
|
||||
event_queue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Keymap {
|
||||
file: MemFile,
|
||||
size: u32,
|
||||
}
|
||||
|
||||
struct InputMethod {
|
||||
input_method: ZwpInputMethodV2,
|
||||
input_method_state: u32,
|
||||
}
|
||||
|
||||
enum InputProxyServiceState {
|
||||
Incomplete {
|
||||
queue_handle: QueueHandle<Self>,
|
||||
seat: Option<WlSeat>,
|
||||
virtual_pointer_manager: Option<ZwlrVirtualPointerManagerV1>,
|
||||
virtual_keyboard_manager: Option<ZwpVirtualKeyboardManagerV1>,
|
||||
input_method_manager: Option<ZwpInputMethodManagerV2>,
|
||||
keymap: Keymap,
|
||||
},
|
||||
Running {
|
||||
#[expect(unused)]
|
||||
seat: WlSeat,
|
||||
#[expect(unused)]
|
||||
virtual_pointer_manager: ZwlrVirtualPointerManagerV1,
|
||||
#[expect(unused)]
|
||||
virtual_keyboard_manager: ZwpVirtualKeyboardManagerV1,
|
||||
|
||||
virtual_pointer: ZwlrVirtualPointerV1,
|
||||
virtual_keyboard: ZwpVirtualKeyboardV1,
|
||||
|
||||
input_method: InputMethod,
|
||||
},
|
||||
}
|
||||
impl InputProxyServiceState {
|
||||
fn new(queue_handle: QueueHandle<Self>) -> Self {
|
||||
let filestr = keymap::KEYMAP;
|
||||
let mut file = memfile::CreateOptions::new().create("keymap").unwrap();
|
||||
file.write_all(filestr.as_bytes()).unwrap();
|
||||
|
||||
let keymap = Keymap {
|
||||
file,
|
||||
size: u32::try_from(filestr.len()).unwrap(),
|
||||
};
|
||||
|
||||
Self::Incomplete {
|
||||
queue_handle,
|
||||
keymap,
|
||||
seat: None,
|
||||
virtual_pointer_manager: None,
|
||||
virtual_keyboard_manager: None,
|
||||
input_method_manager: None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn virtual_keyboard(&self) -> Option<&ZwpVirtualKeyboardV1> {
|
||||
match self {
|
||||
Self::Running {
|
||||
virtual_keyboard, ..
|
||||
} => Some(virtual_keyboard),
|
||||
Self::Incomplete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn virtual_pointer(&self) -> Option<&ZwlrVirtualPointerV1> {
|
||||
match self {
|
||||
Self::Running {
|
||||
virtual_pointer, ..
|
||||
} => Some(virtual_pointer),
|
||||
Self::Incomplete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn input_method_mut(&mut self) -> Option<&mut InputMethod> {
|
||||
match self {
|
||||
Self::Running { input_method, .. } => Some(input_method),
|
||||
Self::Incomplete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_seat(
|
||||
&mut self,
|
||||
seat: WlSeat,
|
||||
) {
|
||||
if let Self::Incomplete {
|
||||
seat: existing @ None,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
*existing = Some(seat);
|
||||
tracing::info!("Obtained Seat!");
|
||||
self.try_upgrade();
|
||||
} else {
|
||||
tracing::info!("Received duplicate wl_seat");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_virtual_pointer_manager(
|
||||
&mut self,
|
||||
virtual_pointer_manager: ZwlrVirtualPointerManagerV1,
|
||||
) {
|
||||
if let Self::Incomplete {
|
||||
virtual_pointer_manager: existing @ None,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
*existing = Some(virtual_pointer_manager);
|
||||
tracing::info!("Obtained Virtual Pointer Manager!");
|
||||
self.try_upgrade();
|
||||
} else {
|
||||
tracing::info!("Received duplicate ZwlrVirtualPointerManagerV1");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_virtual_keyboard_manager(
|
||||
&mut self,
|
||||
virtual_keyboard_manager: ZwpVirtualKeyboardManagerV1,
|
||||
) {
|
||||
if let Self::Incomplete {
|
||||
virtual_keyboard_manager: existing @ None,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
*existing = Some(virtual_keyboard_manager);
|
||||
tracing::info!("Obtained Virtual Keyboard Manager!");
|
||||
self.try_upgrade();
|
||||
} else {
|
||||
tracing::info!("Received duplicate ZwpVirtualKeyboardManagerV1");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_input_method_manager(
|
||||
&mut self,
|
||||
input_method_manager: ZwpInputMethodManagerV2,
|
||||
) {
|
||||
if let Self::Incomplete {
|
||||
input_method_manager: existing @ None,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
*existing = Some(input_method_manager);
|
||||
tracing::info!("Obtained Input Method Manager!");
|
||||
self.try_upgrade();
|
||||
} else {
|
||||
tracing::info!("Received duplicate ZwpInputMethodManagerV2");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_upgrade(&mut self) {
|
||||
if let Self::Incomplete {
|
||||
queue_handle,
|
||||
keymap,
|
||||
seat: seat @ Some(..),
|
||||
virtual_pointer_manager: vpm @ Some(..),
|
||||
virtual_keyboard_manager: vkm @ Some(..),
|
||||
input_method_manager: imm @ Some(..),
|
||||
} = self
|
||||
{
|
||||
let virtual_keyboard = vkm.as_ref().unwrap().create_virtual_keyboard(
|
||||
seat.as_ref().unwrap(),
|
||||
queue_handle,
|
||||
(),
|
||||
);
|
||||
|
||||
virtual_keyboard.keymap(XKB_V1, keymap.file.as_fd(), keymap.size);
|
||||
|
||||
let virtual_pointer = vpm.as_ref().unwrap().create_virtual_pointer(
|
||||
Some(seat.as_ref().unwrap()),
|
||||
queue_handle,
|
||||
(),
|
||||
);
|
||||
|
||||
let input_method =
|
||||
imm.as_ref()
|
||||
.unwrap()
|
||||
.get_input_method(seat.as_ref().unwrap(), queue_handle, ());
|
||||
|
||||
tracing::info!("InputProxyServiceState upgraded to running");
|
||||
*self = Self::Running {
|
||||
seat: seat.take().unwrap(),
|
||||
virtual_pointer_manager: vpm.take().unwrap(),
|
||||
virtual_keyboard_manager: vkm.take().unwrap(),
|
||||
virtual_pointer,
|
||||
virtual_keyboard,
|
||||
input_method: InputMethod {
|
||||
input_method,
|
||||
input_method_state: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrVirtualPointerV1, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwlrVirtualPointerV1,
|
||||
_event: <ZwlrVirtualPointerV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
// No events for ZwlrVirtualPointerV1: https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1
|
||||
tracing::warn!("Unknown event received from ZwlrVirtualPointerV1");
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwpVirtualKeyboardV1, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwpVirtualKeyboardV1,
|
||||
_event: <ZwpVirtualKeyboardV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
// No events for ZwpVirtualKeyboardV1: https://wayland.app/protocols/virtual-keyboard-unstable-v1
|
||||
tracing::warn!("Unknown event received from ZwpVirtualKeyboardV1");
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrVirtualPointerManagerV1, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwlrVirtualPointerManagerV1,
|
||||
_event: <ZwlrVirtualPointerManagerV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
// No events for ZwlrVirtualPointerManagerV1: https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1
|
||||
tracing::warn!("Unknown event received from ZwlrVirtualPointerManagerV1");
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwpVirtualKeyboardManagerV1, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwpVirtualKeyboardManagerV1,
|
||||
_event: <ZwpVirtualKeyboardManagerV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
// No events for ZwpVirtualKeyboardManagerV1: https://wayland.app/protocols/virtual-keyboard-unstable-v1
|
||||
tracing::warn!("Unknown event received from ZwpVirtualKeyboardManagerV1");
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwpInputMethodManagerV2, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwpInputMethodManagerV2,
|
||||
_event: <ZwpInputMethodManagerV2 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
tracing::warn!("Unknown event received from ZwpInputMethodManagerV2");
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwpInputMethodV2, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwpInputMethodV2,
|
||||
_event: <ZwpInputMethodV2 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
tracing::warn!("Unknown event received from ZwpInputMethodV2");
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlSeat, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlSeat,
|
||||
event: <WlSeat as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_seat::Event::Capabilities { capabilities } => {
|
||||
tracing::info!("WlSeat capabilities: {:?}", capabilities);
|
||||
},
|
||||
wl_seat::Event::Name { name } => {
|
||||
tracing::info!("WlSeat name: {:?}", name);
|
||||
},
|
||||
_ => {
|
||||
tracing::warn!("Unknown event received from WlSeat");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlRegistry, ()> for InputProxyServiceState {
|
||||
fn event(
|
||||
app_data: &mut Self,
|
||||
registry: &WlRegistry,
|
||||
event: Event,
|
||||
_udata: &(),
|
||||
_conn: &Connection,
|
||||
queue_handle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} => match interface.as_str() {
|
||||
"wl_seat" => {
|
||||
let seat = registry.bind::<WlSeat, _, _>(name, version, queue_handle, ());
|
||||
app_data.set_seat(seat);
|
||||
},
|
||||
"zwlr_virtual_pointer_manager_v1" => {
|
||||
let manager = registry.bind::<ZwlrVirtualPointerManagerV1, _, _>(
|
||||
name,
|
||||
version,
|
||||
queue_handle,
|
||||
(),
|
||||
);
|
||||
app_data.set_virtual_pointer_manager(manager);
|
||||
},
|
||||
"zwp_virtual_keyboard_manager_v1" => {
|
||||
let manager = registry.bind::<ZwpVirtualKeyboardManagerV1, _, _>(
|
||||
name,
|
||||
version,
|
||||
queue_handle,
|
||||
(),
|
||||
);
|
||||
app_data.set_virtual_keyboard_manager(manager);
|
||||
},
|
||||
"zwp_input_method_manager_v2" => {
|
||||
let manager = registry.bind::<ZwpInputMethodManagerV2, _, _>(
|
||||
name,
|
||||
version,
|
||||
queue_handle,
|
||||
(),
|
||||
);
|
||||
app_data.set_input_method_manager(manager);
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Event::GlobalRemove { .. } => todo!(),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
563
crates/cursor-mover-webapp/src/server/keymap.rs
Normal file
563
crates/cursor-mover-webapp/src/server/keymap.rs
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
#![expect(unused)]
|
||||
|
||||
use dioxus::logger::tracing;
|
||||
|
||||
fn is_key_string(s: &str) -> bool {
|
||||
s.chars().all(|c| !c.is_control()) && s.chars().skip(1).all(|c| !c.is_ascii())
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub fn web_key_to_linux_keycode(s: &str) -> Option<u32> {
|
||||
tracing::info!("Converting {s}");
|
||||
match s {
|
||||
"Unidentified" => todo!(),
|
||||
"Alt" => Some(KEY_LEFTALT),
|
||||
"AltGraph" => Some(KEY_RIGHTALT),
|
||||
"CapsLock" => todo!(),
|
||||
"Control" => todo!(),
|
||||
"Fn" => todo!(),
|
||||
"FnLock" => todo!(),
|
||||
"Meta" => todo!(),
|
||||
"NumLock" => todo!(),
|
||||
"ScrollLock" => todo!(),
|
||||
"Shift" => todo!(),
|
||||
"Symbol" => todo!(),
|
||||
"SymbolLock" => todo!(),
|
||||
"Hyper" => todo!(),
|
||||
"Super" => todo!(),
|
||||
"Enter" => Some(KEY_ENTER),
|
||||
"Tab" => Some(KEY_TAB),
|
||||
"ArrowDown" => Some(KEY_DOWN),
|
||||
"ArrowLeft" => Some(KEY_LEFT),
|
||||
"ArrowRight" => Some(KEY_RIGHT),
|
||||
"ArrowUp" => Some(KEY_UP),
|
||||
"End" => Some(KEY_END),
|
||||
"Home" => Some(KEY_HOME),
|
||||
"PageDown" => Some(KEY_PAGEDOWN),
|
||||
"PageUp" => Some(KEY_PAGEUP),
|
||||
"Backspace" => Some(KEY_BACKSPACE),
|
||||
"Clear" => todo!(),
|
||||
"Copy" => todo!(),
|
||||
"CrSel" => todo!(),
|
||||
"Cut" => todo!(),
|
||||
"Delete" => todo!(),
|
||||
"EraseEof" => todo!(),
|
||||
"ExSel" => todo!(),
|
||||
"Insert" => todo!(),
|
||||
"Paste" => todo!(),
|
||||
"Redo" => todo!(),
|
||||
"Undo" => todo!(),
|
||||
"Accept" => todo!(),
|
||||
"Again" => todo!(),
|
||||
"Attn" => todo!(),
|
||||
"Cancel" => todo!(),
|
||||
"ContextMenu" => todo!(),
|
||||
"Escape" => todo!(),
|
||||
"Execute" => todo!(),
|
||||
"Find" => todo!(),
|
||||
"Help" => todo!(),
|
||||
"Pause" => todo!(),
|
||||
"Play" => todo!(),
|
||||
"Props" => todo!(),
|
||||
"Select" => todo!(),
|
||||
"ZoomIn" => todo!(),
|
||||
"ZoomOut" => todo!(),
|
||||
"BrightnessDown" => todo!(),
|
||||
"BrightnessUp" => todo!(),
|
||||
"Eject" => todo!(),
|
||||
"LogOff" => todo!(),
|
||||
"Power" => todo!(),
|
||||
"PowerOff" => todo!(),
|
||||
"PrintScreen" => todo!(),
|
||||
"Hibernate" => todo!(),
|
||||
"Standby" => todo!(),
|
||||
"WakeUp" => todo!(),
|
||||
"AllCandidates" => todo!(),
|
||||
"Alphanumeric" => todo!(),
|
||||
"CodeInput" => todo!(),
|
||||
"Compose" => todo!(),
|
||||
"Convert" => todo!(),
|
||||
"Dead" => todo!(),
|
||||
"FinalMode" => todo!(),
|
||||
"GroupFirst" => todo!(),
|
||||
"GroupLast" => todo!(),
|
||||
"GroupNext" => todo!(),
|
||||
"GroupPrevious" => todo!(),
|
||||
"ModeChange" => todo!(),
|
||||
"NextCandidate" => todo!(),
|
||||
"NonConvert" => todo!(),
|
||||
"PreviousCandidate" => todo!(),
|
||||
"Process" => None,
|
||||
"SingleCandidate" => todo!(),
|
||||
"HangulMode" => todo!(),
|
||||
"HanjaMode" => todo!(),
|
||||
"JunjaMode" => todo!(),
|
||||
"Eisu" => todo!(),
|
||||
"Hankaku" => todo!(),
|
||||
"Hiragana" => todo!(),
|
||||
"HiraganaKatakana" => todo!(),
|
||||
"KanaMode" => todo!(),
|
||||
"KanjiMode" => todo!(),
|
||||
"Katakana" => todo!(),
|
||||
"Romaji" => todo!(),
|
||||
"Zenkaku" => todo!(),
|
||||
"ZenkakuHankaku" => todo!(),
|
||||
"Soft1" => todo!(),
|
||||
"Soft2" => todo!(),
|
||||
"Soft3" => todo!(),
|
||||
"Soft4" => todo!(),
|
||||
"ChannelDown" => todo!(),
|
||||
"ChannelUp" => todo!(),
|
||||
"Close" => todo!(),
|
||||
"MailForward" => todo!(),
|
||||
"MailReply" => todo!(),
|
||||
"MailSend" => todo!(),
|
||||
"MediaClose" => todo!(),
|
||||
"MediaFastForward" => todo!(),
|
||||
"MediaPause" => todo!(),
|
||||
"MediaPlay" => todo!(),
|
||||
"MediaPlayPause" => todo!(),
|
||||
"MediaRecord" => todo!(),
|
||||
"MediaRewind" => todo!(),
|
||||
"MediaStop" => todo!(),
|
||||
"MediaTrackNext" => todo!(),
|
||||
"MediaTrackPrevious" => todo!(),
|
||||
"New" => todo!(),
|
||||
"Open" => todo!(),
|
||||
"Print" => todo!(),
|
||||
"Save" => todo!(),
|
||||
"SpellCheck" => todo!(),
|
||||
"Key11" => todo!(),
|
||||
"Key12" => todo!(),
|
||||
"AudioBalanceLeft" => todo!(),
|
||||
"AudioBalanceRight" => todo!(),
|
||||
"AudioBassBoostDown" => todo!(),
|
||||
"AudioBassBoostToggle" => todo!(),
|
||||
"AudioBassBoostUp" => todo!(),
|
||||
"AudioFaderFront" => todo!(),
|
||||
"AudioFaderRear" => todo!(),
|
||||
"AudioSurroundModeNext" => todo!(),
|
||||
"AudioTrebleDown" => todo!(),
|
||||
"AudioTrebleUp" => todo!(),
|
||||
"AudioVolumeDown" => todo!(),
|
||||
"AudioVolumeUp" => todo!(),
|
||||
"AudioVolumeMute" => todo!(),
|
||||
"MicrophoneToggle" => todo!(),
|
||||
"MicrophoneVolumeDown" => todo!(),
|
||||
"MicrophoneVolumeUp" => todo!(),
|
||||
"MicrophoneVolumeMute" => todo!(),
|
||||
"SpeechCorrectionList" => todo!(),
|
||||
"SpeechInputToggle" => todo!(),
|
||||
"LaunchApplication1" => todo!(),
|
||||
"LaunchApplication2" => todo!(),
|
||||
"LaunchCalendar" => todo!(),
|
||||
"LaunchContacts" => todo!(),
|
||||
"LaunchMail" => todo!(),
|
||||
"LaunchMediaPlayer" => todo!(),
|
||||
"LaunchMusicPlayer" => todo!(),
|
||||
"LaunchPhone" => todo!(),
|
||||
"LaunchScreenSaver" => todo!(),
|
||||
"LaunchSpreadsheet" => todo!(),
|
||||
"LaunchWebBrowser" => todo!(),
|
||||
"LaunchWebCam" => todo!(),
|
||||
"LaunchWordProcessor" => todo!(),
|
||||
"BrowserBack" => todo!(),
|
||||
"BrowserFavorites" => todo!(),
|
||||
"BrowserForward" => todo!(),
|
||||
"BrowserHome" => todo!(),
|
||||
"BrowserRefresh" => todo!(),
|
||||
"BrowserSearch" => todo!(),
|
||||
"BrowserStop" => todo!(),
|
||||
"AppSwitch" => todo!(),
|
||||
"Call" => todo!(),
|
||||
"Camera" => todo!(),
|
||||
"CameraFocus" => todo!(),
|
||||
"EndCall" => todo!(),
|
||||
"GoBack" => todo!(),
|
||||
"GoHome" => todo!(),
|
||||
"HeadsetHook" => todo!(),
|
||||
"LastNumberRedial" => todo!(),
|
||||
"Notification" => todo!(),
|
||||
"MannerMode" => todo!(),
|
||||
"VoiceDial" => todo!(),
|
||||
"TV" => todo!(),
|
||||
"TV3DMode" => todo!(),
|
||||
"TVAntennaCable" => todo!(),
|
||||
"TVAudioDescription" => todo!(),
|
||||
"TVAudioDescriptionMixDown" => todo!(),
|
||||
"TVAudioDescriptionMixUp" => todo!(),
|
||||
"TVContentsMenu" => todo!(),
|
||||
"TVDataService" => todo!(),
|
||||
"TVInput" => todo!(),
|
||||
"TVInputComponent1" => todo!(),
|
||||
"TVInputComponent2" => todo!(),
|
||||
"TVInputComposite1" => todo!(),
|
||||
"TVInputComposite2" => todo!(),
|
||||
"TVInputHDMI1" => todo!(),
|
||||
"TVInputHDMI2" => todo!(),
|
||||
"TVInputHDMI3" => todo!(),
|
||||
"TVInputHDMI4" => todo!(),
|
||||
"TVInputVGA1" => todo!(),
|
||||
"TVMediaContext" => todo!(),
|
||||
"TVNetwork" => todo!(),
|
||||
"TVNumberEntry" => todo!(),
|
||||
"TVPower" => todo!(),
|
||||
"TVRadioService" => todo!(),
|
||||
"TVSatellite" => todo!(),
|
||||
"TVSatelliteBS" => todo!(),
|
||||
"TVSatelliteCS" => todo!(),
|
||||
"TVSatelliteToggle" => todo!(),
|
||||
"TVTerrestrialAnalog" => todo!(),
|
||||
"TVTerrestrialDigital" => todo!(),
|
||||
"TVTimer" => todo!(),
|
||||
"AVRInput" => todo!(),
|
||||
"AVRPower" => todo!(),
|
||||
"ColorF0Red" => todo!(),
|
||||
"ColorF1Green" => todo!(),
|
||||
"ColorF2Yellow" => todo!(),
|
||||
"ColorF3Blue" => todo!(),
|
||||
"ColorF4Grey" => todo!(),
|
||||
"ColorF5Brown" => todo!(),
|
||||
"ClosedCaptionToggle" => todo!(),
|
||||
"Dimmer" => todo!(),
|
||||
"DisplaySwap" => todo!(),
|
||||
"DVR" => todo!(),
|
||||
"Exit" => todo!(),
|
||||
"FavoriteClear0" => todo!(),
|
||||
"FavoriteClear1" => todo!(),
|
||||
"FavoriteClear2" => todo!(),
|
||||
"FavoriteClear3" => todo!(),
|
||||
"FavoriteRecall0" => todo!(),
|
||||
"FavoriteRecall1" => todo!(),
|
||||
"FavoriteRecall2" => todo!(),
|
||||
"FavoriteRecall3" => todo!(),
|
||||
"FavoriteStore0" => todo!(),
|
||||
"FavoriteStore1" => todo!(),
|
||||
"FavoriteStore2" => todo!(),
|
||||
"FavoriteStore3" => todo!(),
|
||||
"Guide" => todo!(),
|
||||
"GuideNextDay" => todo!(),
|
||||
"GuidePreviousDay" => todo!(),
|
||||
"Info" => todo!(),
|
||||
"InstantReplay" => todo!(),
|
||||
"Link" => todo!(),
|
||||
"ListProgram" => todo!(),
|
||||
"LiveContent" => todo!(),
|
||||
"Lock" => todo!(),
|
||||
"MediaApps" => todo!(),
|
||||
"MediaAudioTrack" => todo!(),
|
||||
"MediaLast" => todo!(),
|
||||
"MediaSkipBackward" => todo!(),
|
||||
"MediaSkipForward" => todo!(),
|
||||
"MediaStepBackward" => todo!(),
|
||||
"MediaStepForward" => todo!(),
|
||||
"MediaTopMenu" => todo!(),
|
||||
"NavigateIn" => todo!(),
|
||||
"NavigateNext" => todo!(),
|
||||
"NavigateOut" => todo!(),
|
||||
"NavigatePrevious" => todo!(),
|
||||
"NextFavoriteChannel" => todo!(),
|
||||
"NextUserProfile" => todo!(),
|
||||
"OnDemand" => todo!(),
|
||||
"Pairing" => todo!(),
|
||||
"PinPDown" => todo!(),
|
||||
"PinPMove" => todo!(),
|
||||
"PinPToggle" => todo!(),
|
||||
"PinPUp" => todo!(),
|
||||
"PlaySpeedDown" => todo!(),
|
||||
"PlaySpeedReset" => todo!(),
|
||||
"PlaySpeedUp" => todo!(),
|
||||
"RandomToggle" => todo!(),
|
||||
"RcLowBattery" => todo!(),
|
||||
"RecordSpeedNext" => todo!(),
|
||||
"RfBypass" => todo!(),
|
||||
"ScanChannelsToggle" => todo!(),
|
||||
"ScreenModeNext" => todo!(),
|
||||
"Settings" => todo!(),
|
||||
"SplitScreenToggle" => todo!(),
|
||||
"STBInput" => todo!(),
|
||||
"STBPower" => todo!(),
|
||||
"Subtitle" => todo!(),
|
||||
"Teletext" => todo!(),
|
||||
"VideoModeNext" => todo!(),
|
||||
"Wink" => todo!(),
|
||||
"ZoomToggle" => todo!(),
|
||||
"F1" => Some(KEY_F1),
|
||||
"F2" => Some(KEY_F2),
|
||||
"F3" => Some(KEY_F3),
|
||||
"F4" => Some(KEY_F4),
|
||||
"F5" => Some(KEY_F5),
|
||||
"F6" => Some(KEY_F6),
|
||||
"F7" => Some(KEY_F7),
|
||||
"F8" => Some(KEY_F8),
|
||||
"F9" => Some(KEY_F9),
|
||||
"F10" => Some(KEY_F10),
|
||||
"F11" => Some(KEY_F11),
|
||||
"F12" => Some(KEY_F12),
|
||||
"F13" => Some(KEY_F13),
|
||||
"F14" => Some(KEY_F14),
|
||||
"F15" => Some(KEY_F15),
|
||||
"F16" => Some(KEY_F16),
|
||||
"F17" => Some(KEY_F17),
|
||||
"F18" => Some(KEY_F18),
|
||||
"F19" => Some(KEY_F19),
|
||||
"F20" => Some(KEY_F20),
|
||||
"F21" => Some(KEY_F21),
|
||||
"F22" => Some(KEY_F22),
|
||||
"F23" => Some(KEY_F23),
|
||||
"F24" => Some(KEY_F24),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
const KEY_RESERVED: u32 = 0;
|
||||
const KEY_ESC: u32 = 1;
|
||||
const KEY_1: u32 = 2;
|
||||
const KEY_2: u32 = 3;
|
||||
const KEY_3: u32 = 4;
|
||||
const KEY_4: u32 = 5;
|
||||
const KEY_5: u32 = 6;
|
||||
const KEY_6: u32 = 7;
|
||||
const KEY_7: u32 = 8;
|
||||
const KEY_8: u32 = 9;
|
||||
const KEY_9: u32 = 10;
|
||||
const KEY_0: u32 = 11;
|
||||
const KEY_MINUS: u32 = 12;
|
||||
const KEY_EQUAL: u32 = 13;
|
||||
const KEY_BACKSPACE: u32 = 14;
|
||||
const KEY_TAB: u32 = 15;
|
||||
const KEY_Q: u32 = 16;
|
||||
const KEY_W: u32 = 17;
|
||||
const KEY_E: u32 = 18;
|
||||
const KEY_R: u32 = 19;
|
||||
const KEY_T: u32 = 20;
|
||||
const KEY_Y: u32 = 21;
|
||||
const KEY_U: u32 = 22;
|
||||
const KEY_I: u32 = 23;
|
||||
const KEY_O: u32 = 24;
|
||||
const KEY_P: u32 = 25;
|
||||
const KEY_LEFTBRACE: u32 = 26;
|
||||
const KEY_RIGHTBRACE: u32 = 27;
|
||||
const KEY_ENTER: u32 = 28;
|
||||
const KEY_LEFTCTRL: u32 = 29;
|
||||
const KEY_A: u32 = 30;
|
||||
const KEY_S: u32 = 31;
|
||||
const KEY_D: u32 = 32;
|
||||
const KEY_F: u32 = 33;
|
||||
const KEY_G: u32 = 34;
|
||||
const KEY_H: u32 = 35;
|
||||
const KEY_J: u32 = 36;
|
||||
const KEY_K: u32 = 37;
|
||||
const KEY_L: u32 = 38;
|
||||
const KEY_SEMICOLON: u32 = 39;
|
||||
const KEY_APOSTROPHE: u32 = 40;
|
||||
const KEY_GRAVE: u32 = 41;
|
||||
const KEY_LEFTSHIFT: u32 = 42;
|
||||
const KEY_BACKSLASH: u32 = 43;
|
||||
const KEY_Z: u32 = 44;
|
||||
const KEY_X: u32 = 45;
|
||||
const KEY_C: u32 = 46;
|
||||
const KEY_V: u32 = 47;
|
||||
const KEY_B: u32 = 48;
|
||||
const KEY_N: u32 = 49;
|
||||
const KEY_M: u32 = 50;
|
||||
const KEY_COMMA: u32 = 51;
|
||||
const KEY_DOT: u32 = 52;
|
||||
const KEY_SLASH: u32 = 53;
|
||||
const KEY_RIGHTSHIFT: u32 = 54;
|
||||
const KEY_KPASTERISK: u32 = 55;
|
||||
const KEY_LEFTALT: u32 = 56;
|
||||
const KEY_SPACE: u32 = 57;
|
||||
const KEY_CAPSLOCK: u32 = 58;
|
||||
const KEY_F1: u32 = 59;
|
||||
const KEY_F2: u32 = 60;
|
||||
const KEY_F3: u32 = 61;
|
||||
const KEY_F4: u32 = 62;
|
||||
const KEY_F5: u32 = 63;
|
||||
const KEY_F6: u32 = 64;
|
||||
const KEY_F7: u32 = 65;
|
||||
const KEY_F8: u32 = 66;
|
||||
const KEY_F9: u32 = 67;
|
||||
const KEY_F10: u32 = 68;
|
||||
const KEY_NUMLOCK: u32 = 69;
|
||||
const KEY_SCROLLLOCK: u32 = 70;
|
||||
const KEY_KP7: u32 = 71;
|
||||
const KEY_KP8: u32 = 72;
|
||||
const KEY_KP9: u32 = 73;
|
||||
const KEY_KPMINUS: u32 = 74;
|
||||
const KEY_KP4: u32 = 75;
|
||||
const KEY_KP5: u32 = 76;
|
||||
const KEY_KP6: u32 = 77;
|
||||
const KEY_KPPLUS: u32 = 78;
|
||||
const KEY_KP1: u32 = 79;
|
||||
const KEY_KP2: u32 = 80;
|
||||
const KEY_KP3: u32 = 81;
|
||||
const KEY_KP0: u32 = 82;
|
||||
const KEY_KPDOT: u32 = 83;
|
||||
const KEY_ZENKAKUHANKAKU: u32 = 85;
|
||||
const KEY_102ND: u32 = 86;
|
||||
const KEY_F11: u32 = 87;
|
||||
const KEY_F12: u32 = 88;
|
||||
const KEY_RO: u32 = 89;
|
||||
const KEY_KATAKANA: u32 = 90;
|
||||
const KEY_HIRAGANA: u32 = 91;
|
||||
const KEY_HENKAN: u32 = 92;
|
||||
const KEY_KATAKANAHIRAGANA: u32 = 93;
|
||||
const KEY_MUHENKAN: u32 = 94;
|
||||
const KEY_KPJPCOMMA: u32 = 95;
|
||||
const KEY_KPENTER: u32 = 96;
|
||||
const KEY_RIGHTCTRL: u32 = 97;
|
||||
const KEY_KPSLASH: u32 = 98;
|
||||
const KEY_SYSRQ: u32 = 99;
|
||||
const KEY_RIGHTALT: u32 = 100;
|
||||
const KEY_LINEFEED: u32 = 101;
|
||||
const KEY_HOME: u32 = 102;
|
||||
const KEY_UP: u32 = 103;
|
||||
const KEY_PAGEUP: u32 = 104;
|
||||
const KEY_LEFT: u32 = 105;
|
||||
const KEY_RIGHT: u32 = 106;
|
||||
const KEY_END: u32 = 107;
|
||||
const KEY_DOWN: u32 = 108;
|
||||
const KEY_PAGEDOWN: u32 = 109;
|
||||
const KEY_INSERT: u32 = 110;
|
||||
const KEY_DELETE: u32 = 111;
|
||||
const KEY_MACRO: u32 = 112;
|
||||
const KEY_MUTE: u32 = 113;
|
||||
const KEY_VOLUMEDOWN: u32 = 114;
|
||||
const KEY_VOLUMEUP: u32 = 115;
|
||||
const KEY_POWER : u32 = 116 /* SC System Power Down */;
|
||||
const KEY_KPEQUAL: u32 = 117;
|
||||
const KEY_KPPLUSMINUS: u32 = 118;
|
||||
const KEY_PAUSE: u32 = 119;
|
||||
const KEY_SCALE : u32 = 120 /* AL Compiz Scale (Expose) */;
|
||||
const KEY_KPCOMMA: u32 = 121;
|
||||
const KEY_HANGEUL: u32 = 122;
|
||||
const KEY_HANGUEL: u32 = KEY_HANGEUL;
|
||||
const KEY_HANJA: u32 = 123;
|
||||
const KEY_YEN: u32 = 124;
|
||||
const KEY_LEFTMETA: u32 = 125;
|
||||
const KEY_RIGHTMETA: u32 = 126;
|
||||
const KEY_COMPOSE: u32 = 127;
|
||||
const KEY_STOP : u32 = 128 /* AC Stop */;
|
||||
const KEY_AGAIN: u32 = 129;
|
||||
const KEY_PROPS : u32 = 130 /* AC Properties */;
|
||||
const KEY_UNDO : u32 = 131 /* AC Undo */;
|
||||
const KEY_FRONT: u32 = 132;
|
||||
const KEY_COPY : u32 = 133 /* AC Copy */;
|
||||
const KEY_OPEN : u32 = 134 /* AC Open */;
|
||||
const KEY_PASTE : u32 = 135 /* AC Paste */;
|
||||
const KEY_FIND : u32 = 136 /* AC Search */;
|
||||
const KEY_CUT : u32 = 137 /* AC Cut */;
|
||||
const KEY_HELP : u32 = 138 /* AL Integrated Help Center */;
|
||||
const KEY_MENU : u32 = 139 /* Menu (show menu) */;
|
||||
const KEY_CALC : u32 = 140 /* AL Calculator */;
|
||||
const KEY_SETUP: u32 = 141;
|
||||
const KEY_SLEEP : u32 = 142 /* SC System Sleep */;
|
||||
const KEY_WAKEUP : u32 = 143 /* System Wake Up */;
|
||||
const KEY_FILE : u32 = 144 /* AL Local Machine Browser */;
|
||||
const KEY_SENDFILE: u32 = 145;
|
||||
const KEY_DELETEFILE: u32 = 146;
|
||||
const KEY_XFER: u32 = 147;
|
||||
const KEY_PROG1: u32 = 148;
|
||||
const KEY_PROG2: u32 = 149;
|
||||
const KEY_WWW : u32 = 150 /* AL Internet Browser */;
|
||||
const KEY_MSDOS: u32 = 151;
|
||||
const KEY_COFFEE : u32 = 152 /* AL Terminal Lock/Screensaver */;
|
||||
const KEY_SCREENLOCK: u32 = KEY_COFFEE;
|
||||
const KEY_ROTATE_DISPLAY : u32 = 153 /* Display orientation for e.g. tablets */;
|
||||
const KEY_DIRECTION: u32 = KEY_ROTATE_DISPLAY;
|
||||
const KEY_CYCLEWINDOWS: u32 = 154;
|
||||
const KEY_MAIL: u32 = 155;
|
||||
const KEY_BOOKMARKS : u32 = 156 /* AC Bookmarks */;
|
||||
const KEY_COMPUTER: u32 = 157;
|
||||
const KEY_BACK : u32 = 158 /* AC Back */;
|
||||
const KEY_FORWARD : u32 = 159 /* AC Forward */;
|
||||
const KEY_CLOSECD: u32 = 160;
|
||||
const KEY_EJECTCD: u32 = 161;
|
||||
const KEY_EJECTCLOSECD: u32 = 162;
|
||||
const KEY_NEXTSONG: u32 = 163;
|
||||
const KEY_PLAYPAUSE: u32 = 164;
|
||||
const KEY_PREVIOUSSONG: u32 = 165;
|
||||
const KEY_STOPCD: u32 = 166;
|
||||
const KEY_RECORD: u32 = 167;
|
||||
const KEY_REWIND: u32 = 168;
|
||||
const KEY_PHONE : u32 = 169 /* Media Select Telephone */;
|
||||
const KEY_ISO: u32 = 170;
|
||||
const KEY_CONFIG : u32 = 171 /* AL Consumer Control Configuration */;
|
||||
const KEY_HOMEPAGE : u32 = 172 /* AC Home */;
|
||||
const KEY_REFRESH : u32 = 173 /* AC Refresh */;
|
||||
const KEY_EXIT : u32 = 174 /* AC Exit */;
|
||||
const KEY_MOVE: u32 = 175;
|
||||
const KEY_EDIT: u32 = 176;
|
||||
const KEY_SCROLLUP: u32 = 177;
|
||||
const KEY_SCROLLDOWN: u32 = 178;
|
||||
const KEY_KPLEFTPAREN: u32 = 179;
|
||||
const KEY_KPRIGHTPAREN: u32 = 180;
|
||||
const KEY_NEW : u32 = 181 /* AC New */;
|
||||
const KEY_REDO : u32 = 182 /* AC Redo/Repeat */;
|
||||
const KEY_F13: u32 = 183;
|
||||
const KEY_F14: u32 = 184;
|
||||
const KEY_F15: u32 = 185;
|
||||
const KEY_F16: u32 = 186;
|
||||
const KEY_F17: u32 = 187;
|
||||
const KEY_F18: u32 = 188;
|
||||
const KEY_F19: u32 = 189;
|
||||
const KEY_F20: u32 = 190;
|
||||
const KEY_F21: u32 = 191;
|
||||
const KEY_F22: u32 = 192;
|
||||
const KEY_F23: u32 = 193;
|
||||
const KEY_F24: u32 = 194;
|
||||
const KEY_PLAYCD: u32 = 200;
|
||||
const KEY_PAUSECD: u32 = 201;
|
||||
const KEY_PROG3: u32 = 202;
|
||||
const KEY_PROG4: u32 = 203;
|
||||
const KEY_ALL_APPLICATIONS : u32 = 204 /* AC Desktop Show All Applications */;
|
||||
const KEY_DASHBOARD: u32 = KEY_ALL_APPLICATIONS;
|
||||
const KEY_SUSPEND: u32 = 205;
|
||||
const KEY_CLOSE : u32 = 206 /* AC Close */;
|
||||
const KEY_PLAY: u32 = 207;
|
||||
const KEY_FASTFORWARD: u32 = 208;
|
||||
const KEY_BASSBOOST: u32 = 209;
|
||||
const KEY_PRINT : u32 = 210 /* AC Print */;
|
||||
const KEY_HP: u32 = 211;
|
||||
const KEY_CAMERA: u32 = 212;
|
||||
const KEY_SOUND: u32 = 213;
|
||||
const KEY_QUESTION: u32 = 214;
|
||||
const KEY_EMAIL: u32 = 215;
|
||||
const KEY_CHAT: u32 = 216;
|
||||
const KEY_SEARCH: u32 = 217;
|
||||
const KEY_CONNECT: u32 = 218;
|
||||
const KEY_FINANCE : u32 = 219 /* AL Checkbook/Finance */;
|
||||
const KEY_SPORT: u32 = 220;
|
||||
const KEY_SHOP: u32 = 221;
|
||||
const KEY_ALTERASE: u32 = 222;
|
||||
const KEY_CANCEL : u32 = 223 /* AC Cancel */;
|
||||
const KEY_BRIGHTNESSDOWN: u32 = 224;
|
||||
const KEY_BRIGHTNESSUP: u32 = 225;
|
||||
const KEY_MEDIA: u32 = 226;
|
||||
const KEY_SWITCHVIDEOMODE : u32 = 227 /* Cycle between available video outputs (Monitor/LCD/TV-out/etc) */;
|
||||
const KEY_KBDILLUMTOGGLE: u32 = 228;
|
||||
const KEY_KBDILLUMDOWN: u32 = 229;
|
||||
const KEY_KBDILLUMUP: u32 = 230;
|
||||
const KEY_SEND : u32 = 231 /* AC Send */;
|
||||
const KEY_REPLY : u32 = 232 /* AC Reply */;
|
||||
const KEY_FORWARDMAIL : u32 = 233 /* AC Forward Msg */;
|
||||
const KEY_SAVE : u32 = 234 /* AC Save */;
|
||||
const KEY_DOCUMENTS: u32 = 235;
|
||||
const KEY_BATTERY: u32 = 236;
|
||||
const KEY_BLUETOOTH: u32 = 237;
|
||||
const KEY_WLAN: u32 = 238;
|
||||
const KEY_UWB: u32 = 239;
|
||||
const KEY_UNKNOWN: u32 = 240;
|
||||
const KEY_VIDEO_NEXT : u32 = 241 /* drive next video source */;
|
||||
const KEY_VIDEO_PREV : u32 = 242 /* drive previous video source */;
|
||||
const KEY_BRIGHTNESS_CYCLE : u32 = 243 /* brightness up, after max is min */;
|
||||
const KEY_BRIGHTNESS_AUTO : u32 = 244 /* Set Auto Brightness: manual brightness control is off, rely on ambient */;
|
||||
const KEY_BRIGHTNESS_ZERO: u32 = KEY_BRIGHTNESS_AUTO;
|
||||
const KEY_DISPLAY_OFF : u32 = 245 /* display device to off state */;
|
||||
const KEY_WWAN : u32 = 246 /* Wireless WAN (LTE, UMTS, GSM, etc.) */;
|
||||
const KEY_WIMAX: u32 = KEY_WWAN;
|
||||
const KEY_RFKILL : u32 = 247 /* Key that controls all radios */;
|
||||
const KEY_MICMUTE : u32 = 248 /* Mute / unmute the microphone */;
|
||||
|
||||
pub const KEYMAP: &str = "xkb_keymap {}";
|
||||
2
crates/cursor-mover-webapp/src/server/mod.rs
Normal file
2
crates/cursor-mover-webapp/src/server/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod input_proxy_service;
|
||||
pub mod keymap;
|
||||
1
crates/cursor-mover-webapp/src/utils/mod.rs
Normal file
1
crates/cursor-mover-webapp/src/utils/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod mouse_filter_buffer;
|
||||
78
crates/cursor-mover-webapp/src/utils/mouse_filter_buffer.rs
Normal file
78
crates/cursor-mover-webapp/src/utils/mouse_filter_buffer.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use dioxus_html::geometry::{
|
||||
ElementSpace,
|
||||
euclid::{Point2D, Vector2D},
|
||||
};
|
||||
use wasmtimer::std::SystemTime;
|
||||
|
||||
pub struct MouseFilterBufferEntry {
|
||||
pub position: Point2D<f64, ElementSpace>,
|
||||
pub time: SystemTime,
|
||||
}
|
||||
|
||||
pub struct MouseFilterBuffer {
|
||||
buffer: VecDeque<MouseFilterBufferEntry>,
|
||||
}
|
||||
|
||||
impl MouseFilterBuffer {
|
||||
pub fn new(
|
||||
size: usize,
|
||||
initial: Point2D<f64, ElementSpace>,
|
||||
initial_timestamp: SystemTime,
|
||||
) -> Self {
|
||||
let mut buffer = VecDeque::with_capacity(size);
|
||||
buffer.push_back(MouseFilterBufferEntry {
|
||||
position: initial,
|
||||
time: initial_timestamp,
|
||||
});
|
||||
Self { buffer }
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&mut self,
|
||||
point: Point2D<f64, ElementSpace>,
|
||||
time: SystemTime,
|
||||
) {
|
||||
if self.buffer.len() == self.buffer.capacity() {
|
||||
self.buffer.pop_front();
|
||||
}
|
||||
self.buffer.push_back(MouseFilterBufferEntry {
|
||||
position: point,
|
||||
time,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn back(&self) -> &MouseFilterBufferEntry {
|
||||
self.buffer
|
||||
.back()
|
||||
.expect("Mouse filter buffer may never have less than one element")
|
||||
}
|
||||
|
||||
pub fn average_velocity_since(
|
||||
&self,
|
||||
start_time: SystemTime,
|
||||
) -> Vector2D<f64, ElementSpace> {
|
||||
let mut total_distance = Vector2D::zero();
|
||||
|
||||
let mut i = self.buffer.iter();
|
||||
|
||||
let Some(mut last) = i.find(|it| it.time >= start_time) else {
|
||||
return Vector2D::zero();
|
||||
};
|
||||
let start_time = last.time;
|
||||
let mut last_time = last.time;
|
||||
|
||||
for point in i {
|
||||
total_distance += point.position - last.position;
|
||||
last = point;
|
||||
last_time = point.time;
|
||||
}
|
||||
|
||||
if total_distance == Vector2D::zero() {
|
||||
return Vector2D::zero();
|
||||
}
|
||||
|
||||
total_distance / (last_time.duration_since(start_time).unwrap().as_secs_f64())
|
||||
}
|
||||
}
|
||||
16
crates/cursor-mover-webapp/src/views/home.rs
Normal file
16
crates/cursor-mover-webapp/src/views/home.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use crate::components::Controls;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// The Home page component that will be rendered when the current route is `[Route::Home]`
|
||||
#[component]
|
||||
pub fn Home() -> Element {
|
||||
#[css_module("/assets/styling/home.module.css")]
|
||||
struct Styles;
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: Styles::container,
|
||||
Controls { }
|
||||
}
|
||||
}
|
||||
}
|
||||
15
crates/cursor-mover-webapp/src/views/mod.rs
Normal file
15
crates/cursor-mover-webapp/src/views/mod.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//! The views module contains the components for all Layouts and Routes for our app. Each layout and route in our [`Route`]
|
||||
//! enum will render one of these components.
|
||||
//!
|
||||
//!
|
||||
//! The [`Home`] and [`Blog`] components will be rendered when the current route is [`Route::Home`] or [`Route::Blog`] respectively.
|
||||
//!
|
||||
//!
|
||||
//! The [`Navbar`] component will be rendered on all pages of our app since every page is under the layout. The layout defines
|
||||
//! a common wrapper around all child routes.
|
||||
|
||||
mod home;
|
||||
pub use home::Home;
|
||||
|
||||
mod navbar;
|
||||
pub use navbar::Navbar;
|
||||
22
crates/cursor-mover-webapp/src/views/navbar.rs
Normal file
22
crates/cursor-mover-webapp/src/views/navbar.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use crate::Route;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Navbar() -> Element {
|
||||
#[css_module("/assets/styling/navbar.module.css")]
|
||||
struct Styles;
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: Styles::navbar,
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"Home"
|
||||
}
|
||||
}
|
||||
|
||||
// The `Outlet` component is used to render the next component inside the layout. In this case, it will render either
|
||||
// the [`Home`] or [`Blog`] component depending on the current route.
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue