forked from 1b-agent/opencadstudio-upstream
301 lines
13 KiB
Rust
301 lines
13 KiB
Rust
use super::{ModalKind, OpenCADStudio};
|
|
use crate::scene::pipeline::{GpuAdapter, GpuStatus};
|
|
|
|
impl OpenCADStudio {
|
|
pub(super) fn queue_startup_prompts(&mut self) {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if !self.default_assoc_prompted {
|
|
self.pending_startup_modals
|
|
.push_back(ModalKind::AssocPrompt);
|
|
}
|
|
// Automation sessions (#1349): once control traffic has arrived (or a
|
|
// web host opted in at boot) while control is enabled, the donation
|
|
// prompt is suppressed — a modal would block every automated
|
|
// operation until a client closed it by hand.
|
|
if self.donation_prompt_version != env!("OCS_APP_VERSION")
|
|
&& !(self.control.enabled && self.control.automation_seen)
|
|
{
|
|
self.pending_startup_modals
|
|
.push_back(ModalKind::DonationPrompt);
|
|
}
|
|
self.show_next_startup_modal();
|
|
}
|
|
|
|
pub(super) fn show_next_startup_modal(&mut self) {
|
|
if self.active_modal.is_none() && self.opening.is_none() && self.pending_opens.is_empty() {
|
|
// A queued AutosaveRestore can outlive its pending list when a
|
|
// concurrent open displaces the modal and the entry is consumed
|
|
// meanwhile; resurfacing it would show an empty prompt.
|
|
#[cfg(target_arch = "wasm32")]
|
|
while self.pending_startup_modals.front() == Some(&ModalKind::AutosaveRestore)
|
|
&& self.web_autosave_pending.is_empty()
|
|
{
|
|
self.pending_startup_modals.pop_front();
|
|
}
|
|
if let Some(&kind) = self.pending_startup_modals.front() {
|
|
self.active_modal = Some(kind);
|
|
self.reset_modal_geometry();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Queue the web crash-recovery prompt behind whatever startup modal is
|
|
/// already showing (donation, GPU warning…); the after-message hook in
|
|
/// `update` surfaces it as soon as the slot is free. No-op when nothing
|
|
/// is pending: once the autosave OPFS directory merely exists (even with
|
|
/// no entries), queueing unconditionally would show an empty
|
|
/// "Recover Drawing" modal on every boot.
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub(super) fn queue_autosave_restore_prompt(&mut self) {
|
|
if self.web_autosave_pending.is_empty() {
|
|
return;
|
|
}
|
|
if !self
|
|
.pending_startup_modals
|
|
.contains(&ModalKind::AutosaveRestore)
|
|
{
|
|
self.pending_startup_modals
|
|
.push_back(ModalKind::AutosaveRestore);
|
|
}
|
|
self.show_next_startup_modal();
|
|
}
|
|
|
|
/// Notice when the scene is drawn by a software rasterizer, or not at
|
|
/// all, and say so everywhere the user might look: the command line,
|
|
/// the log, a one-time popup, and a status-bar pill that stays.
|
|
///
|
|
/// Runs after every message; the pipeline records the adapter when iced
|
|
/// first builds a device and counts the viewport's draws in the meantime,
|
|
/// so the verdict lands on the first message after the first frame
|
|
/// (`subscription` keeps frames ticking until it does). A working GPU
|
|
/// gets no word here — the `[gpu] adapter:` log line already names it.
|
|
pub(super) fn refresh_gpu_status(&mut self) {
|
|
let Some(status) =
|
|
crate::scene::pipeline::gpu_status_if_changed(&mut self.gpu_status_generation)
|
|
else {
|
|
return;
|
|
};
|
|
if status == self.gpu_status {
|
|
return;
|
|
}
|
|
self.gpu_status = status;
|
|
let Some(identity) = self.gpu_status.identity() else {
|
|
return;
|
|
};
|
|
match &self.gpu_status {
|
|
GpuStatus::Software(adapter) => {
|
|
self.command_line
|
|
.push_warning(&software_rasterizer_warning(adapter));
|
|
}
|
|
GpuStatus::NoRenderer => {
|
|
crate::scene::pipeline::report_gpu_line(
|
|
"[gpu] no adapter: wgpu found nothing to draw with; the interface is on \
|
|
iced's software renderer and the scene is not drawn at all",
|
|
);
|
|
self.command_line
|
|
.push_warning(&crate::tr!("gpu", "command-line-no-renderer"));
|
|
}
|
|
GpuStatus::Unknown | GpuStatus::Hardware(_) => {}
|
|
}
|
|
// The popup: once per verdict, unless the user silenced this one.
|
|
let already_queued = self.active_modal == Some(ModalKind::GpuWarning)
|
|
|| self.pending_startup_modals.contains(&ModalKind::GpuWarning);
|
|
if self.gpu_warning_silenced != identity && !already_queued {
|
|
self.pending_startup_modals.push_back(ModalKind::GpuWarning);
|
|
self.show_next_startup_modal();
|
|
}
|
|
}
|
|
|
|
pub(super) fn mark_startup_modal_shown(&mut self) {
|
|
if self.active_modal.is_some()
|
|
&& self.active_modal.as_ref() == self.pending_startup_modals.front()
|
|
{
|
|
self.pending_startup_modals.pop_front();
|
|
}
|
|
if self.active_modal == Some(ModalKind::DonationPrompt)
|
|
&& self.donation_prompt_version != env!("OCS_APP_VERSION")
|
|
{
|
|
self.donation_prompt_version = env!("OCS_APP_VERSION").to_string();
|
|
self.save_config();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The command-line warning for a software rasterizer, naming the adapter so
|
|
/// the user can tell llvmpipe from SwiftShader from WARP.
|
|
pub(super) fn software_rasterizer_warning(adapter: &GpuAdapter) -> String {
|
|
let adapter = &adapter.name;
|
|
crate::tf!("GPU unavailable — rendering in software on the CPU ({adapter}). Large drawings will navigate slowly; check the graphics driver (a driver update without a reboot is the usual cause).")
|
|
.into_owned()
|
|
}
|
|
|
|
/// What usually puts a machine in this state, per platform. The cause is
|
|
/// almost always outside the app, and it differs: on Linux it is a driver
|
|
/// updated under a running kernel module, on Windows a driver that is
|
|
/// missing or a session (Remote Desktop, a VM) that has no GPU to offer, on
|
|
/// macOS a VM without a Metal device, in a browser hardware acceleration
|
|
/// switched off.
|
|
pub(crate) fn gpu_platform_hint() -> String {
|
|
if cfg!(target_arch = "wasm32") {
|
|
crate::tr!("gpu", "hint-web")
|
|
} else if cfg!(target_os = "windows") {
|
|
crate::tr!("gpu", "hint-windows")
|
|
} else if cfg!(target_os = "macos") {
|
|
crate::tr!("gpu", "hint-macos")
|
|
} else {
|
|
crate::tr!("gpu", "hint-linux")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::app::{config::AppConfig, Message};
|
|
use iced::wgpu;
|
|
|
|
#[test]
|
|
fn the_software_warning_names_the_adapter_and_every_platform_has_a_hint() {
|
|
let cpu = GpuAdapter {
|
|
name: "llvmpipe (LLVM 20.1.8, 256 bits)".to_string(),
|
|
backend: wgpu::Backend::Vulkan,
|
|
device_type: wgpu::DeviceType::Cpu,
|
|
};
|
|
let warning = software_rasterizer_warning(&cpu);
|
|
// The name carries parentheses and commas of its own; the localized
|
|
// template must still place it whole. The loader follows the system
|
|
// locale here, so the assertions stay language-neutral.
|
|
assert!(
|
|
warning.contains("(llvmpipe (LLVM 20.1.8, 256 bits))"),
|
|
"{warning}"
|
|
);
|
|
assert!(warning.contains("GPU"), "{warning}");
|
|
assert!(
|
|
!warning.contains("{adapter}") && !warning.contains("__ocs_"),
|
|
"{warning}"
|
|
);
|
|
// Every platform has a hint, and it is a sentence, not a missing key.
|
|
let hint = gpu_platform_hint();
|
|
assert!(hint.len() > 20 && !hint.contains("hint-"), "{hint}");
|
|
}
|
|
|
|
#[test]
|
|
fn silencing_the_popup_is_per_verdict_and_survives_a_settings_round_trip() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.gpu_status = GpuStatus::NoRenderer;
|
|
let _ = app.update(Message::GpuWarningSilence);
|
|
assert_eq!(app.gpu_warning_silenced, "no-renderer");
|
|
assert!(app.active_modal.is_none());
|
|
|
|
let saved = serde_json::to_string(&app.current_config()).unwrap();
|
|
let mut restarted = OpenCADStudio::new_for_test();
|
|
restarted.apply_config(serde_json::from_str(&saved).unwrap());
|
|
assert_eq!(restarted.gpu_warning_silenced, "no-renderer");
|
|
// A different verdict is not covered by that choice.
|
|
let software = GpuStatus::Software(GpuAdapter {
|
|
name: "llvmpipe".to_string(),
|
|
backend: wgpu::Backend::Vulkan,
|
|
device_type: wgpu::DeviceType::Cpu,
|
|
});
|
|
assert_ne!(software.identity().unwrap(), restarted.gpu_warning_silenced);
|
|
}
|
|
|
|
#[test]
|
|
fn donation_prompt_persists_once_per_version() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
// Older settings have no donation version; headless construction never prompts.
|
|
app.apply_config(serde_json::from_str::<AppConfig>("{}").unwrap());
|
|
assert!(app.active_modal.is_none());
|
|
assert!(app.pending_startup_modals.is_empty());
|
|
app.default_assoc_prompted = true;
|
|
app.queue_startup_prompts();
|
|
assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt));
|
|
assert!(app.donation_prompt_version.is_empty());
|
|
|
|
let _ = app.update(Message::ModalContentResized(iced::Size::new(540.0, 280.0)));
|
|
let saved = serde_json::to_string(&app.current_config()).unwrap();
|
|
let _ = app.update(Message::CloseModal);
|
|
assert!(app.active_modal.is_none());
|
|
|
|
let mut restarted = OpenCADStudio::new_for_test();
|
|
restarted.apply_config(serde_json::from_str(&saved).unwrap());
|
|
restarted.queue_startup_prompts();
|
|
assert_eq!(restarted.donation_prompt_version, env!("OCS_APP_VERSION"));
|
|
assert!(restarted.active_modal.is_none());
|
|
|
|
restarted.donation_prompt_version = "previous-release".to_string();
|
|
restarted.queue_startup_prompts();
|
|
assert_eq!(restarted.active_modal, Some(ModalKind::DonationPrompt));
|
|
let _ = restarted.update(Message::CommandEscape);
|
|
assert!(restarted.active_modal.is_none());
|
|
assert_eq!(restarted.donation_prompt_version, env!("OCS_APP_VERSION"));
|
|
let _ = restarted.update(Message::Noop);
|
|
assert!(restarted.active_modal.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn automation_sessions_skip_the_donation_prompt() {
|
|
// First control contact drops a still-queued donation prompt and
|
|
// stops later queueing while control stays enabled (#1349).
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.default_assoc_prompted = true;
|
|
app.queue_startup_prompts();
|
|
assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt));
|
|
let _ = app.update(Message::CloseModal);
|
|
app.donation_prompt_version = "previous-release".to_string();
|
|
app.pending_startup_modals
|
|
.push_back(ModalKind::DonationPrompt);
|
|
let _ = app.control_request(serde_json::json!({"op":"state"}));
|
|
assert!(app.pending_startup_modals.is_empty());
|
|
app.queue_startup_prompts();
|
|
assert_ne!(app.active_modal, Some(ModalKind::DonationPrompt));
|
|
|
|
// A still-queued (not yet shown) prompt is dropped on first contact.
|
|
let mut queued = OpenCADStudio::new_for_test();
|
|
queued.default_assoc_prompted = true;
|
|
queued.donation_prompt_version = "previous-release".to_string();
|
|
queued
|
|
.pending_startup_modals
|
|
.push_back(ModalKind::DonationPrompt);
|
|
let _ = queued.control_request(serde_json::json!({"op":"state"}));
|
|
queued.queue_startup_prompts();
|
|
assert_ne!(queued.active_modal, Some(ModalKind::DonationPrompt));
|
|
|
|
// A prompt already on screen is left for its reader; clients close
|
|
// it themselves, and a session with no control traffic still prompts.
|
|
let mut human = OpenCADStudio::new_for_test();
|
|
human.default_assoc_prompted = true;
|
|
human.queue_startup_prompts();
|
|
assert_eq!(human.active_modal, Some(ModalKind::DonationPrompt));
|
|
}
|
|
|
|
#[test]
|
|
fn startup_dialogs_wait_their_turn_and_preserve_unseen_prompts() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.apply_config(AppConfig::default());
|
|
app.queue_startup_prompts();
|
|
assert_eq!(app.active_modal, Some(ModalKind::AssocPrompt));
|
|
let _ = app.update(Message::UpdateCheckResult(Some(
|
|
crate::io::update_check::UpdateInfo {
|
|
version: "next-release".to_string(),
|
|
body: String::new(),
|
|
},
|
|
)));
|
|
assert_eq!(app.active_modal, Some(ModalKind::AssocPrompt));
|
|
let _ = app.update(Message::AssocPromptNo);
|
|
assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt));
|
|
|
|
// A file-recovery dialog may interrupt startup before the first frame.
|
|
app.active_modal = Some(ModalKind::Recovery);
|
|
let _ = app.update(Message::CloseModal);
|
|
assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt));
|
|
assert!(app.donation_prompt_version.is_empty());
|
|
let _ = app.update(Message::ModalContentResized(iced::Size::new(540.0, 280.0)));
|
|
assert_eq!(app.donation_prompt_version, env!("OCS_APP_VERSION"));
|
|
let _ = app.update(Message::CloseModal);
|
|
assert_eq!(app.active_modal, Some(ModalKind::UpdateNotice));
|
|
let _ = app.update(Message::UpdateNoticeClose);
|
|
assert!(app.active_modal.is_none());
|
|
assert!(app.pending_startup_modals.is_empty());
|
|
}
|
|
}
|