diff --git a/wlx-overlay-s/src/gui/panel/button.rs b/wlx-overlay-s/src/gui/panel/button.rs index 1474d23..87edbd2 100644 --- a/wlx-overlay-s/src/gui/panel/button.rs +++ b/wlx-overlay-s/src/gui/panel/button.rs @@ -20,7 +20,7 @@ use wlx_common::overlays::ToastTopic; use crate::{ RUNNING, backend::task::{OverlayTask, PlayspaceTask, TaskType}, - overlays::toast::Toast, + overlays::{dashboard::DASH_NAME, toast::Toast}, state::AppState, subsystem::hid::VirtualKey, windowing::OverlaySelector, @@ -191,13 +191,21 @@ pub(super) fn setup_custom_button( let button = button.clone(); let callback: EventCallback = match command { - #[cfg(feature = "wayvr")] "::DashToggle" => Box::new(move |_common, data, app, _| { if !test_button(data) || !test_duration(&button, app) { return Ok(EventResult::Pass); } - //FIXME + app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify( + OverlaySelector::Name(DASH_NAME.into()), + Box::new(move |app, owc| { + if owc.active_state.is_none() { + owc.activate(app); + } else { + owc.deactivate(); + } + }), + ))); Ok(EventResult::Consumed) }), "::SetToggle" => { diff --git a/wlx-overlay-s/src/overlays/dashboard.rs b/wlx-overlay-s/src/overlays/dashboard.rs new file mode 100644 index 0000000..c523b73 --- /dev/null +++ b/wlx-overlay-s/src/overlays/dashboard.rs @@ -0,0 +1,297 @@ +use dash_frontend::{ + frontend, + settings::{self, SettingsIO}, +}; +use glam::{Affine2, Affine3A, Vec2, vec2, vec3}; +use wgui::{ + event::{ + Event as WguiEvent, MouseButtonIndex, MouseDownEvent, MouseLeaveEvent, MouseMotionEvent, + MouseUpEvent, MouseWheelEvent, + }, + gfx::cmd::WGfxClearMode, + renderer_vk::context::Context as WguiContext, + widget::EventResult, +}; +use wlx_common::{ + dash_interface_emulated::DashInterfaceEmulated, + overlays::{BackendAttrib, BackendAttribValue}, +}; +use wlx_common::{ + timestep::Timestep, + windowing::{OverlayWindowState, Positioning}, +}; + +use crate::{ + backend::input::{Haptics, HoverResult, PointerHit, PointerMode}, + state::AppState, + subsystem::hid::WheelDelta, + windowing::{ + Z_ORDER_DASHBOARD, + backend::{ + FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, + ui_transform, + }, + window::{OverlayCategory, OverlayWindowConfig}, + }, +}; + +pub const DASH_NAME: &str = "Dashboard"; + +const DASH_RES_U32A: [u32; 2] = [1280, 720]; +const DASH_RES_VEC2: Vec2 = vec2(DASH_RES_U32A[0] as _, DASH_RES_U32A[1] as _); + +//FIXME: replace with proper impl +struct SimpleSettingsIO { + settings: settings::Settings, +} +impl SimpleSettingsIO { + fn new() -> Self { + let mut res = Self { + settings: settings::Settings::default(), + }; + res.read_from_disk(); + res + } +} +impl settings::SettingsIO for SimpleSettingsIO { + fn get_mut(&mut self) -> &mut settings::Settings { + &mut self.settings + } + fn get(&self) -> &dash_frontend::settings::Settings { + &self.settings + } + fn save_to_disk(&mut self) { + log::info!("saving settings"); + let data = self.settings.save(); + std::fs::write("/tmp/testbed_settings.json", data).unwrap(); + } + fn read_from_disk(&mut self) { + log::info!("loading settings"); + if let Ok(res) = std::fs::read("/tmp/testbed_settings.json") { + let data = String::from_utf8(res).unwrap(); + self.settings = settings::Settings::load(&data).unwrap(); + } + } + fn mark_as_dirty(&mut self) { + self.save_to_disk(); + } +} + +pub struct DashFrontend { + inner: frontend::Frontend, + initialized: bool, + interaction_transform: Option, + timestep: Timestep, + has_focus: [bool; 2], + context: WguiContext, +} + +impl DashFrontend { + fn new(app: &mut AppState) -> anyhow::Result { + let settings = SimpleSettingsIO::new(); + let interface = DashInterfaceEmulated::new(); + + let frontend = frontend::Frontend::new(frontend::InitParams { + settings: Box::new(settings), + interface: Box::new(interface), + })?; + let context = WguiContext::new(&mut app.wgui_shared, 1.0)?; + Ok(Self { + inner: frontend, + initialized: false, + interaction_transform: None, + timestep: Timestep::new(), + has_focus: [false, false], + context, + }) + } + + fn update_layout(&mut self) -> anyhow::Result<()> { + self.inner.update(DASH_RES_VEC2.x, DASH_RES_VEC2.y, 0.0) + } + + fn push_event(&mut self, event: &WguiEvent) -> EventResult { + match self.inner.layout.push_event(event, &mut (), &mut ()) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to push event: {e:?}"); + EventResult::NoHit + } + } + } +} + +impl OverlayBackend for DashFrontend { + fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> { + self.context + .update_viewport(&mut app.wgui_shared, DASH_RES_U32A, 1.0)?; + self.interaction_transform = Some(ui_transform(DASH_RES_U32A)); + + if self.inner.layout.content_size.x * self.inner.layout.content_size.y != 0.0 { + self.update_layout()?; + self.initialized = true; + } + Ok(()) + } + + fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> { + Ok(()) + } + + fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> { + self.inner.layout.needs_redraw = true; + self.timestep.reset(); + Ok(()) + } + + fn should_render(&mut self, _app: &mut AppState) -> anyhow::Result { + while self.timestep.on_tick() { + self.inner.layout.tick()?; + } + + Ok(if self.inner.layout.check_toggle_needs_redraw() { + ShouldRender::Should + } else { + ShouldRender::Can + }) + } + + fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> { + self.inner + .layout + .update(DASH_RES_VEC2, self.timestep.alpha)?; + + let globals = self.inner.layout.state.globals.clone(); // sorry + let mut globals = globals.get(); + + let primitives = wgui::drawing::draw(&mut wgui::drawing::DrawParams { + globals: &mut globals, + layout: &mut self.inner.layout, + debug_draw: false, + timestep_alpha: self.timestep.alpha, + })?; + self.context.draw( + &globals.font_system, + &mut app.wgui_shared, + &mut rdr.cmd_buf_single(), + &primitives, + )?; + Ok(()) + } + + fn frame_meta(&mut self) -> Option { + Some(FrameMeta { + clear: WGfxClearMode::Clear([0., 0., 0., 0.]), + extent: [DASH_RES_U32A[0], DASH_RES_U32A[1], 1], + ..Default::default() + }) + } + + fn notify(&mut self, _app: &mut AppState, _data: OverlayEventData) -> anyhow::Result<()> { + Ok(()) + } + + fn on_scroll(&mut self, _app: &mut AppState, hit: &PointerHit, delta: WheelDelta) { + let e = WguiEvent::MouseWheel(MouseWheelEvent { + delta: vec2(delta.x, delta.y), + pos: hit.uv * self.inner.layout.content_size, + device: hit.pointer, + }); + self.push_event(&e); + } + + fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> HoverResult { + let e = &WguiEvent::MouseMotion(MouseMotionEvent { + pos: hit.uv * self.inner.layout.content_size, + device: hit.pointer, + }); + + self.has_focus[hit.pointer] = true; + + let result = self.push_event(e); + + HoverResult { + consume: result != EventResult::NoHit, + haptics: self + .inner + .layout + .check_toggle_haptics_triggered() + .then_some(Haptics { + intensity: 0.1, + duration: 0.01, + frequency: 5.0, + }), + } + } + + fn on_left(&mut self, _app: &mut AppState, pointer: usize) { + let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); + self.has_focus[pointer] = false; + self.push_event(&e); + } + + fn on_pointer(&mut self, _app: &mut AppState, hit: &PointerHit, pressed: bool) { + let index = match hit.mode { + PointerMode::Left => MouseButtonIndex::Left, + PointerMode::Right => MouseButtonIndex::Right, + PointerMode::Middle => MouseButtonIndex::Middle, + _ => return, + }; + + let e = if pressed { + WguiEvent::MouseDown(MouseDownEvent { + pos: hit.uv * self.inner.layout.content_size, + index, + device: hit.pointer, + }) + } else { + WguiEvent::MouseUp(MouseUpEvent { + pos: hit.uv * self.inner.layout.content_size, + index, + device: hit.pointer, + }) + }; + self.push_event(&e); + + // released while off-panel → send mouse leave as well + if !pressed && !self.has_focus[hit.pointer] { + let e = WguiEvent::MouseMotion(MouseMotionEvent { + pos: vec2(-1., -1.), + device: hit.pointer, + }); + self.push_event(&e); + let e = WguiEvent::MouseLeave(MouseLeaveEvent { + device: hit.pointer, + }); + self.push_event(&e); + } + } + + fn get_interaction_transform(&mut self) -> Option { + self.interaction_transform + } + fn get_attrib(&self, _attrib: BackendAttrib) -> Option { + None + } + fn set_attrib(&mut self, _app: &mut AppState, _value: BackendAttribValue) -> bool { + false + } +} + +pub fn create_dash_frontend(app: &mut AppState) -> anyhow::Result { + Ok(OverlayWindowConfig { + name: DASH_NAME.into(), + default_state: OverlayWindowState { + transform: Affine3A::from_translation(vec3(0., 0., -0.9)), + grabbable: true, + interactable: true, + positioning: Positioning::Floating, + curvature: Some(0.15), + ..OverlayWindowState::default() + }, + z_order: Z_ORDER_DASHBOARD, + category: OverlayCategory::Dashboard, + global: true, + ..OverlayWindowConfig::from_backend(Box::new(DashFrontend::new(app)?)) + }) +} diff --git a/wlx-overlay-s/src/overlays/mod.rs b/wlx-overlay-s/src/overlays/mod.rs index 0b9795f..61c7431 100644 --- a/wlx-overlay-s/src/overlays/mod.rs +++ b/wlx-overlay-s/src/overlays/mod.rs @@ -1,5 +1,6 @@ pub mod anchor; pub mod custom; +pub mod dashboard; pub mod edit; pub mod keyboard; pub mod screen; diff --git a/wlx-overlay-s/src/overlays/wayvr.rs b/wlx-overlay-s/src/overlays/wayvr.rs index b91fe50..fcda786 100644 --- a/wlx-overlay-s/src/overlays/wayvr.rs +++ b/wlx-overlay-s/src/overlays/wayvr.rs @@ -43,7 +43,7 @@ pub fn create_wl_window_overlay( transform: Affine3A::from_scale_rotation_translation( Vec3::ONE, Quat::IDENTITY, - vec3(0.0, 0.0, -0.4), + vec3(0.0, 0.0, -0.95), ), ..OverlayWindowState::default() }, diff --git a/wlx-overlay-s/src/windowing/manager.rs b/wlx-overlay-s/src/windowing/manager.rs index df48f53..bf67285 100644 --- a/wlx-overlay-s/src/windowing/manager.rs +++ b/wlx-overlay-s/src/windowing/manager.rs @@ -18,6 +18,7 @@ use crate::{ overlays::{ anchor::{create_anchor, create_grab_help}, custom::create_custom, + dashboard::create_dash_frontend, edit::EditWrapperManager, keyboard::create_keyboard, screen::create_screens, @@ -122,6 +123,9 @@ where let watch = OverlayWindowData::from_config(create_watch(app)?); me.watch_id = me.add(watch, app); + let dash_frontend = OverlayWindowData::from_config(create_dash_frontend(app)?); + me.add(dash_frontend, app); + let grab_help = OverlayWindowData::from_config(create_grab_help(app)?); me.add(grab_help, app);