handsfree mode

This commit is contained in:
galister
2026-01-13 19:15:16 +09:00
parent 1318f23947
commit fd24060d7b
23 changed files with 434 additions and 105 deletions

View File

@@ -67,6 +67,9 @@
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds", "NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"OPAQUE_BACKGROUND": "Opaque background", "OPAQUE_BACKGROUND": "Opaque background",
"OPTION": { "OPTION": {
"NONE": "None",
"HMD_PINCH": "HMD + pinch",
"EYE_PINCH": "Eye + pinch",
"AUTO": "Automatic", "AUTO": "Automatic",
"AUTO_HELP": "ScreenCopy GPU if supported,\notherwise PipeWire GPU.", "AUTO_HELP": "ScreenCopy GPU if supported,\notherwise PipeWire GPU.",
"PIPEWIRE_HELP": "Fast GPU capture,\nstandard on all desktops.", "PIPEWIRE_HELP": "Fast GPU capture,\nstandard on all desktops.",
@@ -97,6 +100,8 @@
"XR_CLICK_SENSITIVITY_RELEASE": "XR release sensitivity", "XR_CLICK_SENSITIVITY_RELEASE": "XR release sensitivity",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Must be lower than click", "XR_CLICK_SENSITIVITY_RELEASE_HELP": "Must be lower than click",
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default", "XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default",
"HANDSFREE_POINTER": "Handsfree mode",
"HANDSFREE_POINTER_HELP": "Input to use when motion\ncontrollers are unavailable",
"AUTOSTART_APPS": "Apps to run on startup" "AUTOSTART_APPS": "Apps to run on startup"
}, },
"APPLICATION_LAUNCHER": "Application launcher", "APPLICATION_LAUNCHER": "Application launcher",

View File

@@ -224,6 +224,7 @@ enum SettingType {
XwaylandByDefault, XwaylandByDefault,
CaptureMethod, CaptureMethod,
KeyboardMiddleClick, KeyboardMiddleClick,
HandsfreePointer,
} }
impl SettingType { impl SettingType {
@@ -286,6 +287,9 @@ impl SettingType {
config.keyboard_middle_click_mode = config.keyboard_middle_click_mode =
wlx_common::config::AltModifier::from_str(value).expect("Invalid enum value!") wlx_common::config::AltModifier::from_str(value).expect("Invalid enum value!")
} }
Self::HandsfreePointer => {
config.handsfree_pointer = wlx_common::config::HandsfreePointer::from_str(value).expect("Invalid enum value!")
}
_ => panic!("Requested enum for non-enum SettingType"), _ => panic!("Requested enum for non-enum SettingType"),
} }
} }
@@ -294,6 +298,7 @@ impl SettingType {
match self { match self {
Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method), Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method),
Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode), Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode),
Self::HandsfreePointer => Self::get_enum_title_inner(config.handsfree_pointer),
_ => panic!("Requested enum for non-enum SettingType"), _ => panic!("Requested enum for non-enum SettingType"),
} }
} }
@@ -353,6 +358,7 @@ impl SettingType {
Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"), Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"),
Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"), Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"),
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"), Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
Self::HandsfreePointer => Ok("APP_SETTINGS.HANDSFREE_POINTER"),
} }
} }
@@ -371,6 +377,7 @@ impl SettingType {
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"), Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"), Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"),
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"), Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
Self::HandsfreePointer => Some("APP_SETTINGS.HANDSFREE_POINTER_HELP"),
_ => None, _ => None,
} }
} }
@@ -382,10 +389,10 @@ impl SettingType {
| Self::RoundMultiplier | Self::RoundMultiplier
| Self::UprightScreenFix | Self::UprightScreenFix
| Self::DoubleCursorFix | Self::DoubleCursorFix
| Self::SetsOnWatch
| Self::UseSkybox | Self::UseSkybox
| Self::UsePassthrough | Self::UsePassthrough
| Self::ScreenRenderDown => true, | Self::ScreenRenderDown
| Self::CaptureMethod => true,
_ => false, _ => false,
} }
} }
@@ -711,6 +718,12 @@ impl<T> TabSettings<T> {
SettingType::KeyboardMiddleClick, SettingType::KeyboardMiddleClick,
wlx_common::config::AltModifier::VARIANTS wlx_common::config::AltModifier::VARIANTS
); );
dropdown!(
mp,
c,
SettingType::HandsfreePointer,
wlx_common::config::HandsfreePointer::VARIANTS
);
checkbox!(mp, c, SettingType::FocusFollowsMouseMode); checkbox!(mp, c, SettingType::FocusFollowsMouseMode);
checkbox!(mp, c, SettingType::LeftHandedMouse); checkbox!(mp, c, SettingType::LeftHandedMouse);
checkbox!(mp, c, SettingType::AllowSliding); checkbox!(mp, c, SettingType::AllowSliding);

View File

@@ -156,7 +156,7 @@ impl View {
} else { } else {
CompositorMode::Native CompositorMode::Native
}; };
radio_compositor.set_value(compositor_mode.as_ref())?; radio_compositor.set_value_simple(compositor_mode.as_ref())?;
tasks.push(Task::SetCompositor(compositor_mode)); tasks.push(Task::SetCompositor(compositor_mode));
let res_mode = ResMode::Res1080; let res_mode = ResMode::Res1080;

View File

@@ -163,12 +163,21 @@
</Button> </Button>
</template> </template>
<blueprint name="menu_handsfree">
<context_menu >
<cell translation="BAR.HANDSFREE.NONE" _press="::HandsfreeMode None" />
<cell translation="BAR.HANDSFREE.HMD" _press="::HandsfreeMode Hmd" />
<cell translation="BAR.HANDSFREE.EYE_TRACKING" _press="::HandsfreeMode EyeTracking" />
</context_menu>
</blueprint>
<blueprint name="menu_burger"> <blueprint name="menu_burger">
<context_menu > <context_menu >
<cell translation="BAR.ADD_MIRROR" _press="::NewMirror" /> <cell translation="BAR.ADD_MIRROR" _press="::NewMirror" />
<cell translation="BAR.EDIT_MODE_TOGGLE" _press="::EditToggle" /> <cell translation="BAR.EDIT_MODE_TOGGLE" _press="::EditToggle" />
<cell translation="BAR.ADD_NEW_SET" _press="::AddSet" /> <cell translation="BAR.ADD_NEW_SET" _press="::AddSet" />
<cell translation="BAR.DELETE_CURRENT_SET" _press="::DeleteSet" /> <cell translation="BAR.DELETE_CURRENT_SET" _press="::DeleteSet" />
<cell translation="BAR.HANDSFREE.TITLE" _press="::ContextMenuOpen menu_handsfree" />
</context_menu> </context_menu>
</blueprint> </blueprint>

