diff --git a/src/ui/app.rs b/src/ui/app.rs index b905c9d..3daa827 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -9,6 +9,7 @@ use super::job_worker::JobWorker; use super::libsurvive_setup_window::LibsurviveSetupWindow; use super::main_view::MainViewMsg; use super::util::open_with_default_handler; +use super::wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg}; use crate::builders::build_basalt::get_build_basalt_jobs; use crate::builders::build_libsurvive::get_build_libsurvive_jobs; use crate::builders::build_mercury::get_build_mercury_job; @@ -102,6 +103,9 @@ pub struct App { fbt_config_editor: Option>, #[tracker::do_not_track] libmonado: Option, + + #[tracker::do_not_track] + wivrn_conf_editor: Option>, } #[derive(Debug)] @@ -128,6 +132,7 @@ pub enum Msg { ConfigFbt, DebugOpenPrefix, DebugOpenData, + OpenWivrnConfig, } impl App { @@ -632,6 +637,15 @@ impl SimpleComponent for App { self.get_selected_profile().prefix )); } + Msg::OpenWivrnConfig => { + let editor = WivrnConfEditor::builder() + .launch(WivrnConfEditorInit { + root_win: self.app_win.clone().upcast::(), + }) + .detach(); + editor.emit(WivrnConfEditorMsg::Present); + self.wivrn_conf_editor = Some(editor); + } } } @@ -669,6 +683,7 @@ impl SimpleComponent for App { } let mut model = App { + tracker: 0, application: init.application, app_win: root.clone(), inhibit_id: None, @@ -687,6 +702,7 @@ impl SimpleComponent for App { MainViewOutMsg::DeleteProfile => Msg::DeleteProfile, MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p), MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup, + MainViewOutMsg::OpenWivrnConfig => Msg::OpenWivrnConfig, }), debug_view: DebugView::builder().launch(DebugViewInit {}).forward( sender.input_sender(), @@ -712,7 +728,6 @@ impl SimpleComponent for App { setcap_confirm_dialog, enable_debug_view: config.debug_view_enabled, config, - tracker: 0, profiles, xrservice_worker: None, build_worker: None, @@ -720,6 +735,7 @@ impl SimpleComponent for App { fbt_config_editor: None, restart_xrservice: false, libmonado: None, + wivrn_conf_editor: None, }; let widgets = view_output!(); diff --git a/src/ui/factories/mod.rs b/src/ui/factories/mod.rs index e311478..6ff4479 100644 --- a/src/ui/factories/mod.rs +++ b/src/ui/factories/mod.rs @@ -1,3 +1,4 @@ pub mod device_row_factory; pub mod env_var_row_factory; pub mod tracker_role_group_factory; +pub mod wivrn_encoder_group_factory; diff --git a/src/ui/factories/wivrn_encoder_group_factory.rs b/src/ui/factories/wivrn_encoder_group_factory.rs new file mode 100644 index 0000000..6fd84a2 --- /dev/null +++ b/src/ui/factories/wivrn_encoder_group_factory.rs @@ -0,0 +1,196 @@ +use crate::{ + file_builders::wivrn_config::{Codec, Encoder, WivrnConfEncoder}, + ui::{ + preference_rows::{combo_row, number_entry_row, spin_row}, + wivrn_conf_editor::WivrnConfEditorMsg, + }, +}; +use relm4::{ + adw::prelude::*, factory::AsyncFactoryComponent, gtk::prelude::*, prelude::*, + AsyncFactorySender, +}; +use uuid::Uuid; + +#[derive(Debug)] +pub struct WivrnEncoderModel { + pub encoder_conf: WivrnConfEncoder, + pub uid: String, +} + +#[derive(Debug)] +pub enum WivrnEncoderModelMsg { + EncoderChanged(u32), + CodecChanged(u32), + BitrateChanged(Option), + WidthChanged(Option), + HeightChanged(Option), + GroupChanged(Option), + Delete, +} + +#[derive(Debug)] +pub enum WivrnEncoderModelOutMsg { + Delete(String), +} + +#[derive(Debug, Default)] +pub struct WivrnEncoderModelInit { + pub encoder_conf: Option, +} + +#[relm4::factory(async pub)] +impl AsyncFactoryComponent for WivrnEncoderModel { + type Init = WivrnEncoderModelInit; + type Input = WivrnEncoderModelMsg; + type Output = WivrnEncoderModelOutMsg; + type CommandOutput = (); + type ParentInput = WivrnConfEditorMsg; + type ParentWidget = adw::PreferencesPage; + + view! { + root = adw::PreferencesGroup { + set_title: "Encoder", + #[wrap(Some)] + set_header_suffix: delete_btn = >k::Button { + set_label: "Delete", + add_css_class: "destructive-action", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Delete) + }, + }, + add: encoder_combo = &combo_row( + "Encoder", + Some("x264: CPU based h264 encoding\nNVEnc: Nvidia GPU encoding\nVAAPI: Intel or AMD GPU encoding"), + &self.encoder_conf.encoder.to_string(), + Encoder::iter().map(Encoder::to_string).collect::>(), + { + let sender = sender.clone(); + move |row| { + sender.input(Self::Input::EncoderChanged(row.selected())); + } + } + ) -> adw::ComboRow, + add: codec_combo = &combo_row( + "Codec", + None, + &self.encoder_conf.codec.to_string(), + Codec::iter().map(Codec::to_string).collect::>(), + { + let sender = sender.clone(); + move |row| { + sender.input(Self::Input::CodecChanged(row.selected())); + } + } + ) -> adw::ComboRow, + add: bitrate_row = &number_entry_row( + "Bitrate", + &self.encoder_conf.bitrate + .and_then(|n| Some(n.to_string())) + .unwrap_or_default(), + false, + { + let sender = sender.clone(); + move |row| { + let txt = row.text(); + sender.input(Self::Input::BitrateChanged( + if txt.is_empty() { + None + } else { + Some(txt.parse::().unwrap()) + } + )); + } + } + ) -> adw::EntryRow, + add: width_row = &spin_row( + "Width", + None, + self.encoder_conf.width.unwrap_or(1.0).into(), + 0.0, + 1.0, + 0.01, + { + let sender = sender.clone(); + move |adj| { + sender.input(Self::Input::WidthChanged( + Some(adj.value() as f32) + )); + } + } + ) -> adw::SpinRow, + add: height_row = &spin_row( + "Height", + None, + self.encoder_conf.height.unwrap_or(1.0).into(), + 0.0, + 1.0, + 0.01, + { + let sender = sender.clone(); + move |adj| { + sender.input(Self::Input::HeightChanged( + Some(adj.value() as f32) + )); + } + } + ) -> adw::SpinRow, + add: group_row = &spin_row( + "Group", + None, + self.encoder_conf.group.unwrap_or(0).into(), + 0.0, + 99999.0, + 1.0, + { + let sender = sender.clone(); + move |adj| { + sender.input(Self::Input::GroupChanged( + Some(adj.value().trunc() as i32) + )); + } + } + ) -> adw::SpinRow, + } + } + + async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender) { + match message { + Self::Input::EncoderChanged(idx) => { + self.encoder_conf.encoder = Encoder::as_vec().get(idx as usize).unwrap().clone(); + } + Self::Input::CodecChanged(idx) => { + self.encoder_conf.codec = Codec::as_vec().get(idx as usize).unwrap().clone(); + } + Self::Input::BitrateChanged(val) => { + self.encoder_conf.bitrate = val; + } + Self::Input::WidthChanged(val) => { + self.encoder_conf.width = val; + } + Self::Input::HeightChanged(val) => { + self.encoder_conf.height = val; + } + Self::Input::GroupChanged(val) => { + self.encoder_conf.group = val; + } + Self::Input::Delete => sender.output(Self::Output::Delete(self.uid.clone())), + } + } + + fn forward_to_parent(output: Self::Output) -> Option { + Some(match output { + Self::Output::Delete(id) => Self::ParentInput::DeleteEncoder(id), + }) + } + + async fn init_model( + init: Self::Init, + _index: &DynamicIndex, + _sender: AsyncFactorySender, + ) -> Self { + Self { + encoder_conf: init.encoder_conf.unwrap_or_default(), + uid: Uuid::new_v4().to_string(), + } + } +} diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index c793c98..930ca18 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -11,7 +11,7 @@ use crate::gpu_profile::{ get_amd_gpu_power_profile, get_first_amd_gpu, get_set_amd_vr_pow_prof_cmd, GpuPowerProfile, GpuSysDrm, }; -use crate::profile::{LighthouseDriver, Profile}; +use crate::profile::{LighthouseDriver, Profile, XRServiceType}; use crate::steamvr_utils::chaperone_info_exists; use crate::ui::app::{ AboutAction, BuildProfileAction, BuildProfileCleanAction, BuildProfileCleanDebugAction, @@ -84,6 +84,7 @@ pub enum MainViewOutMsg { DeleteProfile, SaveProfile(Profile), OpenLibsurviveSetup, + OpenWivrnConfig, } pub struct MainViewInit { @@ -338,6 +339,32 @@ impl SimpleComponent for MainView { }, model.steam_launch_options_box.widget(), model.install_wivrn_box.widget(), + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + set_vexpand: false, + set_spacing: 12, + add_css_class: "card", + add_css_class: "padded", + #[track = "model.changed(Self::selected_profile())"] + set_visible: model.selected_profile.xrservice_type == XRServiceType::Wivrn, + gtk::Label { + add_css_class: "heading", + set_hexpand: true, + set_xalign: 0.0, + set_label: "Configure WiVRn", + set_wrap: true, + set_wrap_mode: gtk::pango::WrapMode::Word, + }, + gtk::Button { + add_css_class: "suggested-action", + set_label: "Configure", + set_halign: gtk::Align::End, + connect_clicked[sender] => move |_| { + sender.output(Self::Output::OpenWivrnConfig).expect("Sender output failed"); + } + }, + }, model.steamvr_calibration_box.widget(), gtk::Box { set_orientation: gtk::Orientation::Vertical, diff --git a/src/ui/preference_rows.rs b/src/ui/preference_rows.rs index bb9294b..9947d3e 100644 --- a/src/ui/preference_rows.rs +++ b/src/ui/preference_rows.rs @@ -122,6 +122,40 @@ pub fn number_entry_row( row } +pub fn spin_row( + title: &str, + subtitle: Option<&str>, + val: f64, + min: f64, + max: f64, + min_increment: f64, + cb: F, +) -> adw::SpinRow { + let adj = gtk::Adjustment::builder() + .lower(min) + .upper(max) + .step_increment(min_increment) + .page_increment(min_increment) + .value(val) + .build(); + let row = adw::SpinRow::builder() + .title(title) + .adjustment(&adj) + .digits(if min_increment == 1.0 { + 0 + } else { + min_increment.to_string().len() - 2 + } as u32) + .build(); + if let Some(sub) = subtitle { + row.set_subtitle(sub); + } + + adj.connect_value_changed(cb); + + row +} + pub fn path_row) + 'static + Clone>( title: &str, description: Option<&str>, diff --git a/src/ui/wivrn_conf_editor.rs b/src/ui/wivrn_conf_editor.rs index bc44305..ff9c9ed 100644 --- a/src/ui/wivrn_conf_editor.rs +++ b/src/ui/wivrn_conf_editor.rs @@ -1,8 +1,10 @@ -use crate::file_builders::wivrn_config::{ - dump_wivrn_config, get_wivrn_config, Encoder, WivrnConfig, +use super::factories::wivrn_encoder_group_factory::{WivrnEncoderModel, WivrnEncoderModelInit}; +use crate::{ + file_builders::wivrn_config::{dump_wivrn_config, get_wivrn_config, WivrnConfig}, + ui::preference_rows::spin_row, }; use adw::prelude::*; -use relm4::prelude::*; +use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; #[tracker::track] pub struct WivrnConfEditor { @@ -10,19 +12,19 @@ pub struct WivrnConfEditor { #[tracker::do_not_track] win: Option, #[tracker::do_not_track] - scalex_entry: Option, + pub encoder_models: Option>, #[tracker::do_not_track] - scaley_entry: Option, + pub scalex_row: Option, #[tracker::do_not_track] - encoder_combo: Option, - #[tracker::do_not_track] - bitrate_entry: Option, + pub scaley_row: Option, } #[derive(Debug)] pub enum WivrnConfEditorMsg { Present, Save, + AddEncoder, + DeleteEncoder(String), } pub struct WivrnConfEditorInit { @@ -70,73 +72,50 @@ impl SimpleComponent for WivrnConfEditor { set_content: pref_page = &adw::PreferencesPage { set_hexpand: true, set_vexpand: true, + set_description: "WiVRn Configuration Documentation", add: scalegrp = &adw::PreferencesGroup { set_title: "Scale", set_description: Some("Render resolution scale. 1.0 is 100%."), - #[name(scalex_entry)] - adw::EntryRow { - set_title: "Scale X", - #[track = "model.changed(Self::conf())"] - set_text: match model.conf.scale { - Some([x, _]) => x.to_string(), - None => "".to_string(), - }.as_str(), - set_input_purpose: gtk::InputPurpose::Number, - }, - #[name(scaley_entry)] - adw::EntryRow { - set_title: "Scale Y", - #[track = "model.changed(Self::conf())"] - set_text: match model.conf.scale { - Some([_, y]) => y.to_string(), - None => "".to_string(), - }.as_str(), - set_input_purpose: gtk::InputPurpose::Number, - }, - }, - add: encgrp = &adw::PreferencesGroup { - set_title: "Encoder", - #[name(encoder_combo)] - adw::ComboRow { - set_title: "Encoder", - set_subtitle: "x264: CPU based h264 encoding\n\nNVEnc: Nvidia GPU encoding\n\nVAAPI: Intel or AMD GPU encoding", - set_model: Some(>k::StringList::new( - Encoder::iter() - .map(Encoder::to_string) - .collect::>() - .iter() - .map(String::as_str) - .collect::>() - .as_slice() - )), - #[track = "model.changed(Self::conf())"] - set_selected: model.conf.encoders.get(0).unwrap().encoder.as_number(), - }, - #[name(bitrate_entry)] - adw::EntryRow { - set_title: "Bitrate", - #[track = "model.changed(Self::conf())"] - set_text: match model.conf.encoders.get(0).unwrap().bitrate { - Some(br) => br.to_string(), - None => "".to_string() - }.as_str(), - set_input_purpose: gtk::InputPurpose::Number, - }, - }, - add: save_grp = &adw::PreferencesGroup { - add: save_box = >k::Box { - set_orientation: gtk::Orientation::Vertical, - set_hexpand: true, - gtk::Button { - set_halign: gtk::Align::Center, - set_label: "Save", - add_css_class: "pill", - add_css_class: "suggested-action", - connect_clicked[sender] => move |_| { - sender.input(Self::Input::Save); - }, + add: scalex_row = &spin_row( + "Scale X", + None, + match model.conf.scale { + Some([x, _]) => x.into(), + None => 1.0, }, - } + 0.0, + 1.0, + 0.01, + move |_| {} + ) -> adw::SpinRow, + add: scaley_row = &spin_row( + "Scale Y", + None, + match model.conf.scale { + Some([_, y]) => y.into(), + None => 1.0, + }, + 0.0, + 1.0, + 0.01, + move |_| {} + ) -> adw::SpinRow, + }, + add: encodersrgp = &adw::PreferencesGroup { + set_title: "Encoders", + adw::ActionRow { + set_title: "Add encoder", + add_suffix: add_encoder_btn = >k::Button { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + add_css_class: "suggested-action", + set_icon_name: "list-add-symbolic", + set_tooltip_text: Some("Add encoder"), + connect_clicked[sender] => move |_| { + sender.input(Self::Input::AddEncoder) + } + }, + }, }, } } @@ -152,34 +131,41 @@ impl SimpleComponent for WivrnConfEditor { self.win.as_ref().unwrap().present(); } Self::Input::Save => { - let scalex = self.scalex_entry.as_ref().unwrap().text().parse::(); - let scaley = self.scaley_entry.as_ref().unwrap().text().parse::(); - if scalex.is_ok() && scaley.is_ok() { - self.conf.scale = Some([*scalex.as_ref().unwrap(), *scaley.as_ref().unwrap()]); - } - if scalex.is_ok() || scaley.is_ok() { - let scale = scalex.unwrap_or(scaley.unwrap()); - self.conf.scale = Some([scale, scale]); - } else { - self.conf.scale = None - } - - let mut enc = self.conf.encoders.remove(0); - let bitrate = self.bitrate_entry.as_ref().unwrap().text().parse::(); - if let Ok(br) = bitrate { - enc.bitrate = Some(br); - } - let encoders = Encoder::as_vec(); - let encoder = - encoders.get(self.encoder_combo.as_ref().unwrap().selected() as usize); - if let Some(e) = encoder { - enc.encoder = e.clone(); - } - self.conf.encoders.insert(0, enc); + let x = self.scalex_row.as_ref().unwrap().adjustment().value(); + let y = self.scaley_row.as_ref().unwrap().adjustment().value(); + Some([x as f32, y as f32]); + self.conf.encoders = self + .encoder_models + .as_ref() + .unwrap() + .iter() + .filter(Option::is_some) + .map(|m| m.as_ref().unwrap().encoder_conf.clone()) + .collect(); dump_wivrn_config(&self.conf); self.win.as_ref().unwrap().close(); } + Self::Input::AddEncoder => { + self.encoder_models + .as_mut() + .unwrap() + .guard() + .push_back(WivrnEncoderModelInit::default()); + } + Self::Input::DeleteEncoder(id) => { + let idx_opt = self + .encoder_models + .as_ref() + .unwrap() + .iter() + .position(|m_opt| m_opt.is_some_and(|m| m.uid == id)); + if let Some(idx) = idx_opt { + self.encoder_models.as_mut().unwrap().guard().remove(idx); + } else { + eprintln!("Couldn't find encoder model with id {id}"); + } + } } } @@ -190,21 +176,28 @@ impl SimpleComponent for WivrnConfEditor { ) -> ComponentParts { let mut model = Self { conf: get_wivrn_config(), + encoder_models: None, win: None, - scalex_entry: None, - scaley_entry: None, - bitrate_entry: None, - encoder_combo: None, + scalex_row: None, + scaley_row: None, tracker: 0, }; let widgets = view_output!(); + model.scalex_row = Some(widgets.scalex_row.clone()); + model.scaley_row = Some(widgets.scaley_row.clone()); + + let mut encoder_models: AsyncFactoryVecDeque = + AsyncFactoryVecDeque::new(widgets.pref_page.clone(), sender.input_sender()); + for encoder_conf in model.conf.encoders.clone() { + encoder_models.guard().push_back(WivrnEncoderModelInit { + encoder_conf: Some(encoder_conf), + }); + } + model.encoder_models = Some(encoder_models); + model.win = Some(widgets.win.clone()); - model.scalex_entry = Some(widgets.scalex_entry.clone()); - model.scaley_entry = Some(widgets.scaley_entry.clone()); - model.bitrate_entry = Some(widgets.bitrate_entry.clone()); - model.encoder_combo = Some(widgets.encoder_combo.clone()); ComponentParts { model, widgets } }