mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-08-03 14:49:04 +00:00
Merge branch 'main' into feat/stardust
This commit is contained in:
commit
c2969b512f
8 changed files with 546 additions and 133 deletions
|
@ -24,11 +24,11 @@ impl Display for Encoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encoder {
|
impl Encoder {
|
||||||
pub fn iter() -> Iter<'static, Encoder> {
|
pub fn iter() -> Iter<'static, Self> {
|
||||||
[Self::X264, Self::Nvenc, Self::Vaapi].iter()
|
[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]
|
vec![Self::X264, Self::Nvenc, Self::Vaapi]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +46,32 @@ impl Encoder {
|
||||||
pub enum Codec {
|
pub enum Codec {
|
||||||
H264,
|
H264,
|
||||||
H265,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -64,6 +88,19 @@ pub struct WivrnConfEncoder {
|
||||||
pub group: Option<i32>,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct WivrnConfig {
|
pub struct WivrnConfig {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
|
@ -9,6 +9,7 @@ use super::job_worker::JobWorker;
|
||||||
use super::libsurvive_setup_window::LibsurviveSetupWindow;
|
use super::libsurvive_setup_window::LibsurviveSetupWindow;
|
||||||
use super::main_view::MainViewMsg;
|
use super::main_view::MainViewMsg;
|
||||||
use super::util::open_with_default_handler;
|
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_basalt::get_build_basalt_jobs;
|
||||||
use crate::builders::build_libsurvive::get_build_libsurvive_jobs;
|
use crate::builders::build_libsurvive::get_build_libsurvive_jobs;
|
||||||
use crate::builders::build_mercury::get_build_mercury_job;
|
use crate::builders::build_mercury::get_build_mercury_job;
|
||||||
|
@ -102,6 +103,9 @@ pub struct App {
|
||||||
fbt_config_editor: Option<Controller<FbtConfigEditor>>,
|
fbt_config_editor: Option<Controller<FbtConfigEditor>>,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
libmonado: Option<libmonado_rs::Monado>,
|
libmonado: Option<libmonado_rs::Monado>,
|
||||||
|
|
||||||
|
#[tracker::do_not_track]
|
||||||
|
wivrn_conf_editor: Option<Controller<WivrnConfEditor>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -128,6 +132,7 @@ pub enum Msg {
|
||||||
ConfigFbt,
|
ConfigFbt,
|
||||||
DebugOpenPrefix,
|
DebugOpenPrefix,
|
||||||
DebugOpenData,
|
DebugOpenData,
|
||||||
|
OpenWivrnConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
@ -632,6 +637,15 @@ impl SimpleComponent for App {
|
||||||
self.get_selected_profile().prefix
|
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 {
|
let mut model = App {
|
||||||
|
tracker: 0,
|
||||||
application: init.application,
|
application: init.application,
|
||||||
app_win: root.clone(),
|
app_win: root.clone(),
|
||||||
inhibit_id: None,
|
inhibit_id: None,
|
||||||
|
@ -687,6 +702,7 @@ impl SimpleComponent for App {
|
||||||
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile,
|
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile,
|
||||||
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
|
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
|
||||||
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
|
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
|
||||||
|
MainViewOutMsg::OpenWivrnConfig => Msg::OpenWivrnConfig,
|
||||||
}),
|
}),
|
||||||
debug_view: DebugView::builder().launch(DebugViewInit {}).forward(
|
debug_view: DebugView::builder().launch(DebugViewInit {}).forward(
|
||||||
sender.input_sender(),
|
sender.input_sender(),
|
||||||
|
@ -712,7 +728,6 @@ impl SimpleComponent for App {
|
||||||
setcap_confirm_dialog,
|
setcap_confirm_dialog,
|
||||||
enable_debug_view: config.debug_view_enabled,
|
enable_debug_view: config.debug_view_enabled,
|
||||||
config,
|
config,
|
||||||
tracker: 0,
|
|
||||||
profiles,
|
profiles,
|
||||||
xrservice_worker: None,
|
xrservice_worker: None,
|
||||||
build_worker: None,
|
build_worker: None,
|
||||||
|
@ -720,6 +735,7 @@ impl SimpleComponent for App {
|
||||||
fbt_config_editor: None,
|
fbt_config_editor: None,
|
||||||
restart_xrservice: false,
|
restart_xrservice: false,
|
||||||
libmonado: None,
|
libmonado: None,
|
||||||
|
wivrn_conf_editor: None,
|
||||||
};
|
};
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod device_row_factory;
|
pub mod device_row_factory;
|
||||||
pub mod env_var_row_factory;
|
pub mod env_var_row_factory;
|
||||||
pub mod tracker_role_group_factory;
|
pub mod tracker_role_group_factory;
|
||||||
|
pub mod wivrn_encoder_group_factory;
|
||||||
|
|
196
src/ui/factories/wivrn_encoder_group_factory.rs
Normal file
196
src/ui/factories/wivrn_encoder_group_factory.rs
Normal 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 = >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::<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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
get_amd_gpu_power_profile, get_first_amd_gpu, get_set_amd_vr_pow_prof_cmd, GpuPowerProfile,
|
||||||
GpuSysDrm,
|
GpuSysDrm,
|
||||||
};
|
};
|
||||||
use crate::profile::{LighthouseDriver, Profile};
|
use crate::profile::{LighthouseDriver, Profile, XRServiceType};
|
||||||
use crate::steamvr_utils::chaperone_info_exists;
|
use crate::steamvr_utils::chaperone_info_exists;
|
||||||
use crate::ui::app::{
|
use crate::ui::app::{
|
||||||
AboutAction, BuildProfileAction, BuildProfileCleanAction, BuildProfileCleanDebugAction,
|
AboutAction, BuildProfileAction, BuildProfileCleanAction, BuildProfileCleanDebugAction,
|
||||||
|
@ -87,6 +87,7 @@ pub enum MainViewOutMsg {
|
||||||
DeleteProfile,
|
DeleteProfile,
|
||||||
SaveProfile(Profile),
|
SaveProfile(Profile),
|
||||||
OpenLibsurviveSetup,
|
OpenLibsurviveSetup,
|
||||||
|
OpenWivrnConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MainViewInit {
|
pub struct MainViewInit {
|
||||||
|
@ -343,6 +344,32 @@ impl SimpleComponent for MainView {
|
||||||
|
|
||||||
model.steam_launch_options_box.widget(),
|
model.steam_launch_options_box.widget(),
|
||||||
model.install_wivrn_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(),
|
model.steamvr_calibration_box.widget(),
|
||||||
|
|
||||||
gtk::Box {
|
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");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} -> {
|
} -> {
|
||||||
|
|
|
@ -60,6 +60,102 @@ pub fn entry_row<F: Fn(&adw::EntryRow) + 'static>(
|
||||||
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<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(>k::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>(
|
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||||
title: &str,
|
title: &str,
|
||||||
description: Option<&str>,
|
description: Option<&str>,
|
||||||
|
@ -147,3 +243,33 @@ pub fn combo_row<F: Fn(&adw::ComboRow) + 'static>(
|
||||||
|
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -66,6 +66,8 @@ impl SimpleComponent for ProfileEditor {
|
||||||
set_vexpand: true,
|
set_vexpand: true,
|
||||||
add_top_bar: top_bar = &adw::HeaderBar {
|
add_top_bar: top_bar = &adw::HeaderBar {
|
||||||
set_vexpand: false,
|
set_vexpand: false,
|
||||||
|
set_show_end_title_buttons: false,
|
||||||
|
set_show_start_title_buttons: false,
|
||||||
pack_end: save_btn = >k::Button {
|
pack_end: save_btn = >k::Button {
|
||||||
set_label: "Save",
|
set_label: "Save",
|
||||||
add_css_class: "suggested-action",
|
add_css_class: "suggested-action",
|
||||||
|
@ -73,6 +75,13 @@ impl SimpleComponent for ProfileEditor {
|
||||||
sender.input(Self::Input::SaveProfile);
|
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)]
|
#[wrap(Some)]
|
||||||
set_content: pref_page = &adw::PreferencesPage {
|
set_content: pref_page = &adw::PreferencesPage {
|
||||||
|
|
|
@ -1,28 +1,30 @@
|
||||||
use crate::file_builders::wivrn_config::{
|
use super::factories::wivrn_encoder_group_factory::{WivrnEncoderModel, WivrnEncoderModelInit};
|
||||||
dump_wivrn_config, get_wivrn_config, Encoder, WivrnConfig,
|
use crate::{
|
||||||
|
file_builders::wivrn_config::{dump_wivrn_config, get_wivrn_config, WivrnConfig},
|
||||||
|
ui::preference_rows::spin_row,
|
||||||
};
|
};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct WivrnConfEditor {
|
pub struct WivrnConfEditor {
|
||||||
conf: WivrnConfig,
|
conf: WivrnConfig,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
win: Option<adw::PreferencesWindow>,
|
win: Option<adw::Window>,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
scalex_entry: Option<adw::EntryRow>,
|
pub encoder_models: Option<AsyncFactoryVecDeque<WivrnEncoderModel>>,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
scaley_entry: Option<adw::EntryRow>,
|
pub scalex_row: Option<adw::SpinRow>,
|
||||||
#[tracker::do_not_track]
|
#[tracker::do_not_track]
|
||||||
encoder_combo: Option<adw::ComboRow>,
|
pub scaley_row: Option<adw::SpinRow>,
|
||||||
#[tracker::do_not_track]
|
|
||||||
bitrate_entry: Option<adw::EntryRow>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum WivrnConfEditorMsg {
|
pub enum WivrnConfEditorMsg {
|
||||||
Present,
|
Present,
|
||||||
Save,
|
Save,
|
||||||
|
AddEncoder,
|
||||||
|
DeleteEncoder(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WivrnConfEditorInit {
|
pub struct WivrnConfEditorInit {
|
||||||
|
@ -37,81 +39,86 @@ impl SimpleComponent for WivrnConfEditor {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[name(win)]
|
#[name(win)]
|
||||||
adw::PreferencesWindow {
|
adw::Window {
|
||||||
set_hide_on_close: true,
|
|
||||||
set_modal: true,
|
set_modal: true,
|
||||||
set_transient_for: Some(&init.root_win),
|
set_transient_for: Some(&init.root_win),
|
||||||
set_title: Some("WiVRn Configuration"),
|
set_title: Some("WiVRn Configuration"),
|
||||||
add: mainpage = &adw::PreferencesPage {
|
set_default_height: 500,
|
||||||
add: scalegrp = &adw::PreferencesGroup {
|
set_default_width: 600,
|
||||||
set_title: "Scale",
|
adw::ToolbarView {
|
||||||
set_description: Some("Render resolution scale. 1.0 is 100%."),
|
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||||
#[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::<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(),
|
|
||||||
},
|
|
||||||
#[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,
|
set_hexpand: true,
|
||||||
gtk::Button {
|
set_vexpand: true,
|
||||||
set_halign: gtk::Align::Center,
|
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",
|
set_label: "Save",
|
||||||
add_css_class: "pill",
|
|
||||||
add_css_class: "suggested-action",
|
add_css_class: "suggested-action",
|
||||||
connect_clicked[sender] => move |_| {
|
connect_clicked[sender] => move |_| {
|
||||||
sender.input(Self::Input::Save);
|
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 {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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.win.as_ref().unwrap().present();
|
||||||
}
|
}
|
||||||
Self::Input::Save => {
|
Self::Input::Save => {
|
||||||
let scalex = self.scalex_entry.as_ref().unwrap().text().parse::<f32>();
|
let x = self.scalex_row.as_ref().unwrap().adjustment().value();
|
||||||
let scaley = self.scaley_entry.as_ref().unwrap().text().parse::<f32>();
|
let y = self.scaley_row.as_ref().unwrap().adjustment().value();
|
||||||
if scalex.is_ok() && scaley.is_ok() {
|
Some([x as f32, y as f32]);
|
||||||
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);
|
|
||||||
|
|
||||||
|
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);
|
dump_wivrn_config(&self.conf);
|
||||||
self.win.as_ref().unwrap().close();
|
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> {
|
) -> ComponentParts<Self> {
|
||||||
let mut model = Self {
|
let mut model = Self {
|
||||||
conf: get_wivrn_config(),
|
conf: get_wivrn_config(),
|
||||||
|
encoder_models: None,
|
||||||
win: None,
|
win: None,
|
||||||
scalex_entry: None,
|
scalex_row: None,
|
||||||
scaley_entry: None,
|
scaley_row: None,
|
||||||
bitrate_entry: None,
|
|
||||||
encoder_combo: None,
|
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let widgets = view_output!();
|
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.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 }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue