Merge branch 'main' into feat/stardust

This commit is contained in:
Gabriele Musco 2023-12-27 09:24:20 +00:00
commit c2969b512f
8 changed files with 546 additions and 133 deletions

View file

@ -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<Encoder> {
pub fn as_vec() -> Vec<Self> {
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<Self> {
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<i32>,
}
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")]

View file

@ -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<Controller<FbtConfigEditor>>,
#[tracker::do_not_track]
libmonado: Option<libmonado_rs::Monado>,
#[tracker::do_not_track]
wivrn_conf_editor: Option<Controller<WivrnConfEditor>>,
}
#[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::<gtk::Window>(),
})
.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!();

View file

@ -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;

View file

@ -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<u32>),
WidthChanged(Option<f32>),
HeightChanged(Option<f32>),
GroupChanged(Option<i32>),
Delete,
}
#[derive(Debug)]
pub enum WivrnEncoderModelOutMsg {
Delete(String),
}
#[derive(Debug, Default)]
pub struct WivrnEncoderModelInit {
pub encoder_conf: Option<WivrnConfEncoder>,
}
#[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 = &gtk::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::<Vec<String>>(),
{
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::<Vec<String>>(),
{
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::<u32>().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<Self>) {
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<Self::ParentInput> {
Some(match output {
Self::Output::Delete(id) => Self::ParentInput::DeleteEncoder(id),
})
}
async fn init_model(
init: Self::Init,
_index: &DynamicIndex,
_sender: AsyncFactorySender<Self>,
) -> Self {
Self {
encoder_conf: init.encoder_conf.unwrap_or_default(),
uid: Uuid::new_v4().to_string(),
}
}
}

View file

@ -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");
}
},
},
}
} -> {

View file

@ -60,6 +60,102 @@ pub fn entry_row<F: Fn(&adw::EntryRow) + 'static>(
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<F: Fn(&adw::EntryRow) + 'static>(
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<F: Fn(&gtk::Adjustment) + 'static>(
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<F: Fn(Option<String>) + 'static + Clone>(
title: &str,
description: Option<&str>,
@ -147,3 +243,33 @@ pub fn combo_row<F: Fn(&adw::ComboRow) + 'static>(
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");
}
}

View file

@ -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 = &gtk::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 = &gtk::Button {
set_label: "Cancel",
add_css_class: "destructive-action",
connect_clicked[win] => move |_| {
win.close();
}
},
},
#[wrap(Some)]
set_content: pref_page = &adw::PreferencesPage {

View file

@ -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<adw::PreferencesWindow>,
win: Option<adw::Window>,
#[tracker::do_not_track]
scalex_entry: Option<adw::EntryRow>,
pub encoder_models: Option<AsyncFactoryVecDeque<WivrnEncoderModel>>,
#[tracker::do_not_track]
scaley_entry: Option<adw::EntryRow>,
pub scalex_row: Option<adw::SpinRow>,
#[tracker::do_not_track]
encoder_combo: Option<adw::ComboRow>,
#[tracker::do_not_track]
bitrate_entry: Option<adw::EntryRow>,
pub scaley_row: Option<adw::SpinRow>,
}
#[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 = &gtk::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 = &gtk::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(&gtk::StringList::new(
Encoder::iter()
.map(Encoder::to_string)
.collect::<Vec<String>>()
.iter()
.map(String::as_str)
.collect::<Vec<&str>>()
.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: "<a href=\"https://github.com/Meumeu/WiVRn#encoders\">WiVRn Configuration Documentation</a>",
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 = &gtk::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 = &gtk::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::<f32>();
let scaley = self.scaley_entry.as_ref().unwrap().text().parse::<f32>();
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::<u32>();
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<Self> {
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<WivrnEncoderModel> =
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 }
}