mod batch;
#[cfg(feature = "editor")]
mod editor;
mod editor_layout;
mod library;
mod optics;
mod remote;
mod render;
pub mod solid_preview;
mod startup_settings;
mod tilt;
mod window_sizing;
pub use library::diagram_list::sync_range_bounds_to_ui;
pub use library::search;
use startup_settings::apply_loaded_settings;
pub(in crate::gui) use startup_settings::{
color_space_from_index, env_map_status_text, is_c_axis_override_available,
local_compute_target_from_index, local_preview_scale_from_index,
refresh_lighting_preset_options, refresh_material_options,
};
use crate::{
EditorModel, LibraryModel, MainWindow, RemoteWorkerModel, SettingsModel, SolidPreviewModel,
TiltModel, ViewportModel,
bridge::{
library::source::LibrarySource,
render_thread::{RenderContext, spawn_render_thread},
},
gui::{
optics::{crystal_optics::gem_material_from_row, curve_path::tilt_curve_path},
remote::{refresh_worker_options, setup_remote_rendering, setup_worker_callbacks},
solid_preview::{
diagram_wiring::{self, DiagramFacetTier, DiagramHoverText, DiagramPick},
preview_state::{
DEFAULT_MESH_BOUNDING_RADIUS, PickBuffer, PreviewFrame, PreviewSink,
SolidLastSolved, SolidPickState, SolidPreviewState,
},
},
},
settings::{self, SettingsPersister},
};
use indicatrix::{geometry::plane::GpuFacetPlane, optics::materials::GemMaterial};
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, Weak};
use std::sync::{Arc, Mutex};
const DB_PATH: &str = "facet_diagrams.sqlite";
pub fn main() -> anyhow::Result<()> {
run_gui()
}
pub struct MainWindowHandle {
pub ui: MainWindow,
#[expect(
dead_code,
reason = "RAII guard field -- kept alive, never read; see comment above"
)]
render_ctx: Arc<Mutex<RenderContext>>,
settings_store: Arc<SettingsPersister>,
#[expect(
dead_code,
reason = "RAII guard field -- kept alive, never read; see comment above"
)]
remote_rendering_timer: slint::Timer,
}
pub fn run_gui() -> anyhow::Result<()> {
let handle = build_main_window()?;
handle.ui.show()?;
let editor_layout_touched = handle
.settings_store
.snapshot()
.settings
.editor_layout_touched;
window_sizing::fit_initial_window_size(&handle.ui, editor_layout_touched);
slint::run_event_loop()?;
handle.ui.hide()?;
Ok(())
}
struct SlintSolidSink {
ui: Weak<MainWindow>,
pick: Arc<Mutex<Option<PickBuffer>>>,
last_solved: SolidLastSolved,
diagram_pick: DiagramPick,
diagram_hover_text: DiagramHoverText,
diagram_facet_tier: DiagramFacetTier,
diagram_tooth_pick: DiagramPick,
hover_text: Arc<Mutex<Vec<String>>>,
facet_tier: Arc<Mutex<Vec<Option<usize>>>>,
render_ctx: Arc<Mutex<RenderContext>>,
solid_active_planes: Arc<Mutex<Option<Arc<Vec<GpuFacetPlane>>>>>,
trace_active_planes: Arc<Mutex<Option<Arc<Vec<GpuFacetPlane>>>>>,
mesh_bounding_radius: Arc<Mutex<f64>>,
}
fn planes_generations_match(
trace: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
solid: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
) -> bool {
let trace = trace
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let solid = solid
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
match (trace, solid) {
(Some(trace), Some(solid)) => Arc::ptr_eq(&trace, &solid),
_ => true,
}
}
fn sync_planes_and_check_trace_match(
planes: &[(glam::Vec3, f32)],
render_ctx: &Mutex<RenderContext>,
solid_active_planes: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
trace_active_planes: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
) -> bool {
if !planes.is_empty() {
let converted: Vec<GpuFacetPlane> = planes
.iter()
.map(|&(normal, offset)| GpuFacetPlane::new(normal, -offset))
.collect();
let mut ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if ctx.active_planes.as_ref() != &converted {
ctx.active_planes = Arc::new(converted);
ctx.dirty = true;
}
*solid_active_planes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(Arc::clone(&ctx.active_planes));
}
planes_generations_match(trace_active_planes, solid_active_planes)
}
fn store_if_some<T>(state: &Mutex<Option<T>>, value: Option<T>) {
if let Some(value) = value {
*state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
}
}
impl PreviewSink for SlintSolidSink {
fn apply(&self, frame: PreviewFrame) {
let PreviewFrame {
image,
has_solid,
status,
solved,
stale,
pick,
edges_image,
diagram_image,
has_diagram,
diagram_pick,
diagram_tooth_pick,
diagram_hover_text,
diagram_facet_tier,
planes,
hover_text,
facet_tier,
#[cfg(feature = "editor")]
generation,
#[cfg(not(feature = "editor"))]
generation: _,
mesh_bounding_radius,
} = frame;
let pick_state = Arc::clone(&self.pick);
let last_solved_state = Arc::clone(&self.last_solved);
let diagram_pick_state = Arc::clone(&self.diagram_pick);
let diagram_tooth_pick_state = Arc::clone(&self.diagram_tooth_pick);
let diagram_hover_text_state = Arc::clone(&self.diagram_hover_text);
let diagram_facet_tier_state = Arc::clone(&self.diagram_facet_tier);
let hover_text_state = Arc::clone(&self.hover_text);
let facet_tier_state = Arc::clone(&self.facet_tier);
let render_ctx_state = Arc::clone(&self.render_ctx);
let solid_active_planes_state = Arc::clone(&self.solid_active_planes);
let trace_active_planes_state = Arc::clone(&self.trace_active_planes);
let mesh_bounding_radius_state = Arc::clone(&self.mesh_bounding_radius);
let _ = self.ui.upgrade_in_event_loop(move |ui| {
*pick_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pick);
*mesh_bounding_radius_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = mesh_bounding_radius;
#[cfg(feature = "editor")]
if let Some(masts) = solved.as_ref() {
editor::apply_matching_preview_frame(&ui, generation, masts);
}
*last_solved_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = solved;
store_if_some(&diagram_pick_state, diagram_pick);
store_if_some(&diagram_tooth_pick_state, diagram_tooth_pick);
store_if_some(&diagram_hover_text_state, diagram_hover_text);
store_if_some(&diagram_facet_tier_state, diagram_facet_tier);
*hover_text_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = hover_text;
*facet_tier_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = facet_tier;
let trace_matches = sync_planes_and_check_trace_match(
&planes,
&render_ctx_state,
&solid_active_planes_state,
&trace_active_planes_state,
);
ui.global::<SolidPreviewModel>()
.set_trace_matches_solid(trace_matches);
ui.global::<SolidPreviewModel>()
.set_image(slint::Image::from_rgba8(image));
ui.global::<SolidPreviewModel>().set_has_solid(has_solid);
ui.global::<SolidPreviewModel>().set_status(status.into());
ui.global::<SolidPreviewModel>().set_stale(stale);
if let Some(edges_image) = edges_image {
ui.global::<SolidPreviewModel>()
.set_edges_image(slint::Image::from_rgba8(edges_image));
ui.global::<SolidPreviewModel>().set_has_solid_edges(true);
} else {
ui.global::<SolidPreviewModel>().set_has_solid_edges(false);
}
if let Some(diagram_image) = diagram_image {
ui.global::<SolidPreviewModel>()
.set_diagram_image(slint::Image::from_rgba8(diagram_image));
}
ui.global::<SolidPreviewModel>()
.set_has_diagram(has_diagram);
});
}
}
#[expect(
clippy::too_many_lines,
reason = "a flat sequence of already-extracted setup_* calls (see comment above) -- \
splitting further just moves the same line count into a wrapper that \
calls them all, not a real reduction"
)]
pub fn build_main_window() -> anyhow::Result<MainWindowHandle> {
let ui = MainWindow::new()?;
ui.global::<SettingsModel>()
.set_gpu_build(cfg!(feature = "gpu"));
ui.global::<EditorModel>()
.set_enabled(cfg!(feature = "editor"));
let db = match Database::new(Some(DB_PATH)) {
Ok(db_inst) => Arc::new(Mutex::new(db_inst)),
Err(e) => {
match Database::new(Some(":memory:")) {
Ok(memory_db) => {
show_toast(
&ui,
&format!(
"Could not open the design library ({e}) -- running in a \
temporary, unsaved library for this session."
),
"error",
);
Arc::new(Mutex::new(memory_db))
}
Err(memory_err) => {
return Err(anyhow::anyhow!(
"failed to open design library at {DB_PATH:?} ({e}), and the \
in-memory fallback database also failed to open ({memory_err})"
));
}
}
}
};
let render_ctx = Arc::new(Mutex::new(RenderContext::default()));
let solid_pick: Arc<Mutex<Option<PickBuffer>>> = Arc::new(Mutex::new(None));
let solid_last_solved: SolidLastSolved = Arc::new(Mutex::new(None));
let diagram_pick: DiagramPick = Arc::new(Mutex::new(None));
let diagram_tooth_pick: DiagramPick = Arc::new(Mutex::new(None));
let diagram_hover_text: DiagramHoverText = Arc::new(Mutex::new(None));
let diagram_facet_tier: DiagramFacetTier = Arc::new(Mutex::new(None));
let solid_hover_text: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let solid_facet_tier: Arc<Mutex<Vec<Option<usize>>>> = Arc::new(Mutex::new(Vec::new()));
let solid_active_planes: Arc<Mutex<Option<Arc<Vec<GpuFacetPlane>>>>> =
Arc::new(Mutex::new(None));
let trace_active_planes: Arc<Mutex<Option<Arc<Vec<GpuFacetPlane>>>>> =
Arc::new(Mutex::new(None));
let mesh_bounding_radius: Arc<Mutex<f64>> = Arc::new(Mutex::new(DEFAULT_MESH_BOUNDING_RADIUS));
let solid_preview_state = SolidPreviewState::new(Arc::new(SlintSolidSink {
ui: ui.as_weak(),
pick: Arc::clone(&solid_pick),
last_solved: Arc::clone(&solid_last_solved),
diagram_pick: Arc::clone(&diagram_pick),
diagram_tooth_pick: Arc::clone(&diagram_tooth_pick),
diagram_hover_text: Arc::clone(&diagram_hover_text),
diagram_facet_tier: Arc::clone(&diagram_facet_tier),
hover_text: Arc::clone(&solid_hover_text),
facet_tier: Arc::clone(&solid_facet_tier),
render_ctx: Arc::clone(&render_ctx),
solid_active_planes: Arc::clone(&solid_active_planes),
trace_active_planes: Arc::clone(&trace_active_planes),
mesh_bounding_radius: Arc::clone(&mesh_bounding_radius),
}));
diagram_wiring::setup_diagram_hover_and_click_callbacks(
&ui,
&diagram_pick,
&diagram_tooth_pick,
&diagram_hover_text,
&diagram_facet_tier,
&solid_preview_state,
);
let library_source: Arc<Mutex<LibrarySource>> = Arc::new(Mutex::new(LibrarySource::default()));
let initial_custom_mats: Vec<GemMaterial> = db
.lock()
.unwrap()
.get_custom_materials()
.unwrap_or_default()
.iter()
.map(gem_material_from_row)
.collect();
render_ctx.lock().unwrap().custom_materials = Arc::new(initial_custom_mats.clone());
refresh_material_options(&ui, &initial_custom_mats);
let settings_path = settings::store::default_settings_path();
settings::store::migrate_legacy_settings_if_needed(&settings_path);
let loaded_settings = settings::store::load_or_default(&settings_path);
apply_loaded_settings(&ui, &render_ctx, &loaded_settings);
editor_layout::apply_editor_layout_from_settings(&ui, &loaded_settings.settings);
refresh_lighting_preset_options(&ui, &loaded_settings.presets);
refresh_worker_options(&ui, &loaded_settings.settings.remote_workers);
ui.global::<RemoteWorkerModel>()
.set_denoise_enabled(loaded_settings.settings.denoise_enabled);
let settings_store = Arc::new(SettingsPersister::spawn(settings_path, loaded_settings));
let detached_frame_target = render::detached_render::setup_live_render_visibility_callbacks(
&ui,
&render_ctx,
&solid_preview_state,
);
render::detached_render::recompute_tab_visible(&ui, &render_ctx);
let ui_weak_render = ui.as_weak();
let render_ctx_frame_push = render_ctx.clone();
let trace_active_planes_push = Arc::clone(&trace_active_planes);
let solid_active_planes_push = Arc::clone(&solid_active_planes);
spawn_render_thread(
ui_weak_render,
render_ctx.clone(),
move |ui: &MainWindow, img: slint::SharedPixelBuffer<slint::Rgba8Pixel>| {
let slint_img = slint::Image::from_rgba8(img);
ui.global::<ViewportModel>()
.set_render_image(slint_img.clone());
ui.global::<ViewportModel>().set_has_render(true);
let detached_weak = detached_frame_target.lock().unwrap().clone();
if let Some(detached) = detached_weak.and_then(|weak| weak.upgrade()) {
detached.set_render_image(slint_img);
detached.set_has_render(true);
}
let planes_now = render_ctx_frame_push
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.active_planes
.clone();
*trace_active_planes_push
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(planes_now);
ui.global::<SolidPreviewModel>()
.set_trace_matches_solid(planes_generations_match(
&trace_active_planes_push,
&solid_active_planes_push,
));
},
|ui: &MainWindow,
brilliance: f32,
fire: f32,
scintillation: f32,
windowing: f32,
extinction: f32,
graph_brilliance: [f32; 19],
graph_extinction: [f32; 19],
graph_windowing: [f32; 19],
cam_pitch_deg: f32| {
ui.global::<ViewportModel>().set_brilliance_pct(brilliance);
ui.global::<ViewportModel>().set_fire_index(fire);
ui.global::<ViewportModel>()
.set_scintillation_pct(scintillation);
ui.global::<ViewportModel>().set_windowing_pct(windowing);
ui.global::<ViewportModel>().set_extinction_pct(extinction);
ui.global::<TiltModel>()
.set_graph_brilliance(slint::ModelRc::new(slint::VecModel::from(
graph_brilliance.to_vec(),
)));
ui.global::<TiltModel>()
.set_graph_extinction(slint::ModelRc::new(slint::VecModel::from(
graph_extinction.to_vec(),
)));
ui.global::<TiltModel>()
.set_graph_windowing(slint::ModelRc::new(slint::VecModel::from(
graph_windowing.to_vec(),
)));
ui.global::<TiltModel>()
.set_graph_brilliance_path(tilt_curve_path(&graph_brilliance).into());
ui.global::<TiltModel>()
.set_graph_extinction_path(tilt_curve_path(&graph_extinction).into());
ui.global::<TiltModel>()
.set_graph_windowing_path(tilt_curve_path(&graph_windowing).into());
ui.global::<TiltModel>().set_cam_pitch_deg(cam_pitch_deg);
},
);
let settings_store_panel = settings_store.clone();
ui.global::<LibraryModel>()
.on_panel_collapsed_changed(move |collapsed: bool| {
settings_store_panel.update(|s| s.settings.library_panel_collapsed = collapsed);
});
editor_layout::setup_editor_layout_callbacks(&ui, &settings_store);
setup_user_manual_callback(&ui);
setup_reveal_last_saved_callback(&ui);
render::camera_lighting::setup_camera_and_lighting_callbacks(
&ui,
&render_ctx,
&settings_store,
&solid_preview_state,
&mesh_bounding_radius,
);
render::material_quality::setup_material_changed_callback(&ui, &render_ctx, &settings_store);
render::material_quality::setup_backdrop_callback(&ui, &render_ctx, &settings_store);
render::material_quality::setup_material_and_quality_callbacks(
&ui,
&render_ctx,
&settings_store,
);
render::material_quality::setup_material_effect_override_callbacks(
&ui,
&render_ctx,
&settings_store,
);
render::camera_lighting::setup_environment_map_callbacks(&ui, &render_ctx, &settings_store);
render::lighting_presets::setup_lighting_preset_callbacks(&ui, &render_ctx, &settings_store);
render::render_export::setup_render_export_callbacks(&ui, &render_ctx, &settings_store);
optics::custom_materials::setup_custom_material_callbacks(&ui, &render_ctx, &db);
library::clipboard::setup_copy_callbacks(&ui);
tilt::tilt_profile::setup_tilt_profile_callback(&ui, &render_ctx);
tilt::tilt_hover_preview::setup_tilt_hover_preview_callback(&ui, &render_ctx);
library::diagram_list::load_filter_options_and_initial_list(&ui, &db);
library::diagram_list::setup_search_and_filter_callbacks(&ui, &db, &library_source);
library::diagram_list::setup_diagram_selection_and_export_callbacks(
&ui,
&db,
&library_source,
&render_ctx,
&solid_preview_state,
);
library::diagram_list::setup_performance_filter_callbacks(&ui);
library::local::setup_import_callback(&ui, &db, &library_source, &settings_store);
library::local::setup_rename_callback(&ui, &db, &library_source);
library::local::setup_set_shape_callback(&ui, &db, &library_source);
library::local::setup_delete_callback(&ui, &db, &library_source, &render_ctx);
library::local::setup_export_asc_callback(&ui, &db, &library_source);
library::local::setup_ignore_toggle_callback(&ui, &db, &library_source);
library::local::setup_add_tag_callback(&ui, &db, &library_source);
library::local::setup_remove_tag_callback(&ui, &db, &library_source);
library::detail::setup_save_metadata_callback(&ui, &db, &library_source, &render_ctx);
#[cfg(feature = "editor")]
let solid_pick_state = SolidPickState {
pick: Arc::clone(&solid_pick),
hover_text: Arc::clone(&solid_hover_text),
facet_tier: Arc::clone(&solid_facet_tier),
};
#[cfg(feature = "editor")]
editor::setup_editor_callbacks(
&ui,
&db,
&library_source,
&render_ctx,
&solid_preview_state,
&solid_last_solved,
&solid_pick_state,
);
let settings_store_solid_view_mode = settings_store.clone();
let render_ctx_solid_view_mode = render_ctx.clone();
let preview_state_solid_view_mode = Arc::clone(&solid_preview_state);
let ui_weak_solid_view_mode = ui.as_weak();
ui.global::<SolidPreviewModel>()
.on_view_mode_changed(move |mode: i32| {
settings_store_solid_view_mode.update(|s| {
s.settings.solid_view_mode = u8::try_from(mode).unwrap_or_default();
});
if let Some(ui) = ui_weak_solid_view_mode.upgrade() {
render::detached_render::recompute_tab_visible(&ui, &render_ctx_solid_view_mode);
let mut ctx = render_ctx_solid_view_mode
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if mode == 1 || mode == 2 {
ctx.dirty = true;
}
render::camera_lighting::resubmit_at_current_pose(
&ui,
&ctx,
&preview_state_solid_view_mode,
);
drop(ctx);
}
});
let render_ctx_viewport_resized = render_ctx.clone();
let preview_state_viewport_resized = Arc::clone(&solid_preview_state);
let ui_weak_viewport_resized = ui.as_weak();
ui.global::<SolidPreviewModel>()
.on_viewport_resized(move || {
let ui_weak = ui_weak_viewport_resized.clone();
let render_ctx = render_ctx_viewport_resized.clone();
let preview_state = Arc::clone(&preview_state_viewport_resized);
slint::Timer::single_shot(std::time::Duration::ZERO, move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
render::camera_lighting::resubmit_at_current_pose(&ui, &ctx, &preview_state);
});
});
let settings_store_live_view_mode = settings_store.clone();
let render_ctx_live_view_mode = render_ctx.clone();
let preview_state_live_view_mode = Arc::clone(&solid_preview_state);
let ui_weak_live_view_mode = ui.as_weak();
ui.global::<ViewportModel>()
.on_live_view_mode_changed(move |mode: i32| {
settings_store_live_view_mode.update(|s| {
s.settings.live_view_mode = u8::try_from(mode).unwrap_or_default();
});
let Some(ui) = ui_weak_live_view_mode.upgrade() else {
return;
};
render::detached_render::recompute_tab_visible(&ui, &render_ctx_live_view_mode);
let mut ctx = render_ctx_live_view_mode.lock().unwrap();
if mode == 1 {
ctx.dirty = true;
} else if mode == 0 {
render::camera_lighting::resubmit_live_solid(
&ui,
&ctx,
&preview_state_live_view_mode,
);
}
drop(ctx);
});
let settings_store_auto_solve_budget = settings_store.clone();
ui.global::<EditorModel>()
.on_auto_solve_budget_ms_changed(move |budget_ms: i32| {
settings_store_auto_solve_budget.update(|s| {
s.settings.editor_auto_solve_budget_ms =
u32::try_from(budget_ms).unwrap_or_default();
});
});
library::remote::setup_library_source_callbacks(&ui, &db, &library_source, &settings_store);
library::remote::setup_mirror_sync_callbacks(&ui, &db, &settings_store);
setup_worker_callbacks(&ui, &render_ctx, &settings_store);
batch::preview_cache::setup_preview_thumbnail_callback(&ui, &db);
batch::preview::setup_preview_batch_callbacks(&ui, &db, &render_ctx, &settings_store);
batch::tilt::setup_tilt_batch_callbacks(&ui, &db, &render_ctx, &settings_store);
let remote_rendering_timer = setup_remote_rendering(&ui, &render_ctx, &settings_store);
let render_ctx_close = render_ctx.clone();
let settings_store_close = settings_store.clone();
let ui_weak_close = ui.as_weak();
ui.window().on_close_requested(move || {
let is_dirty = ui_weak_close
.upgrade()
.is_some_and(|ui| ui.global::<EditorModel>().get_is_dirty());
if !is_dirty {
render_ctx_close.lock().unwrap().running = false;
settings_store_close.flush();
return slint::CloseRequestResponse::HideWindow;
}
if let Some(ui) = ui_weak_close.upgrade() {
ui.set_close_confirm_open(true);
}
slint::CloseRequestResponse::KeepWindowShown
});
setup_close_confirm_callbacks(&ui, &render_ctx, &settings_store);
Ok(MainWindowHandle {
ui,
render_ctx,
settings_store,
remote_rendering_timer,
})
}
fn setup_close_confirm_callbacks(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
settings_store: &Arc<SettingsPersister>,
) {
let render_ctx_save = render_ctx.clone();
let settings_store_save = settings_store.clone();
let ui_weak_save = ui.as_weak();
ui.on_close_confirm_save(move || {
let Some(ui) = ui_weak_save.upgrade() else {
return;
};
ui.set_close_confirm_open(false);
ui.global::<EditorModel>().invoke_save_native();
if ui.global::<EditorModel>().get_is_dirty() {
return;
}
render_ctx_save.lock().unwrap().running = false;
settings_store_save.flush();
let _ = ui.hide();
});
let render_ctx_discard = render_ctx.clone();
let settings_store_discard = settings_store.clone();
let ui_weak_discard = ui.as_weak();
ui.on_close_confirm_discard(move || {
let Some(ui) = ui_weak_discard.upgrade() else {
return;
};
ui.set_close_confirm_open(false);
render_ctx_discard.lock().unwrap().running = false;
settings_store_discard.flush();
let _ = ui.hide();
});
}
fn starting_dir_from_picker_field(current: &str) -> Option<std::path::PathBuf> {
if current.is_empty() {
return None;
}
let path = std::path::Path::new(current);
if path.is_dir() {
Some(path.to_path_buf())
} else if path.is_file() {
path.parent().map(std::path::Path::to_path_buf)
} else {
None
}
}
fn user_manual_candidates(exe_dir: &std::path::Path) -> Vec<std::path::PathBuf> {
vec![
exe_dir.join("docs/manual/README.md"),
exe_dir.join("../docs/manual/README.md"),
exe_dir.join("../../apps/indicatrix-cut/docs/manual/README.md"),
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/manual/README.md"),
]
}
fn locate_user_manual() -> Option<std::path::PathBuf> {
let exe_dir = std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(std::path::Path::to_path_buf))?;
user_manual_candidates(&exe_dir)
.into_iter()
.find(|candidate| candidate.is_file())
}
fn setup_user_manual_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.on_open_user_manual(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Some(manual_path) = locate_user_manual() else {
show_toast(&ui, "User manual not found", "error");
return;
};
open_with_os_handler(&manual_path.to_string_lossy());
});
}
fn open_with_os_handler(path: &str) {
#[cfg(target_os = "linux")]
let _ = std::process::Command::new("xdg-open").arg(path).spawn();
#[cfg(target_os = "windows")]
let _ = std::process::Command::new("cmd")
.args(["/C", "start", "", path])
.spawn();
#[cfg(target_os = "macos")]
let _ = std::process::Command::new("open").arg(path).spawn();
}
fn setup_reveal_last_saved_callback(ui: &MainWindow) {
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_reveal_last_saved_path(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let saved = ui.global::<EditorModel>().get_last_saved_path();
if saved.is_empty() {
return;
}
let path = std::path::PathBuf::from(saved.as_str());
let target = path.parent().unwrap_or(&path);
open_with_os_handler(&target.to_string_lossy());
});
}
pub fn show_toast(ui: &MainWindow, msg: &str, toast_type: &str) {
ui.set_toast_message(msg.into());
ui.set_toast_type(toast_type.into());
ui.set_toast_visible(true);
let generation = ui.get_toast_generation() + 1;
ui.set_toast_generation(generation);
if toast_type == "error" || toast_type == "warning" {
return;
}
let ui_weak = ui.as_weak();
slint::Timer::single_shot(std::time::Duration::from_millis(3500), move || {
if let Some(ui) = ui_weak.upgrade()
&& ui.get_toast_generation() == generation
{
ui.set_toast_visible(false);
}
});
}
#[cfg(test)]
mod tests {
use super::user_manual_candidates;
#[test]
fn user_manual_candidates_are_relative_to_exe_dir_in_order() {
let exe_dir = std::path::Path::new("/opt/indicatrix-cut/bin");
let candidates = user_manual_candidates(exe_dir);
assert_eq!(
candidates[0],
std::path::Path::new("/opt/indicatrix-cut/bin/docs/manual/README.md")
);
assert_eq!(
candidates[1],
std::path::Path::new("/opt/indicatrix-cut/bin/../docs/manual/README.md")
);
assert_eq!(
candidates[2],
std::path::Path::new(
"/opt/indicatrix-cut/bin/../../apps/indicatrix-cut/docs/manual/README.md"
)
);
assert_eq!(
candidates[3],
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/manual/README.md")
);
assert_eq!(candidates.len(), 4);
}
}