basic cursor moving

This commit is contained in:
Mona Mayrhofer 2026-02-28 22:24:42 +01:00
parent e4d51f09a1
commit 966fdbbd50
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
29 changed files with 7052 additions and 52 deletions

7
crates/cursor-move-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,29 @@
[package]
name = "cursor-move-webapp"
version = "0.1.0"
authors = ["Mona Mayrhofer <mona.mayrhofer@proton.me>"]
edition = "2024"
# 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"] }
serde = { version = "1.0.228", features = ["derive"] }
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}
[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:tokio"]
[lints]
workspace = true

View file

@ -0,0 +1,21 @@
[application]
[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 = []

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: 130 KiB

View file

@ -0,0 +1,11 @@
.container {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
& > * {
flex-grow: 1;
}
}

View file

@ -0,0 +1,14 @@
html,
body,
#main {
margin: 0;
padding: 0;
min-height: 100vh;
width: 100vw;
}
#main {
background-color: #214;
display: flex;
flex-direction: column;
}

View file

@ -0,0 +1,5 @@
.mouse-area {
background-color: white;
touch-action: none;
}

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,2 @@
mod mouse_area;
pub use mouse_area::MouseArea;

View file

@ -0,0 +1,79 @@
use dioxus::{
fullstack::{CborEncoding, WebSocketOptions, Websocket, extract::State, use_websocket},
html::geometry::{ElementSpace, euclid::Point2D},
logger::tracing,
prelude::*,
};
use serde::{Deserialize, Serialize};
#[component]
pub fn MouseArea() -> Element {
#[css_module("/assets/styling/mouse_area.module.css")]
struct Styles;
let mut last_cursor_position = use_signal::<Option<Point2D<f64, ElementSpace>>>(|| None);
let mut socket = use_websocket(move || mouse_move(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);
}
}
});
rsx! {
div {
class: Styles::mouse_area,
onpointermove: move |evt| {
evt.prevent_default();
let point = evt.element_coordinates();
let last_position = last_cursor_position.write().replace(point);
if let Some(last_position) = last_position {
let delta = point - last_position;
spawn(async move {
_ = socket.send(ClientEvent::MouseMove { dx: delta.x, dy: delta.y }).await;
});
}
},
onpointerdown: move |evt| {
let point = evt.element_coordinates();
*last_cursor_position.write() = Some(point);
}
}
}
}
#[derive(Serialize, Deserialize, Debug)]
enum ClientEvent {
MouseMove { dx: f64, dy: f64 },
}
#[derive(Serialize, Deserialize, Debug)]
enum ServerEvent {
Ping,
}
#[expect(clippy::unused_async)]
#[get("/api/mouse_move_ws", mouse_service: State<crate::server::mouse_service::MouseService>)]
async fn mouse_move(
options: WebSocketOptions
) -> Result<Websocket<ClientEvent, ServerEvent, CborEncoding>> {
Ok(options.on_upgrade(move |mut socket| async move {
_ = socket.send(ServerEvent::Ping).await;
while let Ok(ClientEvent::MouseMove { dx, dy }) = socket.recv().await {
mouse_service.move_mouse(dx, dy).await;
}
}))
}

View file

@ -0,0 +1,51 @@
#![expect(clippy::volatile_composites)]
use dioxus::prelude::*;
use views::{Home, Navbar};
mod components;
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::mouse_service::MouseService;
use dioxus::server::axum::Extension;
let router = dioxus::server::router(App);
let router = router.layer(Extension(MouseService::start()));
Ok(router)
});
}
#[component]
fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
Router::<Route> {}
}
}

View file

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

View file

