rename to cursor-mover-webapp

This commit is contained in:
Mona Mayrhofer 2026-03-07 18:56:41 +01:00
parent 201f0e5f0a
commit cc52e3e781
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: 374AB152BDEBA1AE
35 changed files with 7 additions and 7 deletions

7
crates/cursor-mover-webapp/.gitignore vendored Normal file
View 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

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

View 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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,3 @@
.controls {
flex-grow: 1;
}

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

View file

@ -0,0 +1,8 @@
.keyboard-area {
height: 48px;
input {
width: 100%;
height: 100%;
}
}

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

View file

@ -0,0 +1,7 @@
.mouse-area {
background: radial-gradient(#0004 15%, transparent 20%), white;
background-size: 15px 15px;
touch-action: none;
flex-grow: 1;
}

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

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

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,2 @@
pub mod input_proxy_service;
pub mod keymap;

View file

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

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

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

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

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