diff --git a/src/file_builders/wivrn_config.rs b/src/file_builders/wivrn_config.rs index 1802e97..71ad055 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] } @@ -46,8 +46,32 @@ impl Encoder { pub enum Codec { H264, H265, - Avc, - 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", + }) + } +} + +impl Codec { + pub fn iter() -> Iter<'static, Self> { + [Self::H264, Self::H265].iter() + } + + pub fn as_vec() -> Vec { + vec![Self::H264, Self::H265] + } + + pub fn as_number(&self) -> u32 { + match self { + Self::H264 => 0, + Self::H265 => 1, + } + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -64,6 +88,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")] 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 2fe347c..5f067c5 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, @@ -87,6 +87,7 @@ pub enum MainViewOutMsg { DeleteProfile, SaveProfile(Profile), OpenLibsurviveSetup, + OpenWivrnConfig, } pub struct MainViewInit { @@ -343,6 +344,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 { @@ -383,26 +410,6 @@ impl SimpleComponent for MainView { } }, }, - gtk::Label { - add_css_class: "dim-label", - set_hexpand: true, - set_label: concat!( - "Libsurvive needs to import your SteamVR calibration to work ", - "properly. You need to have used SteamVR with this setup ", - "before to be able to import its calibration." - ), - set_xalign: 0.0, - set_wrap: true, - set_wrap_mode: gtk::pango::WrapMode::Word, - }, - gtk::Button { - add_css_class: "suggested-action", - set_label: "Calibrate", - set_halign: gtk::Align::Start, - connect_clicked[sender] => move |_| { - sender.output(Self::Output::OpenLibsurviveSetup).expect("Sender output failed"); - } - }, }, } } -> { diff --git a/src/ui/preference_rows.rs b/src/ui/preference_rows.rs index 171b9b0..9947d3e 100644 --- a/src/ui/preference_rows.rs +++ b/src/ui/preference_rows.rs @@ -60,6 +60,102 @@ 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 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>, @@ -147,3 +243,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"); + } +} 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 f4fc860..ff9c9ed 100644 --- a/src/ui/wivrn_conf_editor.rs +++ b/src/ui/wivrn_conf_editor.rs @@ -1,28 +1,30 @@ -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 { conf: WivrnConfig, #[tracker::do_not_track] - win: Option, + 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 { @@ -37,81 +39,86 @@ 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, + 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, + 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", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Save); + }, }, - #[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, + pack_start: cancel_btn = >k::Button { + set_label: "Cancel", + add_css_class: "destructive-action", + connect_clicked[win] => move |_| { + win.close(); + } }, }, - 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(), + #[wrap(Some)] + 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%."), + 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, }, - #[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: 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) + } }, }, - } - }, - }, + }, + } + } } } @@ -124,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}"); + } + } } } @@ -162,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 } }