use super::{
host::{self, NewLive, POPPED_OUT_STATUS, Surface, SurfaceHandle, TRACED_DEBOUNCE},
metrics::SessionMetrics,
origins::{self, KeepGuard, OpenRequest, VariantSide},
render,
session::{self, CompareOrigin, CompareSession, Renderer},
};
use crate::{
CompareModel, CompareWindow, EditorModel, HelpModel, MainWindow, RetargetModel, ViewportModel,
bridge::render_thread::RenderContext,
gui::{
editor::{callbacks, state::EditorState, view::SolidLastSolved},
show_toast,
solid_preview::preview_state::{CameraPose, SolidPreviewState},
tutorial_events::raise,
},
};
use indicatrix::optics::LightingPreset;
use indicatrix_editor::guide::solving_events::COMPARE_WINDOW_OPENED;
use slint::{ComponentHandle, Image, ModelRc, SharedString, VecModel, Weak};
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, Mutex, PoisonError},
time::Duration,
};
use tracing::warn;
#[derive(Clone)]
pub(super) struct CompareDeps {
pub(super) ui: Weak<MainWindow>,
pub(super) state: Rc<RefCell<EditorState>>,
pub(super) render_ctx: Arc<Mutex<RenderContext>>,
pub(super) preview_state: Arc<SolidPreviewState>,
pub(super) solid_last_solved: SolidLastSolved,
}
thread_local! {
static ENTRY_DEPS: RefCell<Option<CompareDeps>> = const { RefCell::new(None) };
static SHOWN_REFERENCE_INDEX: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
}
pub(in crate::gui) fn close_compare_window() {
host::close_embedded_session();
host::close_window(true);
}
fn restore_embedded_if_open(ui: &MainWindow) {
if ui.global::<RetargetModel>().get_is_open() {
ui.global::<CompareModel>().invoke_open_retarget_embedded();
}
}
fn close_window_and_restore(deps: &CompareDeps, hide: bool) {
let origin = host::close_window(hide);
if origin == Some(CompareOrigin::Retarget)
&& let Some(ui) = deps.ui.upgrade()
{
restore_embedded_if_open(&ui);
}
}
fn on_keep(deps: &CompareDeps) {
let Some(ui) = deps.ui.upgrade() else {
return;
};
let Some(guard) = host::keep_guard() else {
return;
};
let still_current = {
let Ok(st) = deps.state.try_borrow() else {
return;
};
origins::guard_still_matches(&guard, &ui, &st, &deps.render_ctx)
};
if !still_current {
show_toast(
&ui,
"The change you compared is no longer the pending one -- open Compare again to \
see the current one.",
"warning",
);
return;
}
host::close_window(true);
match guard {
KeepGuard::Retarget(_) => ui.global::<RetargetModel>().invoke_apply(),
KeepGuard::Optimize { .. } => ui.global::<EditorModel>().invoke_optimize_apply(),
KeepGuard::Snapshot | KeepGuard::Variants => {}
}
}
fn on_discard(deps: &CompareDeps) {
let origin = host::close_window(true);
let Some(ui) = deps.ui.upgrade() else {
return;
};
match origin {
Some(CompareOrigin::Retarget) => {
if let Ok(st) = deps.state.try_borrow() {
callbacks::discard_retarget_preview(
&ui,
&deps.render_ctx,
&deps.preview_state,
&deps.solid_last_solved,
&st,
);
}
restore_embedded_if_open(&ui);
}
Some(CompareOrigin::Optimize) => {
let model = ui.global::<CompareModel>();
model.set_optimize_preview_reset_serial(
model.get_optimize_preview_reset_serial().wrapping_add(1),
);
ui.global::<EditorModel>()
.invoke_optimize_preview_toggled(false);
}
Some(CompareOrigin::Snapshot | CompareOrigin::Variants) | None => {}
}
}
fn register_view_callbacks(model: &CompareModel<'_>, surface: Surface) {
model.on_orbit(move |dx, dy| {
host::change_pose(surface, |pose, _| session::orbit(pose, dx, dy));
});
model.on_zoom(move |delta| {
host::change_pose(surface, |pose, radius| session::zoom(pose, delta, radius));
});
model.on_reset_pose(move || {
host::change_pose(surface, |_, radius| session::default_pose(radius));
});
model.on_drag_begin(move || host::set_drag(surface, true));
model.on_drag_end(move || host::set_drag(surface, false));
model.on_mode_changed(move |_| {
let Some(handle) = host::handle(surface) else {
return;
};
let renderer = Renderer::from_index(handle.with_model(|model| model.get_renderer_index()));
if host::layout_switched(surface, renderer) {
host::request_frames(TRACED_DEBOUNCE);
}
});
model.on_renderer_changed(move |index| {
if host::set_renderer(surface, Renderer::from_index(index)) {
host::request_frames(Duration::ZERO);
}
});
model.on_size_changed(move |width, height| {
let Some(handle) = host::handle(surface) else {
return;
};
let size = render::pixel_size(width, height, handle.scale_factor());
if host::set_size(surface, size) {
host::request_frames(TRACED_DEBOUNCE);
}
});
}
fn register_window_callbacks(window: &CompareWindow, deps: &CompareDeps) {
let model = window.global::<CompareModel>();
register_view_callbacks(&model, Surface::Window);
let keep_deps = deps.clone();
model.on_keep(move || on_keep(&keep_deps));
let discard_deps = deps.clone();
model.on_discard(move || on_discard(&discard_deps));
let close_deps = deps.clone();
model.on_close(move || close_window_and_restore(&close_deps, true));
let reference_deps = deps.clone();
let reference_window = window.as_weak();
model.on_reference_changed(move |index| {
if host::live_kind() != Some((Surface::Window, CompareOrigin::Optimize)) {
return;
}
let reference = origins::Reference::from_index(index);
let refused = std::cell::Cell::new(false);
open_from(&reference_deps, Surface::Window, |_, st, render_ctx| {
let request = origins::optimize_request(st, render_ctx, reference);
refused.set(request.is_err());
request
});
if refused.get()
&& let Some(window) = reference_window.upgrade()
{
let shown = SHOWN_REFERENCE_INDEX.with(std::cell::Cell::get);
window.global::<CompareModel>().set_reference_index(shown);
}
});
model.on_tilt_requested(|| host::start_tilt(Surface::Window));
model.on_tilt_cancelled(|| host::cancel_tilt(Surface::Window));
let help_ui = deps.ui.clone();
window.global::<HelpModel>().on_open_topic(move |topic| {
let Some(ui) = help_ui.upgrade() else {
return;
};
if let Err(message) = crate::gui::help::open_topic(&ui, topic.as_str()) {
show_toast(&ui, &message, "error");
}
});
let x_deps = deps.clone();
window.window().on_close_requested(move || {
close_window_and_restore(&x_deps, false);
slint::CloseRequestResponse::HideWindow
});
}
fn ensure_window(deps: &CompareDeps) -> Option<CompareWindow> {
if let Some(existing) = host::window() {
return Some(existing);
}
let window = match CompareWindow::new() {
Ok(window) => window,
Err(error) => {
warn!("Could not create the compare window: {error}");
return None;
}
};
crate::gui::preferences::bind_compare_window_theme(&window);
register_window_callbacks(&window, deps);
host::store_window(&window);
Some(window)
}
fn viewport_pose(deps: &CompareDeps) -> CameraPose {
let ctx = deps
.render_ctx
.lock()
.unwrap_or_else(PoisonError::into_inner);
CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
}
}
fn carry_view_settings(window: &SurfaceHandle) {
let Some(pane) = host::handle(Surface::Embedded) else {
return;
};
let (mode, renderer) =
pane.with_model(|model| (model.get_mode_index(), model.get_renderer_index()));
window.with_model(|model| {
model.set_mode_index(mode);
model.set_renderer_index(renderer);
});
}
fn retire_previous_surface(old: Option<Surface>, new: Surface) {
match (old, new) {
(Some(Surface::Window), Surface::Embedded) => {
host::close_window(true);
}
(Some(Surface::Embedded), Surface::Window) => {
if let Some(pane) = host::handle(Surface::Embedded) {
host::reset_surface(&pane, POPPED_OUT_STATUS);
}
}
_ => {}
}
}
fn target_handle(deps: &CompareDeps, surface: Surface) -> Option<SurfaceHandle> {
match surface {
Surface::Window => ensure_window(deps).map(SurfaceHandle::Window),
Surface::Embedded => host::handle(surface),
}
}
fn open_session(
deps: &CompareDeps,
surface: Surface,
request: OpenRequest,
lighting: LightingPreset,
) {
let Some(target) = target_handle(deps, surface) else {
return;
};
if surface == Surface::Window
&& host::live_kind().is_some_and(|(live, _)| live == Surface::Embedded)
{
carry_view_settings(&target);
}
let OpenRequest {
reference,
origin,
guard,
before,
after,
} = request;
let scale = target.scale_factor();
let refresh = host::live_kind().is_some_and(|(live, _)| live == surface);
let (renderer, size_px) = target.with_model(|model| {
model.set_before_label(before.label.clone().into());
model.set_after_label(after.label.clone().into());
if !refresh {
model.set_before_image(Image::default());
model.set_after_image(Image::default());
model.set_overlay_image(Image::default());
host::clear_strip(&model);
}
model.set_reference_available(reference.original_label.is_some());
model.set_reference_options(ModelRc::new(VecModel::from(
origins::reference_options(&reference)
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
model.set_reference_index(reference.selected.index());
SHOWN_REFERENCE_INDEX.with(|cell| cell.set(reference.selected.index()));
model.set_can_keep(false);
model.set_show_keep_discard(origin.offers_keep());
model.set_split_fraction(session::clamp_split_fraction(model.get_split_fraction()));
(
Renderer::from_index(model.get_renderer_index()),
render::pixel_size(model.get_image_width(), model.get_image_height(), scale),
)
});
let (id, old) = host::replace_live(NewLive {
surface,
origin,
guard,
renderer,
size_px,
});
let pose = old
.as_ref()
.and_then(host::LiveSession::pose)
.or_else(host::take_retained_pose)
.unwrap_or_else(|| viewport_pose(deps));
host::seed_pose(id, pose);
retire_previous_surface(old.as_ref().map(host::LiveSession::surface), surface);
drop(old);
target.with_model(|model| {
model.set_status(host::solving_status(renderer).into());
model.set_is_open(true);
model.set_embedded_open(surface == Surface::Embedded);
});
if let SurfaceHandle::Window(window) = &target
&& let Err(error) = window.show()
{
warn!("Could not show the compare window: {error}");
host::close_window(false);
return;
}
if matches!(target, SurfaceHandle::Window(_))
&& let Some(ui) = deps.ui.upgrade()
{
raise(&ui, COMPARE_WINDOW_OPENED);
}
let spawned = std::thread::Builder::new()
.name("compare-solve".to_string())
.spawn(move || {
let session = CompareSession::build(before, after, origin, pose);
let metrics = SessionMetrics::measure(&session, lighting);
let _ = slint::invoke_from_event_loop(move || {
host::on_session_built(id, session, metrics);
});
});
if let Err(error) = spawned {
warn!("Could not start the compare solve thread: {error}");
}
}
fn open_from(
deps: &CompareDeps,
surface: Surface,
build: impl FnOnce(
&MainWindow,
&EditorState,
&Arc<Mutex<RenderContext>>,
) -> Result<OpenRequest, String>,
) {
let Some(ui) = deps.ui.upgrade() else {
return;
};
let request = {
let Ok(st) = deps.state.try_borrow() else {
return;
};
build(&ui, &st, &deps.render_ctx)
};
let lighting =
LightingPreset::from_index(ui.global::<ViewportModel>().get_selected_lighting_index());
match request {
Ok(request) => open_session(deps, surface, request, lighting),
Err(reason) if surface == Surface::Embedded => host::drop_embedded(&reason),
Err(reason) => show_toast(&ui, &reason, "info"),
}
}
fn open_retarget_embedded(deps: &CompareDeps) {
let surface = if host::live_kind() == Some((Surface::Window, CompareOrigin::Retarget)) {
Surface::Window
} else {
Surface::Embedded
};
open_from(deps, surface, origins::retarget_request);
}
pub(in crate::gui::editor) fn open_variants_compare(
ui: &MainWindow,
first: VariantSide,
second: VariantSide,
) {
let Some(deps) = ENTRY_DEPS.with(|cell| cell.borrow().clone()) else {
return;
};
let lighting =
LightingPreset::from_index(ui.global::<ViewportModel>().get_selected_lighting_index());
let request = origins::variants_request(&deps.render_ctx, first, second);
open_session(&deps, Surface::Window, request, lighting);
}
pub(super) fn setup_entry_callbacks(ui: &MainWindow, deps: &CompareDeps) {
host::set_main_window(ui);
ENTRY_DEPS.with(|cell| *cell.borrow_mut() = Some(deps.clone()));
let model = ui.global::<CompareModel>();
let retarget_deps = deps.clone();
model.on_open_retarget(move || {
open_from(&retarget_deps, Surface::Window, origins::retarget_request);
});
let optimize_deps = deps.clone();
model.on_open_optimize(move || {
open_from(&optimize_deps, Surface::Window, |_, st, render_ctx| {
origins::optimize_request(st, render_ctx, origins::Reference::Current)
});
});
let snapshot_deps = deps.clone();
model.on_open_snapshot(move || {
open_from(&snapshot_deps, Surface::Window, |_, st, render_ctx| {
origins::snapshot_request(st, render_ctx)
});
});
register_view_callbacks(&model, Surface::Embedded);
let embedded_deps = deps.clone();
model.on_open_retarget_embedded(move || open_retarget_embedded(&embedded_deps));
model.on_close_embedded(host::close_embedded_session);
}