@ -0,0 +1,143 @@
use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use dioxus::{
fullstack::{FullstackContext, extract::FromRef},
logger::tracing,
};
use tokio::sync::Mutex;
use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle, protocol::wl_registry};
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
};
#[derive(Clone)]
pub struct MouseService {
input_proxy_service_state: Arc<Mutex<InputProxy>>,
}
impl MouseService {
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 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
pointer.motion(time as u32, dx, dy);
pointer.frame();
guard.event_queue.flush().unwrap();
}
}
}
impl FromRef<FullstackContext> for MouseService {
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::default();
event_queue
.roundtrip(&mut input_proxy_service_state)
.unwrap();
Self {
state: input_proxy_service_state,
event_queue,
}
}
}
#[derive(Default)]
struct InputProxyServiceState {
virtual_pointer: Option<ZwlrVirtualPointerV1>,
}
impl Dispatch<ZwlrVirtualPointerV1, ()> for InputProxyServiceState {
fn event(
_state: &mut Self,
_proxy: &ZwlrVirtualPointerV1,
_event: <ZwlrVirtualPointerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
tracing::info!("VPointerData");
}
}
impl Dispatch<ZwlrVirtualPointerManagerV1, ()> for InputProxyServiceState {
fn event(
_state: &mut Self,
_proxy: &ZwlrVirtualPointerManagerV1,
_event: <ZwlrVirtualPointerManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
tracing::info!("ZwlrEvent");
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for InputProxyServiceState {
fn event(
app_data: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_udata: &(),
_conn: &Connection,
queue_handle: &QueueHandle<Self>,
) {
println!("WlRegistry Event");
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
&& interface == "zwlr_virtual_pointer_manager_v1"
{
app_data.virtual_pointer.get_or_insert_with(|| {
let manager = registry.bind::<ZwlrVirtualPointerManagerV1, _, _>(
name,
version,
queue_handle,
(),
);
let pointer = manager.create_virtual_pointer(None, queue_handle, ());
println!("Virtual pointer manager created");
pointer
});
}
}
}

View file

@ -0,0 +1,16 @@
use crate::components::MouseArea;
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,
MouseArea { }
}
}
}

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

View file

@ -10,6 +10,7 @@ categories = ["web-programming"]
readme = "README.md"
[dependencies]
tokio = { version = "1.49.0", features = ["full"] }
wayland-client = "0.31.12"
wayland-protocols-wlr = { version = "0.3.10", features = ["client"] }

View file

@ -1,3 +1,14 @@
use std::{
alloc::System,
f64,
future::poll_fn,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::{
select,
time::{self, Instant},
};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, protocol::wl_registry};
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
@ -42,39 +53,35 @@ impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
event: wl_registry::Event,
_: &(),
_: &Connection,
queue_handle: &QueueHandle<AppData>,
queue_handle: &QueueHandle<Self>,
) {
// When receiving events from the wl_registry, we are only interested in the
// `global` event, which signals a new available global.
// When receiving this event, we just print its characteristics in this example.
println!("WlRegistry Event");
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
&& interface == "zwlr_virtual_pointer_manager_v1"
{
//println!("[{name}] {interface} (v{version})");
app_data.virtual_pointer.get_or_insert_with(|| {
let manager = registry.bind::<ZwlrVirtualPointerManagerV1, _, _>(
name,
version,
queue_handle,
(),
);
if interface.as_str() == "zwlr_virtual_pointer_manager_v1" {
app_data.virtual_pointer.get_or_insert_with(|| {
let manager = registry.bind::<ZwlrVirtualPointerManagerV1, _, _>(
name,
version,
queue_handle,
(),
);
let pointer = manager.create_virtual_pointer(None, queue_handle, ());
let pointer = manager.create_virtual_pointer(None, queue_handle, ());
println!("Virtual pointer manager created");
pointer
});
}
println!("Virtual pointer manager created");
pointer
});
}
}
}
fn main() {
#[tokio::main]
async fn main() {
let connection = Connection::connect_to_env().unwrap();
let display = connection.display();
@ -86,14 +93,41 @@ fn main() {
let mut appdata = AppData::default();
event_queue.roundtrip(&mut appdata).unwrap();
let mut interval = time::interval(Duration::from_millis(15));
let start = Instant::now();
loop {
let dispatched = event_queue.dispatch_pending(&mut appdata).unwrap();
if dispatched == 0 {
event_queue.flush().unwrap();
if Instant::now().duration_since(start) > Duration::from_secs(5) {
break;
}
if let Some(pointer) = &appdata.virtual_pointer {
println!("Moving pointer?")
select! {
poll = poll_fn(|cx| event_queue.poll_dispatch_pending(cx, &mut appdata)) => {
println!("Did the mash");
},
now = interval.tick() => {
if let Some(pointer) = appdata.virtual_pointer.as_mut() {
handle_pointer_motion(now, &pointer);
}
event_queue.flush().unwrap();
},
}
}
}
pub fn handle_pointer_motion(
time: Instant,
pointer: &ZwlrVirtualPointerV1,
) {
let time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let x = ((time as f64 / 1000.0 * f64::consts::PI).sin() * 10.0);
let y = ((time as f64 / 1000.0 * f64::consts::PI).cos() * 10.0);
pointer.motion(time as u32, x, y);
pointer.frame();
}