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

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