@@ -28,7 +28,7 @@ use iced::{Element, Size, Subscription, Task, Theme};
2828
2929use crate :: handler:: {
3030 DialogHandler , DomainEditorHandler , ExportHandler , HomeHandler , MenuActionHandler ,
31- MessageHandler , SourceAssignmentHandler ,
31+ MessageHandler , SourceAssignmentHandler , rebuild_validation_cache ,
3232} ;
3333use crate :: message:: { Message , SettingsCategory } ;
3434use crate :: state:: { AppState , DialogState , DialogType , Settings , ViewState } ;
@@ -64,9 +64,12 @@ impl App {
6464 } ;
6565
6666 // Check for post-update status and show toast if update was successful
67- if let Some ( toast) = check_update_status ( ) {
67+ let startup_toast_task = if let Some ( toast) = check_update_status ( ) {
6868 app. state . toast = Some ( toast) ;
69- }
69+ schedule_toast_dismiss ( )
70+ } else {
71+ Task :: none ( )
72+ } ;
7073
7174 // Open the main window (daemon mode requires explicit window creation)
7275 // exit_on_close_request: false allows us to handle close events in our subscription
@@ -88,7 +91,7 @@ impl App {
8891 let init_menu = Task :: perform ( async { } , |_| Message :: InitNativeMenu ) ;
8992
9093 // Chain the tasks
91- let startup = open_window. chain ( init_menu) ;
94+ let startup = open_window. chain ( init_menu) . chain ( startup_toast_task ) ;
9295 ( app, startup)
9396 }
9497
@@ -178,7 +181,7 @@ impl App {
178181 ) ;
179182 self . state . toast =
180183 Some ( crate :: component:: feedback:: toast:: ToastState :: warning ( msg) ) ;
181- Task :: none ( )
184+ schedule_toast_dismiss ( )
182185 }
183186
184187 // =================================================================
@@ -309,12 +312,14 @@ impl App {
309312
310313 // Check if there's a pending project restoration
311314 // (when opening a .tss file, we need to apply saved mappings)
312- if let Some ( ( _, project) ) = self . state . pending_project_restore . take ( ) {
315+ let toast_task = if let Some ( ( _, project) ) =
316+ self . state . pending_project_restore . take ( )
317+ {
313318 // Check for changed source files
314319 let changed_files =
315320 crate :: handler:: project:: detect_changed_source_files ( & project) ;
316321
317- if !changed_files. is_empty ( ) {
322+ let toast_task = if !changed_files. is_empty ( ) {
318323 tracing:: warn!(
319324 "Source files changed since last save: {:?}" ,
320325 changed_files
@@ -327,14 +332,21 @@ impl App {
327332 self . state . toast = Some (
328333 crate :: component:: feedback:: toast:: ToastState :: warning ( msg) ,
329334 ) ;
330- }
335+ schedule_toast_dismiss ( )
336+ } else {
337+ Task :: none ( )
338+ } ;
331339
332340 // Restore mappings regardless (user can re-map if needed)
333341 crate :: handler:: project:: restore_project_mappings (
334342 & mut self . state ,
335343 & project,
336344 ) ;
337- }
345+ toast_task
346+ } else {
347+ Task :: none ( )
348+ } ;
349+ return toast_task;
338350 }
339351 Err ( err) => {
340352 tracing:: error!( "Failed to load study: {}" , err) ;
@@ -371,6 +383,12 @@ impl App {
371383 {
372384 domain_state. set_validation_cache ( report) ;
373385 }
386+ // Rebuild validation UI cache since results changed
387+ if let ViewState :: DomainEditor ( editor) = & self . state . view
388+ && editor. domain == domain
389+ {
390+ rebuild_validation_cache ( & mut self . state , & domain) ;
391+ }
374392 Task :: none ( )
375393 }
376394
@@ -469,11 +487,53 @@ impl App {
469487 }
470488 ToastMessage :: Show ( toast_state) => {
471489 self . state . toast = Some ( toast_state) ;
472- Task :: none ( )
490+ // Schedule auto-dismiss after 5 seconds (#187)
491+ schedule_toast_dismiss ( )
473492 }
474493 }
475494 }
495+ }
496+
497+ // =============================================================================
498+ // EVENT-DRIVEN TIMERS (#187, #193)
499+ // =============================================================================
500+
501+ /// Duration before a toast auto-dismisses.
502+ const TOAST_DISMISS_DELAY : std:: time:: Duration = std:: time:: Duration :: from_secs ( 5 ) ;
476503
504+ /// Duration before checking if auto-save should trigger (debounce).
505+ const AUTO_SAVE_DEBOUNCE_DELAY : std:: time:: Duration = std:: time:: Duration :: from_secs ( 2 ) ;
506+
507+ /// Schedule a one-shot timer to auto-dismiss the current toast.
508+ ///
509+ /// This replaces the polling subscription with an event-driven pattern.
510+ /// Returns a Task that sleeps for 5 seconds then sends a Dismiss message.
511+ fn schedule_toast_dismiss ( ) -> Task < Message > {
512+ use crate :: component:: feedback:: toast:: ToastMessage ;
513+
514+ Task :: perform (
515+ async {
516+ tokio:: time:: sleep ( TOAST_DISMISS_DELAY ) . await ;
517+ } ,
518+ |( ) | Message :: Toast ( ToastMessage :: Dismiss ) ,
519+ )
520+ }
521+
522+ /// Schedule a one-shot timer to check if auto-save should trigger.
523+ ///
524+ /// This replaces the polling subscription with an event-driven pattern.
525+ /// Multiple timers can be in-flight - `should_auto_save()` returns false
526+ /// if already saved or save in progress, making extra triggers harmless.
527+ pub fn schedule_auto_save_check ( ) -> Task < Message > {
528+ Task :: perform (
529+ async {
530+ tokio:: time:: sleep ( AUTO_SAVE_DEBOUNCE_DELAY ) . await ;
531+ } ,
532+ |( ) | Message :: AutoSaveTick ,
533+ )
534+ }
535+
536+ impl App {
477537 /// Render the view for a specific window.
478538 ///
479539 /// This is a pure function that produces UI based on current state.
0 commit comments