View File

@@ -12,7 +12,13 @@
"RELOAD_FROM_DISK": "Reload XML from disk", "RELOAD_FROM_DISK": "Reload XML from disk",
"CLOSE_MIRROR": "Close mirror", "CLOSE_MIRROR": "Close mirror",
"CLOSE_APP": "Close app", "CLOSE_APP": "Close app",
"FORCE_CLOSE_APP": "Force close app" "FORCE_CLOSE_APP": "Force close app",
"HANDSFREE": {
"TITLE": "Handsfree mode",
"NONE": "Off",
"HMD": "HMD + pinch",
"EYE_TRACKING": "Eye + pinch"
}
}, },
"DEFAULT": "Default", "DEFAULT": "Default",
"DISABLED": "Disabled", "DISABLED": "Disabled",

View File

@@ -3,7 +3,7 @@ use std::process::{Child, Command};
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use glam::{Affine3A, Vec2, Vec3A, Vec3Swizzles}; use glam::{Affine3A, Mat3A, Vec2, Vec3, Vec3A, Vec3Swizzles};
use idmap_derive::IntegerId; use idmap_derive::IntegerId;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
@@ -233,6 +233,8 @@ pub struct Pointer {
pub last_click: Instant, pub last_click: Instant,
pub pending_haptics: Option<Haptics>, pub pending_haptics: Option<Haptics>,
pub(super) interaction: InteractionState, pub(super) interaction: InteractionState,
pub tracked: bool,
pub handsfree: bool,
} }
impl Pointer { impl Pointer {
@@ -247,6 +249,8 @@ impl Pointer {
last_click: Instant::now(), last_click: Instant::now(),
pending_haptics: None, pending_haptics: None,
interaction: InteractionState::default(), interaction: InteractionState::default(),
tracked: false,
handsfree: false,
} }
} }
@@ -318,6 +322,47 @@ pub enum PointerMode {
Special, Special,
} }
pub struct PointerLine {
pub mode: PointerMode,
pub a: Vec3A,
pub b: Vec3A,
}
fn populate_lines(
lines: &mut Vec<PointerLine>,
pointer: &Pointer,
maybe_hit: Option<(PointerHit, RayHit)>,
hmd: &Affine3A,
) {
let Some((hit, raw_hit)) = maybe_hit else {
return;
};
if pointer.handsfree {
const HALF_SIZE: f32 = 0.01;
// horizontal arm
lines.push(PointerLine {
mode: PointerMode::Left,
a: raw_hit.global_pos - (hmd.x_axis * HALF_SIZE),
b: raw_hit.global_pos + (hmd.x_axis * HALF_SIZE),
});
// vertical arm
lines.push(PointerLine {
mode: PointerMode::Special,
a: raw_hit.global_pos - (hmd.y_axis * HALF_SIZE),
b: raw_hit.global_pos + (hmd.y_axis * HALF_SIZE),
});
} else {
lines.push(PointerLine {
mode: hit.mode,
a: pointer.pose.translation,
b: raw_hit.global_pos,
});
}
}
fn update_focus(focus: &mut KeyboardFocus, overlay_keyboard_focus: Option<KeyboardFocus>) { fn update_focus(focus: &mut KeyboardFocus, overlay_keyboard_focus: Option<KeyboardFocus>) {
if let Some(f) = &overlay_keyboard_focus if let Some(f) = &overlay_keyboard_focus
&& *focus != *f && *focus != *f
@@ -330,11 +375,12 @@ fn update_focus(focus: &mut KeyboardFocus, overlay_keyboard_focus: Option<Keyboa
pub fn interact<O>( pub fn interact<O>(
overlays: &mut OverlayWindowManager<O>, overlays: &mut OverlayWindowManager<O>,
app: &mut AppState, app: &mut AppState,
) -> [(f32, Option<Haptics>); 2] lines: &mut Vec<PointerLine>,
) -> [Option<Haptics>; 2]
where where
O: Default, O: Default,
{ {
if app.input_state.pointers[1].last_click > app.input_state.pointers[0].last_click { let hits = if app.input_state.pointers[1].last_click > app.input_state.pointers[0].last_click {
let right = interact_hand(1, overlays, app); let right = interact_hand(1, overlays, app);
let left = interact_hand(0, overlays, app); let left = interact_hand(0, overlays, app);
[left, right] [left, right]
@@ -342,21 +388,36 @@ where
let left = interact_hand(0, overlays, app); let left = interact_hand(0, overlays, app);
let right = interact_hand(1, overlays, app); let right = interact_hand(1, overlays, app);
[left, right] [left, right]
};
for (idx, hit) in hits.iter().enumerate() {
populate_lines(
lines,
&mut app.input_state.pointers[idx],
hit.0,
&app.input_state.hmd,
);
} }
[hits[0].1, hits[1].1]
} }
fn interact_hand<O>( fn interact_hand<O>(
idx: usize, idx: usize,
overlays: &mut OverlayWindowManager<O>, overlays: &mut OverlayWindowManager<O>,
app: &mut AppState, app: &mut AppState,
) -> (f32, Option<Haptics>) ) -> (Option<(PointerHit, RayHit)>, Option<Haptics>)
where where
O: Default, O: Default,
{ {
// already grabbing, ignore everything else
let mut pointer = &mut app.input_state.pointers[idx]; let mut pointer = &mut app.input_state.pointers[idx];
let pending_haptics = pointer.pending_haptics.take(); let pending_haptics = pointer.pending_haptics.take();
if !pointer.tracked {
return (None, pending_haptics); // no hit
}
// already grabbing, ignore everything else
if let Some(grab_data) = pointer.interaction.grabbed { if let Some(grab_data) = pointer.interaction.grabbed {
if let Some(grabbed) = overlays.mut_by_id(grab_data.grabbed_id) { if let Some(grabbed) = overlays.mut_by_id(grab_data.grabbed_id) {
handle_grabbed(idx, grabbed, app); handle_grabbed(idx, grabbed, app);
@@ -364,13 +425,13 @@ where
log::warn!("Grabbed overlay {:?} does not exist", grab_data.grabbed_id); log::warn!("Grabbed overlay {:?} does not exist", grab_data.grabbed_id);
pointer.interaction.grabbed = None; pointer.interaction.grabbed = None;
} }
return (0.1, pending_haptics); return (None, pending_haptics);
} }
let hovered_id = pointer.interaction.hovered_id.take(); let hovered_id = pointer.interaction.hovered_id.take();
let (Some(mut hit), haptics) = get_nearest_hit(idx, overlays, app) else { let (Some((mut hit, raw_hit)), haptics) = get_nearest_hit(idx, overlays, app) else {
handle_no_hit(idx, hovered_id, overlays, app); handle_no_hit(idx, hovered_id, overlays, app);
return (0.0, pending_haptics); // no hit return (None, pending_haptics); // no hit
}; };
// focus change // focus change
@@ -394,7 +455,7 @@ where
let Some(hovered) = overlays.mut_by_id(hit.overlay) else { let Some(hovered) = overlays.mut_by_id(hit.overlay) else {
log::warn!("Hit overlay {:?} does not exist", hit.overlay); log::warn!("Hit overlay {:?} does not exist", hit.overlay);
return (0.0, pending_haptics); // no hit return (None, pending_haptics); // no hit
}; };
pointer = &mut app.input_state.pointers[idx]; pointer = &mut app.input_state.pointers[idx];
pointer.interaction.hovered_id = Some(hit.overlay); pointer.interaction.hovered_id = Some(hit.overlay);
@@ -432,7 +493,7 @@ where
); );
log::debug!("Hand {}: grabbed {}", hit.pointer, hovered.config.name); log::debug!("Hand {}: grabbed {}", hit.pointer, hovered.config.name);
return ( return (
hit.dist, Some((hit, raw_hit)),
Some(Haptics { Some(Haptics {
intensity: 0.25, intensity: 0.25,
duration: 0.1, duration: 0.1,
@@ -463,7 +524,7 @@ where
} }
} }
(hit.dist, haptics.or(pending_haptics)) (Some((hit, raw_hit)), haptics.or(pending_haptics))
} }
fn handle_no_hit<O>( fn handle_no_hit<O>(
@@ -562,7 +623,7 @@ fn get_nearest_hit<O>(
pointer_idx: usize, pointer_idx: usize,
overlays: &mut OverlayWindowManager<O>, overlays: &mut OverlayWindowManager<O>,
app: &mut AppState, app: &mut AppState,
) -> (Option<PointerHit>, Option<Haptics>) ) -> (Option<(PointerHit, RayHit)>, Option<Haptics>)
where where
O: Default, O: Default,
{ {
@@ -594,7 +655,7 @@ where
hits.sort_by(|a, b| a.dist.total_cmp(&b.dist)); hits.sort_by(|a, b| a.dist.total_cmp(&b.dist));
for hit in &hits { for hit in hits {
let overlay = overlays.mut_by_id(hit.overlay).unwrap(); // safe because we just got the id from the overlay let overlay = overlays.mut_by_id(hit.overlay).unwrap(); // safe because we just got the id from the overlay
let Some(uv) = overlay let Some(uv) = overlay
@@ -611,7 +672,7 @@ where
continue; continue;
} }
let hit = PointerHit { let pointer_hit = PointerHit {
pointer: pointer_idx, pointer: pointer_idx,
overlay: hit.overlay, overlay: hit.overlay,
mode, mode,
@@ -620,9 +681,9 @@ where
dist: hit.dist, dist: hit.dist,
}; };
let result = overlay.config.backend.on_hover(app, &hit); let result = overlay.config.backend.on_hover(app, &pointer_hit);
if result.consume || overlay.config.editing { if result.consume || overlay.config.editing {
return (Some(hit), result.haptics); return (Some((pointer_hit, hit)), result.haptics);
} }
} }
@@ -673,6 +734,7 @@ fn start_grab(
if let Some(hand) = pointer.hand() if let Some(hand) = pointer.hand()
&& !app.session.config.hide_grab_help && !app.session.config.hide_grab_help
&& !pointer.handsfree
{ {
let pos = state.positioning; let pos = state.positioning;
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify( app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(

View File

@@ -175,6 +175,9 @@ impl OpenVrInputSource {
app_hand.raw_pose = devices[device.0 as usize] app_hand.raw_pose = devices[device.0 as usize]
.mDeviceToAbsoluteTracking .mDeviceToAbsoluteTracking
.to_affine(); .to_affine();
app_hand.tracked = devices[device.0 as usize].bPoseIsValid;
} else {
app_hand.tracked = false;
} }
hand.has_pose = false; hand.has_pose = false;

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use ash::vk::SubmitInfo; use ash::vk::SubmitInfo;
use glam::{Affine3A, Vec3, Vec3A, Vec4}; use glam::{Affine3A, Quat, Vec3, Vec3A, Vec4};
use idmap::IdMap; use idmap::IdMap;
use ovr_overlay::overlay::OverlayManager; use ovr_overlay::overlay::OverlayManager;
use ovr_overlay::sys::ETrackingUniverseOrigin; use ovr_overlay::sys::ETrackingUniverseOrigin;
@@ -103,27 +103,44 @@ impl LinePool {
id id
} }
pub fn draw_from( pub(super) fn draw_between(
&mut self, &mut self,
id: usize, id: usize,
mut from: Affine3A, start: Vec3A,
len: f32, end: Vec3A,
color: usize, color: usize,
hmd: &Affine3A, hmd: &Affine3A,
) { ) {
let rotation = Affine3A::from_axis_angle(Vec3::X, -PI * 0.5); let dir = end - start;
let len = dir.length();
from.translation += from.transform_vector3a(Vec3A::NEG_Z) * (len * 0.5); if len < 0.01 {
let mut transform = from * rotation * Affine3A::from_scale(Vec3::new(1., len / 0.002, 1.)); return;
}
let to_hmd = hmd.translation - from.translation; debug_assert!(color < self.colors.len());
let center = (start + end) * 0.5;
let dir_norm = dir / len;
let xform = Affine3A::from_rotation_translation(
Quat::from_rotation_arc(Vec3::Z, dir_norm.into()),
center.into(),
);
let rotation = Affine3A::from_axis_angle(Vec3::X, PI * 1.5);
let mut transform = xform * rotation;
let to_hmd = hmd.translation - center;
let sides = [Vec3A::Z, Vec3A::X, Vec3A::NEG_Z, Vec3A::NEG_X]; let sides = [Vec3A::Z, Vec3A::X, Vec3A::NEG_Z, Vec3A::NEG_X];
#[allow(clippy::neg_multiply)]
let rotations = [ let rotations = [
Affine3A::IDENTITY, Affine3A::IDENTITY,
Affine3A::from_axis_angle(Vec3::Y, PI * 0.5), Affine3A::from_axis_angle(Vec3::Y, PI * 0.5),
Affine3A::from_axis_angle(Vec3::Y, PI * 1.0), Affine3A::from_axis_angle(Vec3::Y, PI * -1.0),
Affine3A::from_axis_angle(Vec3::Y, PI * 1.5), Affine3A::from_axis_angle(Vec3::Y, PI * 1.5),
]; ];
let mut closest = (0, 0.0); let mut closest = (0, 0.0);
for (i, &side) in sides.iter().enumerate() { for (i, &side) in sides.iter().enumerate() {
let dot = to_hmd.dot(transform.transform_vector3a(side)); let dot = to_hmd.dot(transform.transform_vector3a(side));
@@ -135,7 +152,6 @@ impl LinePool {
transform *= rotations[closest.0]; transform *= rotations[closest.0];
debug_assert!(color < self.colors.len()); debug_assert!(color < self.colors.len());
self.draw_transform(id, transform, self.colors[color]); self.draw_transform(id, transform, self.colors[color]);
} }

View File

@@ -144,6 +144,7 @@ pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
let mut lines = LinePool::new(app.gfx.clone())?; let mut lines = LinePool::new(app.gfx.clone())?;
let pointer_lines = [lines.allocate(), lines.allocate()]; let pointer_lines = [lines.allocate(), lines.allocate()];
let mut current_lines = Vec::with_capacity(2);
'main_loop: loop { 'main_loop: loop {
let _ = overlay_mgr.wait_frame_sync(frame_timeout); let _ = overlay_mgr.wait_frame_sync(frame_timeout);
@@ -276,19 +277,23 @@ pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
playspace.update(&mut chaperone_mgr, &mut overlays, &app); playspace.update(&mut chaperone_mgr, &mut overlays, &app);
let lengths_haptics = interact(&mut overlays, &mut app); current_lines.clear();
for (idx, (len, haptics)) in lengths_haptics.iter().enumerate() {
lines.draw_from( let haptics = interact(&mut overlays, &mut app, &mut current_lines);
pointer_lines[idx], for (idx, haptics) in haptics.iter().enumerate() {
app.input_state.pointers[idx].pose,
*len,
app.input_state.pointers[idx].interaction.mode as usize + 1,
&app.input_state.hmd,
);
if let Some(haptics) = haptics { if let Some(haptics) = haptics {
input_source.haptics(&mut input_mgr, idx, haptics); input_source.haptics(&mut input_mgr, idx, haptics);
} }
} }
for (idx, line) in current_lines.iter().enumerate() {
lines.draw_between(
pointer_lines[idx],
line.a,
line.b,
line.mode as usize + 1,
&app.input_state.hmd,
);
}
app.hid_provider.inner.commit(); app.hid_provider.inner.commit();
let mut futures = GpuFutures::default(); let mut futures = GpuFutures::default();

View File

@@ -37,6 +37,11 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
} else { } else {
log::warn!("Missing EXT_hand_interaction extension."); log::warn!("Missing EXT_hand_interaction extension.");
} }
if available_extensions.ext_eye_gaze_interaction {
enabled_extensions.ext_eye_gaze_interaction = true;
} else {
log::warn!("Missing EXT_eye_gaze_interaction extension.");
}
if available_extensions.khr_composition_layer_cylinder { if available_extensions.khr_composition_layer_cylinder {
enabled_extensions.khr_composition_layer_cylinder = true; enabled_extensions.khr_composition_layer_cylinder = true;
} else { } else {

View File

@@ -8,7 +8,7 @@ use glam::{Affine3A, Quat, Vec3, bool};
use libmonado as mnd; use libmonado as mnd;
use openxr::{self as xr, Quaternionf, Vector2f, Vector3f}; use openxr::{self as xr, Quaternionf, Vector2f, Vector3f};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wlx_common::config_io; use wlx_common::{config::HandsfreePointer, config_io};
use crate::{ use crate::{
backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole}, backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole},
@@ -25,10 +25,11 @@ static CLICK_TIMES: [Duration; 3] = [
pub(super) struct OpenXrInputSource { pub(super) struct OpenXrInputSource {
action_set: xr::ActionSet, action_set: xr::ActionSet,
hands: [OpenXrHand; 2], pointers: [OpenXrPointer; 2],
handsfree_pointer: OpenXrPointer,
} }
pub(super) struct OpenXrHand { pub(super) struct OpenXrPointer {
source: OpenXrHandSource, source: OpenXrHandSource,
space: xr::Space, space: xr::Space,
} }
@@ -170,22 +171,27 @@ impl OpenXrInputSource {
let left_source = OpenXrHandSource::new(&mut action_set, "left")?; let left_source = OpenXrHandSource::new(&mut action_set, "left")?;
let right_source = OpenXrHandSource::new(&mut action_set, "right")?; let right_source = OpenXrHandSource::new(&mut action_set, "right")?;
let fallback_source = OpenXrHandSource::new(&mut action_set, "handsfree")?;
suggest_bindings(&xr.instance, &[&left_source, &right_source]); suggest_bindings(
&xr.instance,
&[&left_source, &right_source, &fallback_source],
);
xr.session.attach_action_sets(&[&action_set])?; xr.session.attach_action_sets(&[&action_set])?;
Ok(Self { Ok(Self {
action_set, action_set,
hands: [ pointers: [
OpenXrHand::new(xr, left_source)?, OpenXrPointer::new(xr, left_source)?,
OpenXrHand::new(xr, right_source)?, OpenXrPointer::new(xr, right_source)?,
], ],
handsfree_pointer: OpenXrPointer::new(xr, fallback_source)?,
}) })
} }
pub fn haptics(&self, xr: &XrState, hand: usize, haptics: &Haptics) { pub fn haptics(&self, xr: &XrState, hand: usize, haptics: &Haptics) {
let action = &self.hands[hand].source.haptics; let action = &self.pointers[hand].source.haptics;
let duration_nanos = f64::from(haptics.duration) * 1_000_000_000.0; let duration_nanos = f64::from(haptics.duration) * 1_000_000_000.0;
@@ -204,11 +210,14 @@ impl OpenXrInputSource {
let loc = xr.view.locate(&xr.stage, xr.predicted_display_time)?; let loc = xr.view.locate(&xr.stage, xr.predicted_display_time)?;
let hmd = posef_to_transform(&loc.pose); let hmd = posef_to_transform(&loc.pose);
let mut hmd_tracked = true;
if loc if loc
.location_flags .location_flags
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID) .contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
{ {
state.input_state.hmd.matrix3 = hmd.matrix3; state.input_state.hmd.matrix3 = hmd.matrix3;
} else {
hmd_tracked = false;
} }
if loc if loc
@@ -216,11 +225,26 @@ impl OpenXrInputSource {
.contains(xr::SpaceLocationFlags::POSITION_VALID) .contains(xr::SpaceLocationFlags::POSITION_VALID)
{ {
state.input_state.hmd.translation = hmd.translation; state.input_state.hmd.translation = hmd.translation;
} else {
hmd_tracked = false;
} }
let mut any_tracked = false;
for i in 0..2 { for i in 0..2 {
self.hands[i].update(&mut state.input_state.pointers[i], xr, &state.session)?; let pointer = &mut state.input_state.pointers[i];
self.pointers[i].update(pointer, xr, &state.session)?;
any_tracked |= pointer.tracked;
} }
if !any_tracked {
self.handsfree_pointer.update_handsfree(
&mut state.input_state.pointers[0],
xr,
&state.session,
hmd,
hmd_tracked,
)?;
}
Ok(()) Ok(())
} }
@@ -305,7 +329,7 @@ impl OpenXrInputSource {
} }
} }
impl OpenXrHand { impl OpenXrPointer {
pub(super) fn new(xr: &XrState, source: OpenXrHandSource) -> Result<Self, xr::sys::Result> { pub(super) fn new(xr: &XrState, source: OpenXrHandSource) -> Result<Self, xr::sys::Result> {
let space = source let space = source
.pose .pose
@@ -314,11 +338,62 @@ impl OpenXrHand {
Ok(Self { source, space }) Ok(Self { source, space })
} }
pub(super) fn update_handsfree(
&mut self,
pointer: &mut Pointer,
xr: &XrState,
session: &AppSession,
hmd: Affine3A,
hmd_tracked: bool,
) -> anyhow::Result<()> {
match session.config.handsfree_pointer {
HandsfreePointer::None => return Ok(()),
HandsfreePointer::Hmd => {
pointer.tracked = hmd_tracked;
pointer.raw_pose = hmd;
pointer.pose = hmd;
let (cur_quat, cur_pos) =
(Quat::from_affine3(&pointer.pose), pointer.pose.translation);
let (new_quat, new_pos) = (Quat::from_affine3(&hmd), Vec3::from(hmd.translation));
let lerp_factor =
(1.0 / (xr.fps / 100.0) * session.config.pointer_lerp_factor).clamp(0.1, 1.0);
pointer.raw_pose = Affine3A::from_rotation_translation(new_quat, new_pos);
pointer.pose = Affine3A::from_rotation_translation(
cur_quat.lerp(new_quat, lerp_factor),
cur_pos.lerp(new_pos.into(), lerp_factor).into(),
);
}
HandsfreePointer::EyeTracking => {
// more aggressive smoothing for eye
self.pointer_load_pose(pointer, xr, session.config.pointer_lerp_factor * 0.5)?;
}
}
pointer.handsfree = pointer.tracked;
self.pointer_load_actions(pointer, xr, session)?;
Ok(())
}
pub(super) fn update( pub(super) fn update(
&mut self, &mut self,
pointer: &mut Pointer, pointer: &mut Pointer,
xr: &XrState, xr: &XrState,
session: &AppSession, session: &AppSession,
) -> anyhow::Result<()> {
pointer.handsfree = false;
self.pointer_load_pose(pointer, xr, session.config.pointer_lerp_factor)?;
self.pointer_load_actions(pointer, xr, session)?;
Ok(())
}
fn pointer_load_pose(
&mut self,
pointer: &mut Pointer,
xr: &XrState,
lerp_factor: f32,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let location = self.space.locate(&xr.stage, xr.predicted_display_time)?; let location = self.space.locate(&xr.stage, xr.predicted_display_time)?;
if location if location
@@ -333,15 +408,25 @@ impl OpenXrHand {
transmute::<Vector3f, Vec3>(location.pose.position), transmute::<Vector3f, Vec3>(location.pose.position),
) )
}; };
let lerp_factor = let lerp_factor = (1.0 / (xr.fps / 100.0) * lerp_factor).clamp(0.1, 1.0);
(1.0 / (xr.fps / 100.0) * session.config.pointer_lerp_factor).clamp(0.1, 1.0);
pointer.raw_pose = Affine3A::from_rotation_translation(new_quat, new_pos); pointer.raw_pose = Affine3A::from_rotation_translation(new_quat, new_pos);
pointer.pose = Affine3A::from_rotation_translation( pointer.pose = Affine3A::from_rotation_translation(
cur_quat.lerp(new_quat, lerp_factor), cur_quat.lerp(new_quat, lerp_factor),
cur_pos.lerp(new_pos.into(), lerp_factor).into(), cur_pos.lerp(new_pos.into(), lerp_factor).into(),
); );
pointer.tracked = true;
} else {
pointer.tracked = false;
}
Ok(())
} }
fn pointer_load_actions(
&mut self,
pointer: &mut Pointer,
xr: &XrState,
session: &AppSession,
) -> anyhow::Result<()> {
pointer.now.click = self.source.click.state(pointer.before.click, xr, session)?; pointer.now.click = self.source.click.state(pointer.before.click, xr, session)?;
pointer.now.grab = self.source.grab.state(pointer.before.grab, xr, session)?; pointer.now.grab = self.source.grab.state(pointer.before.grab, xr, session)?;
@@ -464,11 +549,12 @@ fn is_bool(path_str: &str) -> bool {
macro_rules! add_custom { macro_rules! add_custom {
($action:expr, $field:ident, $hands:expr, $bindings:expr, $instance:expr) => { ($action:expr, $field:ident, $hands:expr, $bindings:expr, $instance:expr) => {
if let Some(action) = $action.as_ref() { if let Some(action) = $action.as_ref() {
for i in 0..2 { for i in 0..3 {
let spec = if i == 0 { let spec = match i {
action.left.as_ref() 0 => action.left.as_ref(),
} else { 1 => action.right.as_ref(),
action.right.as_ref() 2 => action.handsfree.as_ref(),
_ => unreachable!(),
}; };
if let Some(spec) = spec { if let Some(spec) = spec {
@@ -526,11 +612,12 @@ macro_rules! add_custom {
macro_rules! add_custom_lr { macro_rules! add_custom_lr {
($action:expr, $field:ident, $hands:expr, $bindings:expr, $instance:expr) => { ($action:expr, $field:ident, $hands:expr, $bindings:expr, $instance:expr) => {
if let Some(action) = $action { if let Some(action) = $action {
for i in 0..2 { for i in 0..3 {
let spec = if i == 0 { let spec = match i {
action.left.as_ref() 0 => action.left.as_ref(),
} else { 1 => action.right.as_ref(),
action.right.as_ref() 2 => action.handsfree.as_ref(),
_ => unreachable!(),
}; };
if let Some(spec) = spec { if let Some(spec) = spec {
@@ -551,12 +638,14 @@ macro_rules! add_custom_lr {
} }
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) { fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 3]) {
let profiles = load_action_profiles(); let profiles = load_action_profiles();
for profile in profiles { for profile in profiles {
log::warn!("Loading profile {}", &profile.profile);
let Ok(profile_path) = instance.string_to_path(&profile.profile) else { let Ok(profile_path) = instance.string_to_path(&profile.profile) else {
log::debug!("Profile not supported: {}", profile.profile); log::warn!("Profile not supported: {}", profile.profile);
continue; continue;
}; };
@@ -618,6 +707,11 @@ fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) {
{ {
log::error!("Bad bindings for {}", &profile.profile[22..]); log::error!("Bad bindings for {}", &profile.profile[22..]);
log::error!("Verify config: ~/.config/wayvr/openxr_actions.json5"); log::error!("Verify config: ~/.config/wayvr/openxr_actions.json5");
} else {
log::debug!(
"Bindings for {} bound successfully.",
&profile.profile[22..]
)
} }
} }
} }
@@ -633,6 +727,7 @@ enum OneOrMany<T> {
struct OpenXrActionConfAction { struct OpenXrActionConfAction {
left: Option<OneOrMany<String>>, left: Option<OneOrMany<String>>,
right: Option<OneOrMany<String>>, right: Option<OneOrMany<String>>,
handsfree: Option<OneOrMany<String>>,
threshold: Option<[f32; 2]>, threshold: Option<[f32; 2]>,
double_click: Option<bool>, double_click: Option<bool>,
triple_click: Option<bool>, triple_click: Option<bool>,
@@ -672,6 +767,8 @@ fn load_action_profiles() -> Vec<OpenXrActionConfProfile> {
for new in override_profiles { for new in override_profiles {
if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) { if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) {
profiles[i] = new; profiles[i] = new;
} else {
profiles.push(new);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
use glam::{Affine3A, Vec3, Vec3A}; use glam::{Affine3A, Quat, Vec3, Vec3A};
use idmap::IdMap; use idmap::IdMap;
use openxr as xr; use openxr as xr;
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -96,14 +96,17 @@ impl LinePool {
Ok(id) Ok(id)
} }
pub(super) fn draw_from( pub(super) fn draw_between(
&mut self, &mut self,
id: usize, id: usize,
mut from: Affine3A, start: Vec3A,
len: f32, end: Vec3A,
color: usize, color: usize,
hmd: &Affine3A, hmd: Affine3A,
) { ) {
let dir = end - start;
let len = dir.length();
if len < 0.01 { if len < 0.01 {
return; return;
} }
@@ -115,13 +118,19 @@ impl LinePool {
return; return;
}; };
let center = (start + end) * 0.5;
let dir_norm = dir / len;
let xform = Affine3A::from_rotation_translation(
Quat::from_rotation_arc(Vec3::Z, dir_norm.into()),
center.into(),
);
let rotation = Affine3A::from_axis_angle(Vec3::X, PI * 1.5); let rotation = Affine3A::from_axis_angle(Vec3::X, PI * 1.5);
let mut transform = xform * rotation;
from.translation += from.transform_vector3a(Vec3A::NEG_Z) * (len * 0.5); let to_hmd = hmd.translation - center;
let mut transform = from * rotation;
let to_hmd = hmd.translation - from.translation;
let sides = [Vec3A::Z, Vec3A::X, Vec3A::NEG_Z, Vec3A::NEG_X]; let sides = [Vec3A::Z, Vec3A::X, Vec3A::NEG_Z, Vec3A::NEG_X];
#[allow(clippy::neg_multiply)] #[allow(clippy::neg_multiply)]
let rotations = [ let rotations = [
Affine3A::IDENTITY, Affine3A::IDENTITY,
@@ -129,6 +138,7 @@ impl LinePool {
Affine3A::from_axis_angle(Vec3::Y, PI * -1.0), Affine3A::from_axis_angle(Vec3::Y, PI * -1.0),
Affine3A::from_axis_angle(Vec3::Y, PI * 1.5), Affine3A::from_axis_angle(Vec3::Y, PI * 1.5),
]; ];
let mut closest = (0, 0.0); let mut closest = (0, 0.0);
for (i, &side) in sides.iter().enumerate() { for (i, &side) in sides.iter().enumerate() {
let dot = to_hmd.dot(transform.transform_vector3a(side)); let dot = to_hmd.dot(transform.transform_vector3a(side));

View File

@@ -90,6 +90,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
let mut overlays = OverlayWindowManager::<OpenXrOverlayData>::new(&mut app, headless)?; let mut overlays = OverlayWindowManager::<OpenXrOverlayData>::new(&mut app, headless)?;
let mut lines = LinePool::new(&app)?; let mut lines = LinePool::new(&app)?;
let mut current_lines = Vec::with_capacity(2);
let mut notifications = NotificationManager::new(); let mut notifications = NotificationManager::new();
notifications.run_dbus(&mut app.dbus); notifications.run_dbus(&mut app.dbus);
@@ -327,19 +328,23 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
.values_mut() .values_mut()
.for_each(|o| o.config.auto_movement(&mut app)); .for_each(|o| o.config.auto_movement(&mut app));
let lengths_haptics = interact(&mut overlays, &mut app); current_lines.clear();
for (idx, (len, haptics)) in lengths_haptics.iter().enumerate() {
lines.draw_from( let haptics = interact(&mut overlays, &mut app, &mut current_lines);
pointer_lines[idx], for (idx, haptics) in haptics.iter().enumerate() {
app.input_state.pointers[idx].pose,
*len,
app.input_state.pointers[idx].interaction.mode as usize + 1,
&app.input_state.hmd,
);
if let Some(haptics) = haptics { if let Some(haptics) = haptics {
input_source.haptics(&xr_state, idx, haptics); input_source.haptics(&xr_state, idx, haptics);
} }
} }
for (idx, line) in current_lines.iter().enumerate() {
lines.draw_between(
pointer_lines[idx],
line.a,
line.b,
line.mode as usize + 1,
app.input_state.hmd,
);
}
app.hid_provider.inner.commit(); app.hid_provider.inner.commit();

View File

@@ -34,22 +34,21 @@
// do not mess with these, unless you know what you're doing // do not mess with these, unless you know what you're doing
[ [
// Hand tracking {
// { profile: "/interaction_profiles/ext/eye_gaze_interaction",
// profile: "/interaction_profiles/ext/hand_interaction_ext", pose: {
// pose: { fallback: "/user/eyes_ext/input/gaze_ext/pose",
// left: "/user/hand/left/input/aim/pose", },
// right: "/user/hand/right/input/aim/pose" },
// }, {
// click: { profile: "/interaction_profiles/ext/hand_interaction_ext",
// left: "/user/hand/left/input/pinch_ext/value", click: {
// right: "/user/hand/right/input/pinch_ext/value" fallback: "/user/hand/right/input/pinch_ext/value"
// }, },
// grab: { grab: {
// left: "/user/hand/left/input/grasp_ext/value", fallback: "/user/hand/left/input/pinch_ext/value",
// right: "/user/hand/right/input/grasp_ext/value" },
// }, },
// },
// Fallback controller, intended for testing // Fallback controller, intended for testing
{ {

View File

@@ -66,6 +66,8 @@ pub enum ModifyPanelCommand {
SetColor(String), SetColor(String),
SetImage(String), SetImage(String),
SetVisible(bool), SetVisible(bool),
GetValue,
SetValue(String),
SetStickyState(bool), SetStickyState(bool),
} }

View File

@@ -36,7 +36,6 @@ use std::fs::File;
use std::io::Write; use std::io::Write;
use std::os::fd::OwnedFd; use std::os::fd::OwnedFd;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use wayland_client::WEnum;
use smithay::utils::Serial; use smithay::utils::Serial;
use smithay::wayland::compositor::{self, BufferAssignment, SurfaceAttributes, send_surface_state}; use smithay::wayland::compositor::{self, BufferAssignment, SurfaceAttributes, send_surface_state};

View File

@@ -6,7 +6,8 @@ use wayvr_ipc::packet_client::WvrProcessLaunchParams;
use wlx_common::{ use wlx_common::{
astr_containers::AStrMap, astr_containers::AStrMap,
config::{ config::{
AltModifier, CaptureMethod, GeneralConfig, SerializedWindowSet, SerializedWindowStates, AltModifier, CaptureMethod, GeneralConfig, HandsfreePointer, SerializedWindowSet,
SerializedWindowStates,
}, },
config_io, config_io,
overlays::BackendAttribValue, overlays::BackendAttribValue,
@@ -139,6 +140,7 @@ pub struct AutoSettings {
pub capture_method: CaptureMethod, pub capture_method: CaptureMethod,
pub keyboard_middle_click_mode: AltModifier, pub keyboard_middle_click_mode: AltModifier,
pub autostart_apps: Vec<WvrProcessLaunchParams>, pub autostart_apps: Vec<WvrProcessLaunchParams>,
pub handsfree_pointer: HandsfreePointer,
} }
fn get_settings_path() -> PathBuf { fn get_settings_path() -> PathBuf {
@@ -185,6 +187,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
capture_method: config.capture_method, capture_method: config.capture_method,
keyboard_middle_click_mode: config.keyboard_middle_click_mode, keyboard_middle_click_mode: config.keyboard_middle_click_mode,
autostart_apps: config.autostart_apps.clone(), autostart_apps: config.autostart_apps.clone(),
handsfree_pointer: config.handsfree_pointer,
}; };
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic

View File

@@ -22,7 +22,7 @@ use wgui::{
widget::EventResult, widget::EventResult,
windowing::context_menu::{Blueprint, ContextMenu, OpenParams}, windowing::context_menu::{Blueprint, ContextMenu, OpenParams},
}; };
use wlx_common::overlays::ToastTopic; use wlx_common::{config::HandsfreePointer, overlays::ToastTopic};
use crate::{ use crate::{
RESTART, RUNNING, RESTART, RUNNING,
@@ -600,6 +600,26 @@ pub(super) fn setup_custom_button<S: 'static>(
Ok(EventResult::Consumed) Ok(EventResult::Consumed)
}), }),
"::HandsfreeMode" => {
let Some(arg) = args.next() else {
log_cmd_missing_arg(parser_state, TAG, name, command);
return;
};
let Ok(val) = HandsfreePointer::from_str(arg) else {
let msg = format!("expected HandsfreePointer, found \"{arg}\"");
log_cmd_invalid_arg(parser_state, TAG, name, command, &msg);
return;
};
Box::new(move |_common, data, app, _| {
if !test_button(data) || !test_duration(&button, app) {
return Ok(EventResult::Pass);
}
app.session.config.handsfree_pointer = val;
Ok(EventResult::Consumed)
})
}
"::SendKey" => { "::SendKey" => {
let Some(arg) = args.next() else { let Some(arg) = args.next() else {
log_cmd_missing_arg(parser_state, TAG, name, command); log_cmd_missing_arg(parser_state, TAG, name, command);

View File

@@ -7,7 +7,10 @@ use idmap::IdMap;
use label::setup_custom_label; use label::setup_custom_label;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::{
button::ComponentButton, checkbox::ComponentCheckbox, radio_group::ComponentRadioGroup,
slider::ComponentSlider,
},
drawing, drawing,
event::{ event::{
CallbackDataCommon, Event as WguiEvent, EventAlterables, EventCallback, EventListenerID, CallbackDataCommon, Event as WguiEvent, EventAlterables, EventCallback, EventListenerID,
@@ -20,8 +23,7 @@ use wgui::{
parser::{ parser::{
self, CustomAttribsInfoOwned, Fetchable, ParseDocumentExtra, ParserState, parse_color_hex, self, CustomAttribsInfoOwned, Fetchable, ParseDocumentExtra, ParserState, parse_color_hex,
}, },
renderer_vk::context::Context as WguiContext, renderer_vk::{context::Context as WguiContext, text::custom_glyph::CustomGlyphData},
renderer_vk::text::custom_glyph::CustomGlyphData,
taffy, taffy,
widget::{ widget::{
EventResult, image::WidgetImage, label::WidgetLabel, rectangle::WidgetRectangle, EventResult, image::WidgetImage, label::WidgetLabel, rectangle::WidgetRectangle,
@@ -574,6 +576,33 @@ pub fn apply_custom_command<T>(
.set_style(wid, wgui::event::StyleSetRequest::Display(display)); .set_style(wid, wgui::event::StyleSetRequest::Display(display));
com.alterables.mark_redraw(); com.alterables.mark_redraw();
} }
ModifyPanelCommand::SetValue(value_str) => {
if let Ok(cb) = panel
.parser_state
.fetch_component_as::<ComponentCheckbox>(element)
{
let value_b = match value_str.to_lowercase().as_str() {
"0" | "false" => false,
"1" | "true" => true,
_ => anyhow::bail!(
"Not a valid bool. Supported values: '0', '1', 'false', 'true'"
),
};
cb.set_checked(&mut com, value_b);
} else if let Ok(slider) = panel
.parser_state
.fetch_component_as::<ComponentSlider>(element)
{
let value_f32 = value_str.parse::<f32>().context("Not a valid number")?;
slider.set_value(&mut com, value_f32);
} else if let Ok(radio) = panel
.parser_state
.fetch_component_as::<ComponentRadioGroup>(element)
{
radio.set_value(&mut com, &value_str)?;
}
}
ModifyPanelCommand::GetValue => todo!(),
ModifyPanelCommand::SetStickyState(sticky_down) => { ModifyPanelCommand::SetStickyState(sticky_down) => {
let button = panel let button = panel
.parser_state .parser_state

View File

@@ -279,6 +279,7 @@ fn logging_init(args: &mut Args) {
.with_default_directive(LevelFilter::INFO.into()) .with_default_directive(LevelFilter::INFO.into())
.from_env_lossy() .from_env_lossy()
.add_directive("symphonia_core::probe=warn".parse().unwrap()) .add_directive("symphonia_core::probe=warn".parse().unwrap())
.add_directive("symphonia_bundle_mp3=warn".parse().unwrap())
.add_directive("zbus=warn".parse().unwrap()) .add_directive("zbus=warn".parse().unwrap())
.add_directive("usvg=error".parse().unwrap()) .add_directive("usvg=error".parse().unwrap())
.add_directive("resvg=error".parse().unwrap()) .add_directive("resvg=error".parse().unwrap())

View File

@@ -123,6 +123,11 @@ impl ComponentCheckbox {
common.alterables.mark_redraw(); common.alterables.mark_redraw();
} }
pub fn get_checked(&self) -> bool {
let state = self.state.borrow_mut();
state.checked
}
pub fn get_value(&self) -> Option<Rc<str>> { pub fn get_value(&self) -> Option<Rc<str>> {
self.data.value.clone() self.data.value.clone()
} }

View File

@@ -86,7 +86,7 @@ impl ComponentRadioGroup {
self.state.borrow().selected.as_ref().and_then(|b| b.get_value()) self.state.borrow().selected.as_ref().and_then(|b| b.get_value())
} }
pub fn set_value(&self, value: &str) -> anyhow::Result<()> { pub fn set_value_simple(&self, value: &str) -> anyhow::Result<()> {
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
for radio_box in &state.radio_boxes { for radio_box in &state.radio_boxes {
if radio_box.get_value().is_some_and(|box_val| &*box_val == value) { if radio_box.get_value().is_some_and(|box_val| &*box_val == value) {
@@ -97,6 +97,26 @@ impl ComponentRadioGroup {
anyhow::bail!("No RadioBox found with value '{value}'") anyhow::bail!("No RadioBox found with value '{value}'")
} }
pub fn set_value(&self, common: &mut CallbackDataCommon, value: &str) -> anyhow::Result<()> {
let mut state = self.state.borrow_mut();
let mut selected = None;
for radio_box in &state.radio_boxes {
if radio_box.get_value().is_some_and(|box_val| &*box_val == value) {
selected = Some(radio_box.clone());
radio_box.set_checked(common, true);
} else {
radio_box.set_checked(common, false);
}
}
if selected.is_some() {
state.selected = selected;
Ok(())
} else {
anyhow::bail!("No RadioBox found with value '{value}'")
}
}
pub fn on_value_changed(&self, callback: RadioValueChangeCallback) { pub fn on_value_changed(&self, callback: RadioValueChangeCallback) {
self.state.borrow_mut().on_value_changed = Some(callback); self.state.borrow_mut().on_value_changed = Some(callback);
} }

View File

@@ -41,6 +41,7 @@ pub enum CaptureMethod {
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, AsRefStr, EnumString, EnumProperty, VariantArray)] #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, AsRefStr, EnumString, EnumProperty, VariantArray)]
pub enum AltModifier { pub enum AltModifier {
#[default] #[default]
#[strum(props(Translation = "APP_SETTINGS.OPTION.NONE"))]
None, None,
Shift, Shift,
Ctrl, Ctrl,
@@ -49,6 +50,17 @@ pub enum AltModifier {
Meta, Meta,
} }
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, AsRefStr, EnumString, EnumProperty, VariantArray)]
pub enum HandsfreePointer {
#[strum(props(Translation = "APP_SETTINGS.OPTION.NONE"))]
None,
#[strum(props(Translation = "APP_SETTINGS.OPTION.HMD_PINCH"))]
#[default]
Hmd,
#[strum(props(Translation = "APP_SETTINGS.OPTION.EYE_PINCH"))]
EyeTracking,
}
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct SerializedWindowSet { pub struct SerializedWindowSet {
pub name: Arc<str>, pub name: Arc<str>,
@@ -295,4 +307,7 @@ pub struct GeneralConfig {
#[serde(default)] #[serde(default)]
pub keyboard_middle_click_mode: AltModifier, pub keyboard_middle_click_mode: AltModifier,
#[serde(default)]
pub handsfree_pointer: HandsfreePointer,
} }