diff --git a/Cargo.lock b/Cargo.lock index bed2259..07e63ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1587,7 +1587,7 @@ dependencies = [ [[package]] name = "libmonado" version = "1.3.2" -source = "git+https://github.com/technobaboo/libmonado-rs.git?rev=26292e5b14663ee2f089f66f0851438a0c00ee67#26292e5b14663ee2f089f66f0851438a0c00ee67" +source = "git+https://github.com/technobaboo/libmonado-rs.git?rev=ad3162df5255716c8e78d618adbf1d808c80906b#ad3162df5255716c8e78d618adbf1d808c80906b" dependencies = [ "bindgen", "cmake", diff --git a/Cargo.toml b/Cargo.toml index f62854a..e9de1f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ git2 = "0.19.0" gtk = { package = "gtk4", version = "0.10.3", features = ["v4_10"] } lazy_static = "1.5.0" adw = { package = "libadwaita", version = "0.8.1", features = ["v1_5"] } -libmonado = { git = "https://github.com/technobaboo/libmonado-rs.git", rev = "26292e5b14663ee2f089f66f0851438a0c00ee67" } +libmonado = { git = "https://github.com/technobaboo/libmonado-rs.git", rev = "ad3162df5255716c8e78d618adbf1d808c80906b", features = ["rc"] } rusb = "0.9.4" nix = { version = "0.30.1", features = ["fs", "signal"] } relm4 = { version = "0.10.0", features = ["libadwaita"] } diff --git a/src/main.rs b/src/main.rs index c56c084..38a977a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,7 @@ pub mod util; pub mod vulkaninfo; pub mod wivrn_dbus; pub mod xdg; +pub mod xr_clients; pub mod xr_devices; fn restore_steam_xr_files() { diff --git a/src/ui/app.rs b/src/ui/app.rs index 02fb354..a6cbeee 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -59,6 +59,7 @@ use crate::{ }, vulkaninfo::VulkanInfo, wivrn_dbus, + xr_clients::XRClient, xr_devices::XRDevice, }; use adw::{ResponseAppearance, prelude::*}; @@ -70,7 +71,7 @@ use relm4::{ new_action_group, new_stateful_action, new_stateless_action, prelude::*, }; -use std::{collections::VecDeque, fs::remove_file, time::Duration}; +use std::{collections::VecDeque, fs::remove_file, rc::Rc, time::Duration}; use tracing::error; pub struct App { @@ -93,8 +94,7 @@ pub struct App { build_worker: Option, monado_gui_worker: Option, profiles: Vec, - xr_devices: Vec, - libmonado: Option, + libmonado: Option>, wivrn_conf_editor: Option>, skip_depcheck: bool, @@ -248,7 +248,6 @@ impl App { return; }; self.debug_view.sender().emit(DebugViewMsg::ClearLog); - self.xr_devices = vec![]; { let ipc_file = prof.xrservice_type.ipc_file_path(); if ipc_file.exists() { @@ -456,7 +455,6 @@ impl AsyncComponent for App { .sender() .emit(DebugViewMsg::XRServiceActiveChanged(false)); self.libmonado = None; - self.xr_devices = vec![]; if code != 0 && code != 15 { // 15 is SIGTERM sender.input(Msg::OnServiceLog(vec![format!( @@ -480,13 +478,15 @@ impl AsyncComponent for App { .is_some_and(JobWorker::is_alive); let should_poll_for_devices = self.xrservice_ready && xrservice_worker_is_alive; if should_poll_for_devices { - if let Some(monado) = self.libmonado.as_ref() { - self.xr_devices = XRDevice::from_libmonado(monado); + if let Some(monado) = self.libmonado.clone() { + self.main_view.sender().emit(MainViewMsg::UpdateDevices( + XRDevice::from_libmonado(monado.clone()), + )); self.main_view .sender() - .emit(MainViewMsg::UpdateDevices(self.xr_devices.clone())); + .emit(MainViewMsg::UpdateClients(XRClient::from_libmonado(monado))); } else if let Some(so) = self.get_selected_profile().libmonado_so() { - self.libmonado = libmonado::Monado::create(so).ok(); + self.libmonado = libmonado::Monado::create(so).map(Rc::new).ok(); if self.libmonado.is_some() { sender.input(Msg::ClockTicking); } @@ -1372,7 +1372,6 @@ impl AsyncComponent for App { plugins_worker: None, build_worker: None, monado_gui_worker: None, - xr_devices: vec![], restart_xrservice: false, libmonado: None, wivrn_conf_editor: None, diff --git a/src/ui/battery_status.rs b/src/ui/battery_status.rs index 45a1ff3..e875622 100644 --- a/src/ui/battery_status.rs +++ b/src/ui/battery_status.rs @@ -6,6 +6,15 @@ pub struct EnvisionBatteryStatus { pub battery_status: BatteryStatus, } +impl PartialEq for EnvisionBatteryStatus { + fn eq(&self, other: &Self) -> bool { + self.battery_status.present == other.battery_status.present + && self.battery_status.charging == other.battery_status.charging + && self.battery_status.charge == other.battery_status.charge + } +} +impl Eq for EnvisionBatteryStatus {} + impl Display for EnvisionBatteryStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&format!( diff --git a/src/ui/clients_box.rs b/src/ui/clients_box.rs new file mode 100644 index 0000000..920d159 --- /dev/null +++ b/src/ui/clients_box.rs @@ -0,0 +1,100 @@ +use crate::{ + ui::factories::client_row_factory::{ClientRowModel, ClientRowModelInit}, + xr_clients::XRClient, +}; +use adw::prelude::*; +use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; + +#[tracker::track] +pub struct ClientsBox { + has_clients: bool, + + #[tracker::do_not_track] + client_rows: AsyncFactoryVecDeque, +} + +#[derive(Debug)] +pub enum ClientsBoxMsg { + UpdateClients(Vec), +} + +#[relm4::component(pub)] +impl SimpleComponent for ClientsBox { + type Init = (); + type Input = ClientsBoxMsg; + type Output = (); + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_hexpand: true, + set_vexpand: false, + set_spacing: 12, + #[track = "model.changed(Self::has_clients())"] + set_visible: model.has_clients, + append: &clients_listbox, + } + } + + fn update(&mut self, message: Self::Input, _sender: ComponentSender) { + self.reset(); + + match message { + Self::Input::UpdateClients(clients) => { + self.set_has_clients(!clients.is_empty()); + let mut guard = self.client_rows.guard(); + let existing_clients: Vec = guard + .iter() + .filter_map(|optrow| optrow.map(|row| row.client.clone())) + .collect(); + let mut new_clients = clients.clone(); + new_clients.retain(|cl| !existing_clients.iter().any(|ecl| ecl == cl)); + // remove stale rows + { + let mut indexes_to_rm: Vec = Vec::new(); + for (i, row) in guard.iter().enumerate().rev() { + if let Some(row) = row + && clients.iter().any(|cl| cl == &row.client) + { + // row can be retained + } else { + indexes_to_rm.push(i); + } + } + for i in indexes_to_rm { + guard.remove(i); + } + } + if !self.has_clients { + return; + } + for cl in new_clients { + guard.push_back(ClientRowModelInit { client: cl.clone() }); + } + } + } + } + + fn init( + _init: Self::Init, + root: Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { + let clients_listbox = gtk::ListBox::builder() + .css_classes(["boxed-list"]) + .selection_mode(gtk::SelectionMode::None) + .build(); + + let model = Self { + tracker: 0, + has_clients: false, + client_rows: AsyncFactoryVecDeque::builder() + .launch(clients_listbox.clone()) + .detach(), + }; + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } +} diff --git a/src/ui/devices_box.rs b/src/ui/devices_box.rs index 64496b1..b42e718 100644 --- a/src/ui/devices_box.rs +++ b/src/ui/devices_box.rs @@ -1,12 +1,14 @@ use super::factories::device_row_factory::{DeviceRowModel, DeviceRowModelInit, DeviceRowState}; -use crate::xr_devices::{XRDevice, XRDeviceRole}; +use crate::{ + ui::factories::device_row_factory::DeviceRowModelMsg, + xr_devices::{XRDevice, XRDeviceRole}, +}; use adw::prelude::*; use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; #[tracker::track] pub struct DevicesBox { - #[no_eq] - devices: Vec, + has_devices: bool, #[tracker::do_not_track] device_rows: AsyncFactoryVecDeque, @@ -29,8 +31,8 @@ impl SimpleComponent for DevicesBox { set_hexpand: true, set_vexpand: false, set_spacing: 12, - #[track = "model.changed(Self::devices())"] - set_visible: !model.devices.is_empty(), + #[track = "model.changed(Self::has_devices())"] + set_visible: model.has_devices, append: &devices_listbox, } } @@ -40,18 +42,47 @@ impl SimpleComponent for DevicesBox { match message { Self::Input::UpdateDevices(devs) => { - self.set_devices(devs); + self.set_has_devices(!devs.is_empty()); let mut guard = self.device_rows.guard(); - guard.clear(); - if self.devices.is_empty() { + let existing_devs: Vec = guard + .iter() + .filter_map(|optrow| optrow.and_then(|row| row.device.clone())) + .collect(); + let mut new_devs = devs.clone(); + new_devs.retain(|d| !existing_devs.iter().any(|ed| ed == d)); + // remove stale rows + { + let mut indexes_to_rm: Vec = Vec::new(); + for (i, row) in guard.iter().enumerate().rev() { + if let Some(row) = row + && let Some(dev) = row.device.as_ref() + && devs.iter().any(|d| d == dev) + { + // row can be retained + } else { + indexes_to_rm.push(i); + } + } + for i in indexes_to_rm { + guard.remove(i); + } + } + if !self.has_devices { return; } - let mut has_head = false; - let mut has_left = false; - let mut has_right = false; + guard.broadcast(DeviceRowModelMsg::Update); + let mut has_head = existing_devs + .iter() + .any(|d| d.roles.contains(&XRDeviceRole::Head)); + let mut has_left = existing_devs + .iter() + .any(|d| d.roles.contains(&XRDeviceRole::Left)); + let mut has_right = existing_devs + .iter() + .any(|d| d.roles.contains(&XRDeviceRole::Right)); let mut models: Vec = Vec::new(); - for dev in &self.devices { - let mut row_model = DeviceRowModelInit::from(dev); + for dev in new_devs { + let mut row_model = DeviceRowModelInit::from(&dev); if !has_head && dev.roles.contains(&XRDeviceRole::Head) { has_head = true; if ["Simulated HMD", "Qwerty HMD"].contains(&dev.name.as_str()) { @@ -77,15 +108,15 @@ impl SimpleComponent for DevicesBox { } models.push(row_model); } + if !has_head { + models.push(DeviceRowModelInit::new_missing(XRDeviceRole::Head)); + } if !has_right { models.push(DeviceRowModelInit::new_missing(XRDeviceRole::Right)); } if !has_left { models.push(DeviceRowModelInit::new_missing(XRDeviceRole::Left)); } - if !has_head { - models.push(DeviceRowModelInit::new_missing(XRDeviceRole::Head)); - } models.sort_by(|m1, m2| m1.sort_index.cmp(&m2.sort_index)); @@ -108,7 +139,7 @@ impl SimpleComponent for DevicesBox { let model = Self { tracker: 0, - devices: vec![], + has_devices: false, device_rows: AsyncFactoryVecDeque::builder() .launch(devices_listbox.clone()) .detach(), diff --git a/src/ui/factories/client_row_factory.rs b/src/ui/factories/client_row_factory.rs new file mode 100644 index 0000000..c276402 --- /dev/null +++ b/src/ui/factories/client_row_factory.rs @@ -0,0 +1,54 @@ +#![allow(unused_assignments)] // vars named in view! macro are not correctly recognized as used + +use crate::xr_clients::XRClient; +use adw::prelude::*; +use relm4::{AsyncFactorySender, factory::AsyncFactoryComponent, prelude::*}; + +#[derive(Debug)] +pub struct ClientRowModel { + pub client: XRClient, +} + +#[derive(Debug, Clone)] +pub struct ClientRowModelInit { + pub client: XRClient, +} + +#[relm4::factory(async pub)] +impl AsyncFactoryComponent for ClientRowModel { + type Init = ClientRowModelInit; + type Input = (); + type Output = (); + type CommandOutput = (); + type ParentWidget = gtk::ListBox; + + view! { + root = adw::ActionRow { + set_title: &self.client.name, + } + } + + async fn update(&mut self, _message: Self::Input, _sender: AsyncFactorySender) -> () {} + + fn init_widgets( + &mut self, + _index: &DynamicIndex, + root: Self::Root, + _returned_widget: &::ReturnedWidget, + _sender: AsyncFactorySender, + ) -> Self::Widgets { + let widgets = view_output!(); + + widgets + } + + async fn init_model( + init: Self::Init, + _index: &DynamicIndex, + _sender: AsyncFactorySender, + ) -> Self { + Self { + client: init.client, + } + } +} diff --git a/src/ui/factories/device_row_factory.rs b/src/ui/factories/device_row_factory.rs index eb1739d..0fdd3c4 100644 --- a/src/ui/factories/device_row_factory.rs +++ b/src/ui/factories/device_row_factory.rs @@ -6,6 +6,7 @@ use crate::{ }; use adw::prelude::*; use relm4::{AsyncFactorySender, factory::AsyncFactoryComponent, prelude::*}; +use tracing::error; #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum DeviceRowState { @@ -33,13 +34,18 @@ impl DeviceRowState { } } +#[tracker::track] #[derive(Debug)] pub struct DeviceRowModel { + #[tracker::do_not_track] title: String, + #[tracker::do_not_track] subtitle: String, state: DeviceRowState, - suffix: Option, battery_status: Option, + brightness: Option, + #[tracker::do_not_track] + pub device: Option, } #[derive(Debug, Default, Clone)] @@ -47,8 +53,9 @@ pub struct DeviceRowModelInit { pub title: Option, pub subtitle: Option, pub state: Option, - pub suffix: Option, pub battery_status: Option, + pub device: Option, + pub brightness: Option, pub sort_index: u32, } @@ -66,6 +73,7 @@ impl From<&XRDevice> for DeviceRowModelInit { }), subtitle: Some(d.name.clone()), battery_status: d.battery.map(EnvisionBatteryStatus::from), + brightness: d.brightness, sort_index: u32::from( d.roles .iter() @@ -73,6 +81,7 @@ impl From<&XRDevice> for DeviceRowModelInit { .unwrap_or(&XRDeviceRole::GenericTracker), ) * 1000 + d.index, + device: Some(d.clone()), ..Default::default() } } @@ -89,10 +98,16 @@ impl DeviceRowModelInit { } } +#[derive(Debug, Clone, Copy)] +pub enum DeviceRowModelMsg { + Update, + BrightnessChanged(f32), +} + #[relm4::factory(async pub)] impl AsyncFactoryComponent for DeviceRowModel { type Init = DeviceRowModelInit; - type Input = (); + type Input = DeviceRowModelMsg; type Output = (); type CommandOutput = (); type ParentWidget = gtk::ListBox; @@ -102,7 +117,43 @@ impl AsyncFactoryComponent for DeviceRowModel { add_prefix: icon = >k::Image { set_icon_name: Some(self.state.icon()), }, + add_suffix: brightness_btn = >k::MenuButton { + add_css_class: "circular", + add_css_class: "flat", + set_icon_name: "display-brightness-symbolic", + set_valign: gtk::Align::Center, + #[track = "self.changed(Self::brightness())"] + set_visible: self.brightness.is_some(), + set_tooltip_text: Some("Adjust brightness"), + #[wrap(Some)] + set_popover: brightness_popover = >k::Popover { + gtk::Box { + set_margin_all: 6, + gtk::Scale { + set_width_request: 250, + set_digits: 3, + set_draw_value: true, + set_value_pos: gtk::PositionType::Right, + set_format_value_func: move |_, v| { + format!("{:.0}%", v*100.0) + }, + set_adjustment: adj = >k::Adjustment { + set_lower: 0.0, + set_upper: 1.0, + set_step_increment: 0.01, + set_page_increment: 0.1, + #[track = "self.changed(Self::brightness())"] + set_value: self.brightness.unwrap_or(0.0).into(), + connect_value_changed[sender] => move |adj| { + sender.input(Self::Input::BrightnessChanged(adj.value() as f32)); + } + } + }, + }, + }, + }, add_suffix: batt_info = >k::Box { + #[track = "self.changed(Self::battery_status())"] set_visible: self.battery_status.is_some(), set_orientation: gtk::Orientation::Horizontal, set_spacing: 6, @@ -112,6 +163,7 @@ impl AsyncFactoryComponent for DeviceRowModel { .map(|bs| bs.icon()).as_deref(), }, gtk::Label { + #[track = "self.changed(Self::battery_status())"] set_text: &self.battery_status .as_ref() .map(|bs| bs.to_string()) @@ -123,18 +175,51 @@ impl AsyncFactoryComponent for DeviceRowModel { } } + async fn update(&mut self, message: Self::Input, _sender: AsyncFactorySender) -> () { + self.reset(); + + match message { + Self::Input::Update => { + if let Some(dev) = self.device.as_mut() { + dev.update(); + } + let mut data: Option<(Option, Option)> = None; + if let Some(dev) = self.device.as_ref() { + data = Some((dev.battery.map(EnvisionBatteryStatus::from), dev.brightness)); + } + if let Some((batt, brightness)) = data { + self.set_battery_status(batt); + self.set_brightness(brightness); + } + } + Self::Input::BrightnessChanged(v) => { + if self.device.is_none() { + return; + } + if let Some(b) = self.brightness + && b == v + { + return; + } + self.set_brightness(Some(v)); + if let Some(dev) = self.device.as_ref() + && let Err(e) = dev.set_brightness(v) + { + error!("Failed to set device brightness: {e:?}"); + } + } + } + } + fn init_widgets( &mut self, _index: &DynamicIndex, root: Self::Root, _returned_widget: &::ReturnedWidget, - _sender: AsyncFactorySender, + sender: AsyncFactorySender, ) -> Self::Widgets { let widgets = view_output!(); - if let Some(suffix) = self.suffix.as_ref() { - widgets.root.add_suffix(suffix); - } if let Some(cls) = self.state.class_name() { widgets.root.add_css_class(cls); widgets.icon.add_css_class(cls); @@ -153,7 +238,9 @@ impl AsyncFactoryComponent for DeviceRowModel { subtitle: init.subtitle.unwrap_or_default(), state: init.state.unwrap_or_default(), battery_status: init.battery_status, - suffix: init.suffix, + brightness: init.brightness, + device: init.device, + tracker: 0, } } } diff --git a/src/ui/factories/mod.rs b/src/ui/factories/mod.rs index dfa3b3d..c399ff1 100644 --- a/src/ui/factories/mod.rs +++ b/src/ui/factories/mod.rs @@ -1,3 +1,4 @@ +pub mod client_row_factory; pub mod device_row_factory; pub mod env_var_row_factory; pub mod wivrn_encoder_group_factory; diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 7cd0614..4eae776 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -19,6 +19,7 @@ use crate::{ ui::{ alert::alert_w_widget, app::{PreferencesAction, ThemeManagerAction}, + clients_box::{ClientsBox, ClientsBoxMsg}, util::{copiable_code_snippet, warn_card}, }, util::{ @@ -29,6 +30,7 @@ use crate::{ steam_library_folder::chaperone_info_exists, }, wivrn_dbus, + xr_clients::XRClient, xr_devices::XRDevice, }; use adw::prelude::*; @@ -50,6 +52,8 @@ pub struct MainView { #[tracker::do_not_track] devices_box: Controller, #[tracker::do_not_track] + clients_box: Controller, + #[tracker::do_not_track] steamvr_calibration_box: Controller, #[tracker::do_not_track] openhmd_calibration_box: Controller, @@ -71,6 +75,7 @@ pub enum MainViewMsg { EnableDebugViewChanged(bool), UpdateSelectedProfile(Profile), UpdateDevices(Vec), + UpdateClients(Vec), UpdateXrServiceReady(bool), SetWivrnSupportsPairing(bool), SetWivrnPairingMode(bool), @@ -229,6 +234,7 @@ impl AsyncComponent for MainView { }, }, model.devices_box.widget(), + model.clients_box.widget(), gtk::Box { set_orientation: gtk::Orientation::Vertical, set_hexpand: true, @@ -531,6 +537,7 @@ impl AsyncComponent for MainView { .emit(OpenHmdCalibrationBoxMsg::XRServiceActiveChanged(active)); if !active { sender.input(Self::Input::UpdateDevices(vec![])); + sender.input(Self::Input::UpdateClients(vec![])); } self.steam_launch_options_box.sender().emit( SteamLaunchOptionsBoxMsg::UpdateXRServiceActive(show_launch_opts), @@ -568,6 +575,11 @@ impl AsyncComponent for MainView { .sender() .emit(DevicesBoxMsg::UpdateDevices(devs)); } + Self::Input::UpdateClients(clients) => { + self.clients_box + .sender() + .emit(ClientsBoxMsg::UpdateClients(clients)); + } Self::Input::UpdateXrServiceReady(ready) => { self.set_xrservice_ready(ready); } @@ -651,6 +663,7 @@ impl AsyncComponent for MainView { }) .detach(), devices_box: DevicesBox::builder().launch(()).detach(), + clients_box: ClientsBox::builder().launch(()).detach(), selected_profile: init.selected_profile.clone(), steamvr_calibration_box, openhmd_calibration_box, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e7f9ce9..5d251cd 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,6 +3,7 @@ mod alert; pub mod app; mod battery_status; mod build_window; +pub mod clients_box; pub mod cmdline_opts; mod debug_view; mod devices_box; diff --git a/src/xr_clients.rs b/src/xr_clients.rs new file mode 100644 index 0000000..7c1a960 --- /dev/null +++ b/src/xr_clients.rs @@ -0,0 +1,44 @@ +use libmonado::ClientLogic; +use std::rc::Rc; + +#[derive(Clone)] +pub struct XRClient { + pub id: u32, + pub name: String, + // client reference not necessary right now + // client: libmonado::ClientRc, +} + +impl std::fmt::Debug for XRClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XRClient") + .field("name", &self.name) + .finish_non_exhaustive() + } +} + +impl PartialEq for XRClient { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.name == other.name + } +} +impl Eq for XRClient {} + +impl XRClient { + pub fn from_libmonado(monado: Rc) -> Vec { + if let Ok(monado_clients) = libmonado::Monado::clients_rc(&monado) { + monado_clients + .into_iter() + .map(|mut cl| { + Self { + id: cl.id(), + name: cl.name().unwrap_or("".into()), + // client: cl, + } + }) + .collect() + } else { + Vec::default() + } + } +} diff --git a/src/xr_devices.rs b/src/xr_devices.rs index 8ae622c..a5d1f52 100644 --- a/src/xr_devices.rs +++ b/src/xr_devices.rs @@ -1,5 +1,5 @@ -use libmonado::{self, BatteryStatus, DeviceRole}; -use std::{collections::HashMap, fmt::Display, slice::Iter}; +use libmonado::{self, BatteryStatus, DeviceLogic, DeviceRole, MndProperty, MndResult}; +use std::{collections::HashMap, fmt::Display, rc::Rc, slice::Iter}; use tracing::error; #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -231,7 +231,7 @@ impl From for XRDeviceRole { } } -#[derive(Debug, Clone, Default)] +#[derive(Clone)] pub struct XRDevice { pub roles: Vec, pub name: String, @@ -239,14 +239,44 @@ pub struct XRDevice { pub index: u32, pub serial: Option, pub battery: Option, + pub brightness: Option, + pub has_brightness_control: bool, + device: libmonado::DeviceRc, } +impl std::fmt::Debug for XRDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XRDevice") + .field("roles", &self.roles) + .field("name", &self.name) + .field("id", &self.id) + .field("index", &self.index) + .field("serial", &self.serial) + .field("battery", &self.battery) + .field("has_brightness_control", &self.has_brightness_control) + .finish_non_exhaustive() + } +} + +impl PartialEq for XRDevice { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.has_brightness_control == other.has_brightness_control + && self.name == other.name + && self.roles == other.roles + } +} +impl Eq for XRDevice {} + impl XRDevice { - pub fn from_libmonado(monado: &libmonado::Monado) -> Vec { - if let Ok(monado_devs) = monado.devices() { + pub fn from_libmonado(monado: Rc) -> Vec { + if let Ok(monado_devs) = libmonado::Monado::devices_rc(&monado) { let mut devs: HashMap = monado_devs .into_iter() .map(|dev| { + let has_brightness_control = dev + .get_info_bool(MndProperty::PropertySupportsBrightnessBool) + .unwrap_or(false); ( dev.index, Self { @@ -257,8 +287,15 @@ impl XRDevice { .battery_status() .ok() .and_then(|bs| if bs.present { Some(bs) } else { None }), - name: dev.name, + has_brightness_control, + brightness: if has_brightness_control { + dev.brightness().ok() + } else { + None + }, + name: dev.name.clone(), roles: Vec::default(), + device: dev.clone(), }, ) }) @@ -290,4 +327,25 @@ impl XRDevice { Vec::default() } } + + pub fn update(&mut self) { + self.battery = self + .device + .battery_status() + .ok() + .and_then(|bs| if bs.present { Some(bs) } else { None }); + self.brightness = if self.has_brightness_control { + self.device.brightness().ok() + } else { + None + }; + } + + /// IMPORTANT: value must be between 0 and 1; values outside this range + /// will be clamped + pub fn set_brightness(&self, value: f32) -> Result<(), MndResult> { + // clamp the value for bonus correctness + let value = value.clamp(0.0, 1.0); + self.device.set_brightness(value, false) + } }