From ec1a4e4205920df9fd85d780d9faeabb4d331b14 Mon Sep 17 00:00:00 2001
From: galister <22305755+galister@users.noreply.github.com>
Date: Sun, 21 Dec 2025 22:16:02 +0900
Subject: [PATCH] left, right & middle-specific press-release actions on
buttons
---
wlx-overlay-s/src/gui/README.md | 5 +
wlx-overlay-s/src/gui/panel/button.rs | 144 +++++++++++++++++++++----
wlx-overlay-s/src/overlays/edit/mod.rs | 50 +++++++--
wlx-overlay-s/src/overlays/watch.rs | 34 ++++--
4 files changed, 199 insertions(+), 34 deletions(-)
diff --git a/wlx-overlay-s/src/gui/README.md b/wlx-overlay-s/src/gui/README.md
index d50955a3..029b1f2b 100644
--- a/wlx-overlay-s/src/gui/README.md
+++ b/wlx-overlay-s/src/gui/README.md
@@ -81,6 +81,11 @@ Supported events:
```
+Laser-color-specific variants are also available:
+- `_press_left` & `_release_left` for blue laser
+- `_press_right` & `_release_right` for orange laser
+- `_press_middle` & `_release_middle` for purple laser
+
#### Supported button actions
##### `::ShellExec [args ..]`
diff --git a/wlx-overlay-s/src/gui/panel/button.rs b/wlx-overlay-s/src/gui/panel/button.rs
index 9fa2adc6..6ea7d731 100644
--- a/wlx-overlay-s/src/gui/panel/button.rs
+++ b/wlx-overlay-s/src/gui/panel/button.rs
@@ -10,7 +10,9 @@ use std::{
use anyhow::Context;
use wgui::{
components::button::ComponentButton,
- event::{self, EventCallback, EventListenerKind},
+ event::{
+ self, CallbackData, CallbackMetadata, EventCallback, EventListenerKind, MouseButtonIndex,
+ },
i18n::Translation,
layout::Layout,
parser::CustomAttribsInfoOwned,
@@ -31,18 +33,72 @@ use crate::{
#[cfg(feature = "wayvr")]
use crate::backend::wayvr::WayVRAction;
-pub const BUTTON_EVENTS: [(&str, EventListenerKind); 2] = [
- ("_press", EventListenerKind::MousePress),
- ("_release", EventListenerKind::MouseRelease),
+pub const BUTTON_EVENTS: [(&str, EventListenerKind, fn(&mut CallbackData) -> bool); 8] = [
+ ("_press", EventListenerKind::MousePress, any_button),
+ ("_release", EventListenerKind::MouseRelease, any_button),
+ ("_press_left", EventListenerKind::MousePress, left_button),
+ (
+ "_release_left",
+ EventListenerKind::MouseRelease,
+ left_button,
+ ),
+ ("_press_right", EventListenerKind::MousePress, right_button),
+ (
+ "_release_right",
+ EventListenerKind::MouseRelease,
+ right_button,
+ ),
+ (
+ "_press_middle",
+ EventListenerKind::MousePress,
+ middle_button,
+ ),
+ (
+ "_release_middle",
+ EventListenerKind::MouseRelease,
+ middle_button,
+ ),
];
+fn any_button(_: &mut CallbackData) -> bool {
+ true
+}
+
+fn left_button(data: &mut CallbackData) -> bool {
+ if let CallbackMetadata::MouseButton(b) = data.metadata
+ && let MouseButtonIndex::Left = b.index
+ {
+ true
+ } else {
+ false
+ }
+}
+fn right_button(data: &mut CallbackData) -> bool {
+ if let CallbackMetadata::MouseButton(b) = data.metadata
+ && let MouseButtonIndex::Right = b.index
+ {
+ true
+ } else {
+ false
+ }
+}
+fn middle_button(data: &mut CallbackData) -> bool {
+ if let CallbackMetadata::MouseButton(b) = data.metadata
+ && let MouseButtonIndex::Middle = b.index
+ {
+ true
+ } else {
+ false
+ }
+}
+
pub(super) fn setup_custom_button(
layout: &mut Layout,
attribs: &CustomAttribsInfoOwned,
_app: &AppState,
button: Rc,
) {
- for (name, kind) in &BUTTON_EVENTS {
+ for (name, kind, test_btn) in &BUTTON_EVENTS {
let Some(action) = attribs.get_value(name) else {
continue;
};
@@ -54,7 +110,11 @@ pub(super) fn setup_custom_button(
let callback: EventCallback = match command {
#[cfg(feature = "wayvr")]
- "::DashToggle" => Box::new(move |_common, _data, app, _| {
+ "::DashToggle" => Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks
.enqueue(TaskType::WayVR(WayVRAction::ToggleDashboard));
Ok(EventResult::Consumed)
@@ -65,7 +125,11 @@ pub(super) fn setup_custom_button(
log::error!("{command} has invalid argument: \"{arg}\"");
return;
};
- Box::new(move |_common, _data, app, _| {
+ Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::ToggleSet(set_idx)));
Ok(EventResult::Consumed)
@@ -77,7 +141,11 @@ pub(super) fn setup_custom_button(
return;
};
- Box::new(move |_common, _data, app, _| {
+ Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(arg.clone()),
Box::new(move |app, owc| {
@@ -91,13 +159,21 @@ pub(super) fn setup_custom_button(
Ok(EventResult::Consumed)
})
}
- "::EditToggle" => Box::new(move |_common, _data, app, _| {
+ "::EditToggle" => Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::ToggleEditMode));
Ok(EventResult::Consumed)
}),
#[cfg(feature = "wayland")]
- "::NewMirror" => Box::new(move |_common, _data, app, _| {
+ "::NewMirror" => Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let name = crate::overlays::mirror::new_mirror_name();
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Create(
OverlaySelector::Name(name.clone()),
@@ -107,21 +183,37 @@ pub(super) fn setup_custom_button(
)));
Ok(EventResult::Consumed)
}),
- "::CleanupMirrors" => Box::new(move |_common, _data, app, _| {
+ "::CleanupMirrors" => Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::CleanupMirrors));
Ok(EventResult::Consumed)
}),
- "::PlayspaceReset" => Box::new(move |_common, _data, app, _| {
+ "::PlayspaceReset" => Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks.enqueue(TaskType::Playspace(PlayspaceTask::Reset));
Ok(EventResult::Consumed)
}),
- "::PlayspaceRecenter" => Box::new(move |_common, _data, app, _| {
+ "::PlayspaceRecenter" => Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks
.enqueue(TaskType::Playspace(PlayspaceTask::Recenter));
Ok(EventResult::Consumed)
}),
- "::PlayspaceFixFloor" => Box::new(move |_common, _data, app, _| {
+ "::PlayspaceFixFloor" => Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
for i in 0..5 {
Toast::new(
ToastTopic::System,
@@ -138,7 +230,11 @@ pub(super) fn setup_custom_button(
);
Ok(EventResult::Consumed)
}),
- "::Shutdown" => Box::new(move |_common, _data, _app, _| {
+ "::Shutdown" => Box::new(move |_common, data, _app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
RUNNING.store(false, Ordering::Relaxed);
Ok(EventResult::Consumed)
}),
@@ -155,7 +251,11 @@ pub(super) fn setup_custom_button(
log::error!("{command} has bad/missing arguments");
return;
};
- Box::new(move |_common, _data, app, _| {
+ Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.hid_provider.send_key_routed(key, down);
Ok(EventResult::Consumed)
})
@@ -182,7 +282,11 @@ pub(super) fn setup_custom_button(
}),
);
- Box::new(move |_common, _data, _app, _| {
+ Box::new(move |_common, data, _app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let _ = shell_on_action(&state).inspect_err(|e| log::error!("{e:?}"));
Ok(EventResult::Consumed)
})
@@ -206,7 +310,11 @@ pub(super) fn setup_custom_button(
osc_args.push(osc_arg);
}
- Box::new(move |_common, _data, app, _| {
+ Box::new(move |_common, data, app, _| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let Some(sender) = app.osc_sender.as_mut() else {
log::error!("OscSend: sender is not available.");
return Ok(EventResult::Consumed);
diff --git a/wlx-overlay-s/src/overlays/edit/mod.rs b/wlx-overlay-s/src/overlays/edit/mod.rs
index 41758527..3694e1d7 100644
--- a/wlx-overlay-s/src/overlays/edit/mod.rs
+++ b/wlx-overlay-s/src/overlays/edit/mod.rs
@@ -283,7 +283,7 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result {
};
let on_custom_attrib: OnCustomAttribFunc = Box::new(move |layout, attribs, _app| {
- for (name, kind) in &BUTTON_EVENTS {
+ for (name, kind, test_btn) in &BUTTON_EVENTS {
let Some(action) = attribs.get_value(name) else {
continue;
};
@@ -294,14 +294,22 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result {
};
let callback: EventCallback = match command {
- "::EditModeToggleLock" => Box::new(move |common, _data, app, state| {
+ "::EditModeToggleLock" => Box::new(move |common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let sel = OverlaySelector::Id(*state.id.borrow());
let task = state.lock.toggle(common, app);
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::Modify(sel, task)));
Ok(EventResult::Consumed)
}),
- "::EditModeToggleGrab" => Box::new(move |_common, _data, app, state| {
+ "::EditModeToggleGrab" => Box::new(move |_common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let sel = OverlaySelector::Id(*state.id.borrow());
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
sel,
@@ -314,14 +322,22 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result {
}),
"::EditModeTab" => {
let tab_name = args.next().unwrap().to_owned();
- Box::new(move |common, _data, _app, state| {
+ Box::new(move |common, data, _app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
state.tabs.tab_button_clicked(common, &tab_name);
Ok(EventResult::Consumed)
})
}
"::EditModeSetPos" => {
let key = args.next().unwrap().to_owned();
- Box::new(move |common, _data, app, state| {
+ Box::new(move |common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let sel = OverlaySelector::Id(*state.id.borrow());
let task = state.pos.button_clicked(common, &key);
app.tasks
@@ -331,7 +347,11 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result {
}
"::EditModeSetStereo" => {
let key = args.next().unwrap().to_owned();
- Box::new(move |common, _data, app, state| {
+ Box::new(move |common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let sel = OverlaySelector::Id(*state.id.borrow());
let task = state.stereo.button_clicked(common, &key);
app.tasks
@@ -341,7 +361,11 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result {
}
"::EditModeSetMouse" => {
let key = args.next().unwrap().to_owned();
- Box::new(move |common, _data, app, state| {
+ Box::new(move |common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let sel = OverlaySelector::Id(*state.id.borrow());
let task = state.mouse.button_clicked(common, &key);
app.tasks
@@ -349,12 +373,20 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result {
Ok(EventResult::Consumed)
})
}
- "::EditModeDeletePress" => Box::new(move |_common, _data, _app, state| {
+ "::EditModeDeletePress" => Box::new(move |_common, data, _app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
state.delete.pressed = Instant::now();
// TODO: animate to light up button after 2s
Ok(EventResult::Consumed)
}),
- "::EditModeDeleteRelease" => Box::new(move |_common, _data, app, state| {
+ "::EditModeDeleteRelease" => Box::new(move |_common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
if state.delete.pressed.elapsed() < Duration::from_secs(1) {
return Ok(EventResult::Pass);
}
diff --git a/wlx-overlay-s/src/overlays/watch.rs b/wlx-overlay-s/src/overlays/watch.rs
index cc351d1d..20955058 100644
--- a/wlx-overlay-s/src/overlays/watch.rs
+++ b/wlx-overlay-s/src/overlays/watch.rs
@@ -5,7 +5,7 @@ use std::{
};
use glam::{Affine3A, Quat, Vec3, Vec3A, vec3};
-use idmap::{DirectIdMap, ordered::Keys};
+use idmap::DirectIdMap;
use slotmap::SecondaryMap;
use wgui::{
components::button::ComponentButton,
@@ -79,7 +79,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result {
let state = WatchState::default();
let on_custom_attrib: OnCustomAttribFunc = Box::new(move |layout, attribs, _app| {
- for (name, kind) in &BUTTON_EVENTS {
+ for (name, kind, test_btn) in &BUTTON_EVENTS {
let Some(action) = attribs.get_value(name) else {
continue;
};
@@ -90,11 +90,19 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result {
};
let callback: EventCallback = match command {
- "::EditModeDeleteDown" => Box::new(move |_common, _data, _app, state| {
+ "::EditModeDeleteDown" => Box::new(move |_common, data, _app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
state.delete.pressed = Instant::now();
Ok(EventResult::Consumed)
}),
- "::EditModeDeleteUp" => Box::new(move |_common, _data, app, state| {
+ "::EditModeDeleteUp" => Box::new(move |_common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
if state.delete.pressed.elapsed() < Duration::from_secs(1) {
return Ok(EventResult::Consumed);
}
@@ -102,7 +110,11 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result {
.enqueue(TaskType::Overlay(OverlayTask::DeleteActiveSet));
Ok(EventResult::Consumed)
}),
- "::EditModeAddSet" => Box::new(move |_common, _data, app, _state| {
+ "::EditModeAddSet" => Box::new(move |_common, data, app, _state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
app.tasks.enqueue(TaskType::Overlay(OverlayTask::AddSet));
Ok(EventResult::Consumed)
}),
@@ -112,7 +124,11 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result {
log::error!("{command} has invalid argument: \"{arg}\"");
return;
};
- Box::new(move |_common, _data, app, state| {
+ Box::new(move |_common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let Some(overlay) = state.overlay_metas.get(idx) else {
log::error!("No overlay at index {idx}.");
return Ok(EventResult::Consumed);
@@ -137,7 +153,11 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result {
log::error!("{command} has invalid argument: \"{arg}\"");
return;
};
- Box::new(move |_common, _data, app, state| {
+ Box::new(move |_common, data, app, state| {
+ if !test_btn(data) {
+ return Ok(EventResult::Pass);
+ }
+
let Some(overlay) = state.overlay_metas.get(idx) else {
log::error!("No overlay at index {idx}.");
return Ok(EventResult::Consumed);