From cd0e55aa9b336a23be32ef297e2eb71673900b41 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Tue, 26 Dec 2023 07:57:37 +0000 Subject: [PATCH 1/8] feat: normal window for wivrn config editor --- src/ui/wivrn_conf_editor.rs | 161 ++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 71 deletions(-) diff --git a/src/ui/wivrn_conf_editor.rs b/src/ui/wivrn_conf_editor.rs index f4fc860..460e533 100644 --- a/src/ui/wivrn_conf_editor.rs +++ b/src/ui/wivrn_conf_editor.rs @@ -8,7 +8,7 @@ use relm4::prelude::*; pub struct WivrnConfEditor { conf: WivrnConfig, #[tracker::do_not_track] - win: Option, + win: Option, #[tracker::do_not_track] scalex_entry: Option, #[tracker::do_not_track] @@ -37,81 +37,100 @@ impl SimpleComponent for WivrnConfEditor { view! { #[name(win)] - adw::PreferencesWindow { - set_hide_on_close: true, + adw::Window { set_modal: true, set_transient_for: Some(&init.root_win), set_title: Some("WiVRn Configuration"), - add: mainpage = &adw::PreferencesPage { - 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); - }, + set_default_height: 500, + set_default_width: 600, + adw::ToolbarView { + set_top_bar_style: adw::ToolbarStyle::Flat, + set_hexpand: true, + set_vexpand: true, + add_top_bar: top_bar = &adw::HeaderBar { + set_vexpand: false, + pack_end: save_btn = >k::Button { + set_label: "Save", + add_css_class: "suggested-action", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Save); }, - } + }, }, - }, + #[wrap(Some)] + set_content: pref_page = &adw::PreferencesPage { + set_hexpand: true, + set_vexpand: true, + 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); + }, + }, + } + }, + } + } } } From e8220b410ff432b634627b507e47b837725b3b7c Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Tue, 26 Dec 2023 08:03:23 +0000 Subject: [PATCH 2/8] feat: more explicit cancel button for profile editor and wivrn config editor --- src/ui/profile_editor.rs | 9 +++++++++ src/ui/wivrn_conf_editor.rs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/ui/profile_editor.rs b/src/ui/profile_editor.rs index 20fbdc9..20e1ed2 100644 --- a/src/ui/profile_editor.rs +++ b/src/ui/profile_editor.rs @@ -66,6 +66,8 @@ impl SimpleComponent for ProfileEditor { set_vexpand: true, add_top_bar: top_bar = &adw::HeaderBar { set_vexpand: false, + set_show_end_title_buttons: false, + set_show_start_title_buttons: false, pack_end: save_btn = >k::Button { set_label: "Save", add_css_class: "suggested-action", @@ -73,6 +75,13 @@ impl SimpleComponent for ProfileEditor { sender.input(Self::Input::SaveProfile); }, }, + pack_start: cancel_btn = >k::Button { + set_label: "Cancel", + add_css_class: "destructive-action", + connect_clicked[win] => move |_| { + win.close(); + } + }, }, #[wrap(Some)] set_content: pref_page = &adw::PreferencesPage { diff --git a/src/ui/wivrn_conf_editor.rs b/src/ui/wivrn_conf_editor.rs index 460e533..bc44305 100644 --- a/src/ui/wivrn_conf_editor.rs +++ b/src/ui/wivrn_conf_editor.rs @@ -49,6 +49,8 @@ impl SimpleComponent for WivrnConfEditor { set_vexpand: true, add_top_bar: top_bar = &adw::HeaderBar { set_vexpand: false, + set_show_end_title_buttons: false, + set_show_start_title_buttons: false, pack_end: save_btn = >k::Button { set_label: "Save", add_css_class: "suggested-action", @@ -56,6 +58,13 @@ impl SimpleComponent for WivrnConfEditor { sender.input(Self::Input::Save); }, }, + pack_start: cancel_btn = >k::Button { + set_label: "Cancel", + add_css_class: "destructive-action", + connect_clicked[win] => move |_| { + win.close(); + } + }, }, #[wrap(Some)] set_content: pref_page = &adw::PreferencesPage { From 034f7d7e38e391e209ffacd063a14628b485800d Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Tue, 26 Dec 2023 11:29:06 +0000 Subject: [PATCH 3/8] feat: some traits and functions for wivrn config and related enums --- src/file_builders/wivrn_config.rs | 47 +++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/file_builders/wivrn_config.rs b/src/file_builders/wivrn_config.rs index 1802e97..17ad4be 100644 --- a/src/file_builders/wivrn_config.rs +++ b/src/file_builders/wivrn_config.rs @@ -24,11 +24,11 @@ impl Display for Encoder { } impl Encoder { - pub fn iter() -> Iter<'static, Encoder> { + pub fn iter() -> Iter<'static, Self> { [Self::X264, Self::Nvenc, Self::Vaapi].iter() } - pub fn as_vec() -> Vec { + pub fn as_vec() -> Vec { vec![Self::X264, Self::Nvenc, Self::Vaapi] } @@ -50,6 +50,36 @@ pub enum Codec { Hevc, } +impl Display for Codec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::H264 => "h264", + Self::H265 => "h265", + Self::Avc => "AVC", + Self::Hevc => "HEVC", + }) + } +} + +impl Codec { + pub fn iter() -> Iter<'static, Self> { + [Self::H264, Self::H265, Self::Avc, Self::Hevc].iter() + } + + pub fn as_vec() -> Vec { + vec![Self::H264, Self::H265, Self::Avc, Self::Hevc] + } + + pub fn as_number(&self) -> u32 { + match self { + Self::H264 => 0, + Self::H265 => 1, + Self::Avc => 2, + Self::Hevc => 3, + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WivrnConfEncoder { pub encoder: Encoder, @@ -64,6 +94,19 @@ pub struct WivrnConfEncoder { pub group: Option, } +impl Default for WivrnConfEncoder { + fn default() -> Self { + Self { + encoder: Encoder::X264, + codec: Codec::H264, + bitrate: None, + width: None, + height: None, + group: None, + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WivrnConfig { #[serde(skip_serializing_if = "Option::is_none")] From 41fa38acf5957db68d170b0033867084d7d2a367 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Tue, 26 Dec 2023 11:29:32 +0000 Subject: [PATCH 4/8] feat: number entry adw row with automatic number filtering --- src/ui/preference_rows.rs | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/ui/preference_rows.rs b/src/ui/preference_rows.rs index 171b9b0..bb9294b 100644 --- a/src/ui/preference_rows.rs +++ b/src/ui/preference_rows.rs @@ -60,6 +60,68 @@ pub fn entry_row( row } +fn is_int(t: &str) -> bool { + t.find(|c: char| !c.is_digit(10)).is_none() +} + +fn convert_to_int(t: &str) -> String { + t.trim().chars().filter(|c| c.is_digit(10)).collect() +} + +fn is_float(t: &str) -> bool { + let mut has_dot = false; + for c in t.chars() { + if c == '.' { + if has_dot { + return false; + } + has_dot = true; + } else if !c.is_digit(10) { + return false; + } + } + true +} + +fn convert_to_float(t: &str) -> String { + let mut s = String::new(); + let mut has_dot = false; + for c in t.trim().chars() { + if c.is_digit(10) { + s.push(c); + } else if c == '.' && !has_dot { + s.push(c); + has_dot = true; + } + } + s +} + +pub fn number_entry_row( + title: &str, + value: &str, + float: bool, + cb: F, +) -> adw::EntryRow { + let validator = if float { is_float } else { is_int }; + let converter = if float { + convert_to_float + } else { + convert_to_int + }; + let row = entry_row(title, value, move |row| { + let txt_gstr = row.text(); + let txt = txt_gstr.as_str(); + if validator(txt) { + cb(row) + } else { + row.set_text(&converter(txt)); + } + }); + row.set_input_purpose(gtk::InputPurpose::Number); + row +} + pub fn path_row) + 'static + Clone>( title: &str, description: Option<&str>, @@ -147,3 +209,33 @@ pub fn combo_row( row } + +#[cfg(test)] +mod tests { + use crate::ui::preference_rows::{convert_to_float, is_float}; + + #[test] + fn accepts_float() { + assert!(is_float("132.1425")); + } + + #[test] + fn rejects_float_with_many_dots() { + assert!(!is_float("132.142.5")); + } + + #[test] + fn accepts_float_without_dots() { + assert!(is_float("1321425")); + } + + #[test] + fn rejects_float_with_alphas() { + assert!(!is_float("123.34a65")); + } + + #[test] + fn converts_to_float() { + assert_eq!(convert_to_float("123a.435.123"), "123.435123"); + } +} From 1ace5564deb4613dcb0d15a9d0c1487091780883 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Tue, 26 Dec 2023 11:39:24 +0000 Subject: [PATCH 5/8] feat: remove functionally duplicate codecs --- src/file_builders/wivrn_config.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/file_builders/wivrn_config.rs b/src/file_builders/wivrn_config.rs index 17ad4be..71ad055 100644 --- a/src/file_builders/wivrn_config.rs +++ b/src/file_builders/wivrn_config.rs @@ -46,8 +46,6 @@ impl Encoder { pub enum Codec { H264, H265, - Avc, - Hevc, } impl Display for Codec { @@ -55,27 +53,23 @@ impl Display for Codec { f.write_str(match self { Self::H264 => "h264", Self::H265 => "h265", - Self::Avc => "AVC", - Self::Hevc => "HEVC", }) } } impl Codec { pub fn iter() -> Iter<'static, Self> { - [Self::H264, Self::H265, Self::Avc, Self::Hevc].iter() + [Self::H264, Self::H265].iter() } pub fn as_vec() -> Vec { - vec![Self::H264, Self::H265, Self::Avc, Self::Hevc] + vec![Self::H264, Self::H265] } pub fn as_number(&self) -> u32 { match self { Self::H264 => 0, Self::H265 => 1, - Self::Avc => 2, - Self::Hevc => 3, } } } From afd51184ef41b8481e1bc9049be5224f53703f63 Mon Sep 17 00:00:00 2001 From: GabMus Date: Wed, 27 Dec 2023 09:03:52 +0000 Subject: [PATCH 6/8] feat: wivrn config editor --- src/ui/app.rs | 18 +- src/ui/factories/mod.rs | 1 + .../factories/wivrn_encoder_group_factory.rs | 196 +++++++++++++++++ src/ui/main_view.rs | 29 ++- src/ui/preference_rows.rs | 34 +++ src/ui/wivrn_conf_editor.rs | 199 +++++++++--------- 6 files changed, 372 insertions(+), 105 deletions(-) create mode 100644 src/ui/factories/wivrn_encoder_group_factory.rs 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 } } From 99a6145012c7d2e89d1488d9ca464410d189fbc8 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Wed, 27 Dec 2023 09:18:28 +0000 Subject: [PATCH 7/8] chore: remove unused import --- src/ui/main_view.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 930ca18..fa1a528 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -5,7 +5,6 @@ use super::profile_editor::{ProfileEditor, ProfileEditorMsg, ProfileEditorOutMsg use super::steam_launch_options_box::{SteamLaunchOptionsBox, SteamLaunchOptionsBoxMsg}; use super::steamvr_calibration_box::SteamVrCalibrationBox; use crate::config::Config; -use crate::constants::APP_NAME; use crate::file_utils::mount_has_nosuid; use crate::gpu_profile::{ get_amd_gpu_power_profile, get_first_amd_gpu, get_set_amd_vr_pow_prof_cmd, GpuPowerProfile, From bd8ecb645caf7a230f3a8a02dd114aae139400d6 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Wed, 27 Dec 2023 09:20:17 +0000 Subject: [PATCH 8/8] chore: newlines to facilitate merges --- src/ui/main_view.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index fa1a528..2e1dbdb 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -336,6 +336,7 @@ impl SimpleComponent for MainView { } }, }, + model.steam_launch_options_box.widget(), model.install_wivrn_box.widget(), gtk::Box { @@ -365,6 +366,7 @@ impl SimpleComponent for MainView { }, }, model.steamvr_calibration_box.widget(), + gtk::Box { set_orientation: gtk::Orientation::Vertical, set_hexpand: true,