use super::{live_compute_target_from_index, worker_settings::from_worker_item};
use crate::{
MainWindow, RemoteWorkerModel, SettingsModel, WorkerItem,
bridge::{export_thread, remote::remote_render, render_thread::RenderContext},
gui::{
remote::{RemoteOrchestratorHandle, refresh_remote_ui},
show_toast,
},
settings::{LiveTransfer, SettingsPersister, WorkerSettings},
};
use slint::ComponentHandle;
use std::sync::{Arc, Mutex, PoisonError};
pub fn setup_worker_callbacks(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
orchestrator: &RemoteOrchestratorHandle,
) {
setup_save_remote_callback(ui, settings_store);
setup_remove_remote_callback(ui, settings_store);
setup_denoise_toggle_callback(ui, render_ctx, settings_store, orchestrator);
setup_live_compute_target_callback(ui, render_ctx, settings_store);
setup_live_transfer_callback(ui, render_ctx, settings_store);
setup_contribute_to_final_picture_callback(ui, settings_store);
setup_claim_token_callback(ui);
setup_test_worker_connection_callback(ui);
setup_cert_dir_picker_callback(ui);
super::advanced_in_use::setup_advanced_in_use_callback(ui);
}
fn setup_save_remote_callback(ui: &MainWindow, settings_store: &Arc<SettingsPersister>) {
let settings_store_save = settings_store.clone();
let ui_weak_save = ui.as_weak();
ui.global::<RemoteWorkerModel>()
.on_save_remote(move |item: WorkerItem| {
let Some(ui) = ui_weak_save.upgrade() else {
return;
};
settings_store_save.update(|s| {
let endpoint = from_worker_item(&item, s.settings.remote.as_ref());
s.settings.remote = Some(endpoint);
});
export_thread::forget_final_picture_refusals();
refresh_remote_ui(&ui, settings_store_save.snapshot().settings.remote.as_ref());
show_toast(&ui, "Remote coordinator saved.", "success");
});
}
fn setup_remove_remote_callback(ui: &MainWindow, settings_store: &Arc<SettingsPersister>) {
let settings_store_remove = settings_store.clone();
let ui_weak_remove = ui.as_weak();
ui.global::<RemoteWorkerModel>().on_remove_remote(move || {
let Some(ui) = ui_weak_remove.upgrade() else {
return;
};
settings_store_remove.update(|s| s.settings.remote = None);
refresh_remote_ui(&ui, None);
show_toast(
&ui,
"Remote coordinator removed -- renders stay local.",
"info",
);
});
}
fn setup_live_transfer_callback(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
) {
let settings_store = settings_store.clone();
let render_ctx = render_ctx.clone();
ui.global::<SettingsModel>()
.on_live_transfer_changed(move |index: i32| {
let transfer = LiveTransfer::from_index(index);
let mut changed = false;
settings_store.update(|s| {
if let Some(remote) = s.settings.remote.as_mut() {
changed = remote.live_transfer != transfer;
remote.live_transfer = transfer;
}
});
if changed {
let mut ctx = render_ctx.lock().unwrap_or_else(PoisonError::into_inner);
if ctx.remote_active || ctx.live_epoch.is_some() {
ctx.release_remote();
}
}
});
}
fn setup_contribute_to_final_picture_callback(
ui: &MainWindow,
settings_store: &Arc<SettingsPersister>,
) {
let settings_store = settings_store.clone();
ui.global::<SettingsModel>()
.on_contribute_to_final_picture_changed(move |on: bool| {
settings_store.update(|s| s.settings.contribute_to_final_picture = on);
});
}
fn setup_denoise_toggle_callback(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
orchestrator: &RemoteOrchestratorHandle,
) {
let settings_store_denoise = settings_store.clone();
let render_ctx_denoise = render_ctx.clone();
let orchestrator_denoise = orchestrator.clone();
let ui_weak_denoise = ui.as_weak();
ui.global::<RemoteWorkerModel>()
.on_denoise_toggled(move |enabled: bool| {
let mut ctx = render_ctx_denoise
.lock()
.unwrap_or_else(PoisonError::into_inner);
ctx.denoise_enabled = enabled;
ctx.redisplay_requested = true;
drop(ctx);
settings_store_denoise.update(|s| s.settings.denoise_enabled = enabled);
if let Some(ui) = ui_weak_denoise.upgrade() {
orchestrator_denoise.request_redisplay(&ui, &render_ctx_denoise);
}
});
}
fn setup_test_worker_connection_callback(ui: &MainWindow) {
let ui_weak_test = ui.as_weak();
ui.global::<RemoteWorkerModel>().on_test_worker_connection(
move |address: slint::SharedString, cert_dir: slint::SharedString| {
let Some(ui) = ui_weak_test.upgrade() else {
return;
};
ui.global::<RemoteWorkerModel>()
.set_testing_connection(true);
let worker = WorkerSettings {
address: address.to_string(),
cert_dir: cert_dir.to_string(),
..WorkerSettings::default()
};
let ui_weak_result = ui.as_weak();
std::thread::spawn(move || {
let result = remote_render::test_connection(&worker);
let _ = ui_weak_result.upgrade_in_event_loop(move |ui| {
ui.global::<RemoteWorkerModel>()
.set_testing_connection(false);
match result {
Ok(info) => {
ui.global::<RemoteWorkerModel>()
.set_test_connection_is_error(false);
ui.global::<RemoteWorkerModel>().set_test_connection_result(
format!(
"Compatible -- {} (protocol v{})",
backend_label(info.render.as_ref()),
info.protocol_version
)
.into(),
);
}
Err(err) => {
ui.global::<RemoteWorkerModel>()
.set_test_connection_is_error(true);
ui.global::<RemoteWorkerModel>()
.set_test_connection_result(err.to_string().into());
}
}
});
});
},
);
}
fn setup_cert_dir_picker_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.global::<RemoteWorkerModel>()
.on_pick_cert_dir(move |current: slint::SharedString| {
use crate::gui::pickers::{PickerKind, PickerRequest, pick};
let Some(ui) = ui_weak.upgrade() else {
return;
};
ui.global::<RemoteWorkerModel>()
.set_picked_cert_dir("".into());
let starting_dir = crate::gui::starting_dir_from_picker_field(current.as_str());
let request = PickerRequest {
kind: PickerKind::PickFolder,
title: None,
filters: Vec::new(),
default_file_name: None,
starting_dir,
};
pick(&ui, request, |ui, picked| {
if let Some(path) = picked {
ui.global::<RemoteWorkerModel>()
.set_picked_cert_dir(path.display().to_string().into());
}
});
});
}
fn setup_live_compute_target_callback(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
) {
let settings_store_compute = settings_store.clone();
let render_ctx_compute = render_ctx.clone();
ui.global::<SettingsModel>()
.on_live_compute_target_changed(move |index: i32| {
let target = live_compute_target_from_index(index);
{
let mut ctx = render_ctx_compute
.lock()
.unwrap_or_else(PoisonError::into_inner);
if ctx.live_compute_target != target
&& (ctx.remote_active || ctx.live_epoch.is_some())
{
ctx.release_remote();
}
ctx.live_compute_target = target;
}
settings_store_compute.update(|s| s.settings.live_compute_target = target);
});
}
fn setup_claim_token_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.global::<RemoteWorkerModel>().on_claim_token(
move |worker_name: slint::SharedString,
enroll_addr: slint::SharedString,
token: slint::SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
ui.global::<RemoteWorkerModel>().set_claiming_token(true);
ui.global::<RemoteWorkerModel>()
.set_claim_result_text("".into());
ui.global::<RemoteWorkerModel>()
.set_claimed_cert_dir("".into());
ui.global::<RemoteWorkerModel>()
.set_claimed_address("".into());
let settings_dir = crate::settings::store::default_settings_path()
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_default();
let worker_name = worker_name.to_string();
let enroll_addr = enroll_addr.to_string();
let token = token.to_string();
let suggested_address =
crate::bridge::remote::enroll::suggested_serve_address(&enroll_addr);
let ui_weak_result = ui.as_weak();
std::thread::spawn(move || {
let bundle_dir =
crate::bridge::remote::enroll::bundle_dir_for(&settings_dir, &worker_name);
let result = crate::bridge::remote::enroll::claim_and_write_bundle(
&token,
&enroll_addr,
&bundle_dir,
);
let _ = ui_weak_result.upgrade_in_event_loop(move |ui| {
ui.global::<RemoteWorkerModel>().set_claiming_token(false);
match result {
Ok(dir) => {
ui.global::<RemoteWorkerModel>()
.set_claim_result_is_error(false);
ui.global::<RemoteWorkerModel>().set_claim_result_text(
"Token redeemed -- certificate folder filled in.".into(),
);
ui.global::<RemoteWorkerModel>()
.set_claimed_cert_dir(dir.display().to_string().into());
ui.global::<RemoteWorkerModel>()
.set_claimed_address(suggested_address.into());
}
Err(message) => {
ui.global::<RemoteWorkerModel>()
.set_claim_result_is_error(true);
ui.global::<RemoteWorkerModel>()
.set_claim_result_text(message.into());
}
}
});
});
},
);
}
pub(super) fn backend_label(render: Option<&indicatrix_net::messages::RenderCapability>) -> String {
match render.map(|r| &r.backend) {
Some(indicatrix_net::messages::Backend::Cpu { threads }) => {
format!("CPU, {threads} threads")
}
Some(indicatrix_net::messages::Backend::Gpu { adapter }) => format!("GPU ({adapter})"),
Some(indicatrix_net::messages::Backend::Coordinator { workers, .. }) => {
coordinator_label(*workers)
}
None => "library only (no render capacity)".to_string(),
}
}
fn coordinator_label(workers: u32) -> String {
if workers == 1 {
"coordinator (1 worker)".to_string()
} else {
format!("coordinator ({workers} workers)")
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_net::messages::{Backend, RenderCapability};
fn capability(backend: Backend) -> RenderCapability {
RenderCapability {
backend,
max_pixels: 1,
min_cadence_ms: 100,
hdr: false,
}
}
#[test]
fn served_by_names_a_coordinator_by_its_worker_count() {
let three = capability(Backend::Coordinator {
workers: 3,
threads: 16,
gpus: 1,
});
assert_eq!(backend_label(Some(&three)), "coordinator (3 workers)");
let one = capability(Backend::Coordinator {
workers: 1,
threads: 0,
gpus: 0,
});
assert_eq!(backend_label(Some(&one)), "coordinator (1 worker)");
assert_eq!(
backend_label(Some(&capability(Backend::Cpu { threads: 8 }))),
"CPU, 8 threads"
);
assert_eq!(backend_label(None), "library only (no render capacity)");
}
}