@@ -561,6 +561,29 @@ struct TabDragPreview {
561561 ordered_ids : Vec < i64 > ,
562562}
563563
564+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
565+ struct TabDragCommitRequest {
566+ context : TabDragContext ,
567+ original_ids : Vec < i64 > ,
568+ ordered_ids : Vec < i64 > ,
569+ }
570+
571+ impl From < & TabDragPreview > for TabDragCommitRequest {
572+ fn from ( preview : & TabDragPreview ) -> Self {
573+ Self {
574+ context : preview. context . clone ( ) ,
575+ original_ids : preview. original_ids . clone ( ) ,
576+ ordered_ids : preview. ordered_ids . clone ( ) ,
577+ }
578+ }
579+ }
580+
581+ #[ derive( Debug , PartialEq , Eq ) ]
582+ enum TabDragSettlement {
583+ Ignored ,
584+ Settled ( Result < bool , String > ) ,
585+ }
586+
564587fn same_stable_ids ( left : & [ i64 ] , right : & [ i64 ] ) -> bool {
565588 if left. len ( ) != right. len ( ) || left. contains ( & 0 ) {
566589 return false ;
@@ -593,6 +616,49 @@ fn dispatch_tab_drag_commit_with(
593616 Ok ( true )
594617}
595618
619+ fn settle_tab_drag_commit_with (
620+ preview : & mut Option < TabDragPreview > ,
621+ authoritative_ids : & [ i64 ] ,
622+ request : TabDragCommitRequest ,
623+ apply : impl FnOnce ( i64 , Vec < i64 > ) -> Result < ( ) , String > ,
624+ ) -> TabDragSettlement {
625+ if preview
626+ . as_ref ( )
627+ . is_none_or ( |preview| preview. context != request. context )
628+ {
629+ return TabDragSettlement :: Ignored ;
630+ }
631+
632+ let result = dispatch_tab_drag_commit_with (
633+ preview. as_ref ( ) ,
634+ authoritative_ids,
635+ & request. context ,
636+ & request. original_ids ,
637+ request. ordered_ids ,
638+ apply,
639+ ) ;
640+ * preview = None ;
641+ TabDragSettlement :: Settled ( result)
642+ }
643+
644+ fn end_tab_drag_preview_if_owned (
645+ preview : & mut Option < TabDragPreview > ,
646+ authoritative_ids : & [ i64 ] ,
647+ context : & TabDragContext ,
648+ original_ids : & [ i64 ] ,
649+ ) -> bool {
650+ let owned = preview. as_ref ( ) . is_some_and ( |preview| {
651+ preview. context == * context
652+ && preview. original_ids == original_ids
653+ && preview. ordered_ids == original_ids
654+ && authoritative_ids == original_ids
655+ } ) ;
656+ if owned {
657+ * preview = None ;
658+ }
659+ owned
660+ }
661+
596662fn rename_target_label ( projects : & [ Project ] , target : RenameTarget ) -> Option < & str > {
597663 match target {
598664 RenameTarget :: Project ( project_id) => projects
@@ -3700,40 +3766,97 @@ impl App {
37003766 }
37013767 }
37023768
3703- fn commit_tab_drag (
3769+ fn end_tab_drag_preview (
37043770 & mut self ,
37053771 project_id : i64 ,
37063772 source_id : i64 ,
37073773 context_generation : u64 ,
37083774 original_ids : & [ i64 ] ,
3709- ordered_ids : Vec < i64 > ,
37103775 ) {
3711- let authoritative = self . active_project_tab_ids ( project_id) ;
37123776 let context = TabDragContext {
37133777 project_id,
37143778 source_id,
37153779 generation : context_generation,
37163780 } ;
3717- let result = dispatch_tab_drag_commit_with (
3718- self . tab_drag_preview . as_ref ( ) ,
3781+ let authoritative = self . active_project_tab_ids ( project_id) ;
3782+ end_tab_drag_preview_if_owned (
3783+ & mut self . tab_drag_preview ,
37193784 & authoritative,
37203785 & context,
37213786 original_ids,
3787+ ) ;
3788+ }
3789+
3790+ fn commit_tab_drag (
3791+ & mut self ,
3792+ project_id : i64 ,
3793+ source_id : i64 ,
3794+ context_generation : u64 ,
3795+ original_ids : & [ i64 ] ,
3796+ ordered_ids : Vec < i64 > ,
3797+ ) {
3798+ let authoritative = self . active_project_tab_ids ( project_id) ;
3799+ let request = TabDragCommitRequest {
3800+ context : TabDragContext {
3801+ project_id,
3802+ source_id,
3803+ generation : context_generation,
3804+ } ,
3805+ original_ids : original_ids. to_vec ( ) ,
37223806 ordered_ids,
3807+ } ;
3808+ let runtime = & self . runtime ;
3809+ let client = & self . client ;
3810+ let settlement = settle_tab_drag_commit_with (
3811+ & mut self . tab_drag_preview ,
3812+ & authoritative,
3813+ request,
37233814 |project_id, ordered_ids| {
3724- self . runtime
3725- . block_on ( self . client . reorder_tabs ( project_id, ordered_ids) )
3815+ runtime
3816+ . block_on ( client. reorder_tabs ( project_id, ordered_ids) )
37263817 . map_err ( |error| error. to_string ( ) )
37273818 } ,
37283819 ) ;
3729- self . cancel_tab_drag ( ) ;
3820+ tracing:: debug!(
3821+ ?settlement,
3822+ project_id,
3823+ source_id,
3824+ context_generation,
3825+ "Iced tab drag settlement"
3826+ ) ;
3827+ let TabDragSettlement :: Settled ( result) = settlement else {
3828+ return ;
3829+ } ;
3830+ self . tab_strip_generation = self . tab_strip_generation . wrapping_add ( 1 ) ;
37303831 if let Err ( error) = result {
37313832 tracing:: warn!( ?error, project_id, source_id, "Iced tab reorder failed" ) ;
37323833 self . set_status ( format ! ( "reorder tabs: {error}" ) ) ;
37333834 }
37343835 self . reconcile ( ) ;
37353836 }
37363837
3838+ pub ( crate ) fn tab_pointer_released ( & mut self ) {
3839+ let Some ( preview) = self . tab_drag_preview . as_ref ( ) else {
3840+ tracing:: debug!( "Iced root release had no tab drag preview" ) ;
3841+ return ;
3842+ } ;
3843+ tracing:: debug!(
3844+ project_id = preview. context. project_id,
3845+ source_id = preview. context. source_id,
3846+ generation = preview. context. generation,
3847+ ordered_ids = ?preview. ordered_ids,
3848+ "Iced root release settling tab drag preview"
3849+ ) ;
3850+ let request = TabDragCommitRequest :: from ( preview) ;
3851+ self . commit_tab_drag (
3852+ request. context . project_id ,
3853+ request. context . source_id ,
3854+ request. context . generation ,
3855+ & request. original_ids ,
3856+ request. ordered_ids ,
3857+ ) ;
3858+ }
3859+
37373860 pub ( crate ) fn tab_strip_event ( & mut self , event : TabStripEvent ) {
37383861 match event {
37393862 TabStripEvent :: Started {
@@ -3770,6 +3893,14 @@ impl App {
37703893 & original_ids,
37713894 ordered_ids,
37723895 ) ,
3896+ TabStripEvent :: Ended {
3897+ project_id,
3898+ source_id,
3899+ context_generation,
3900+ original_ids,
3901+ } => {
3902+ self . end_tab_drag_preview ( project_id, source_id, context_generation, & original_ids)
3903+ }
37733904 TabStripEvent :: Cancel { context_generation } => {
37743905 if context_generation == self . tab_strip_generation {
37753906 self . cancel_tab_drag ( ) ;
@@ -5964,6 +6095,166 @@ mod tests {
59646095 assert_eq ! ( stale_generation, Ok ( false ) ) ;
59656096 }
59666097
6098+ #[ test]
6099+ fn tab_drag_settlement_is_owned_once_in_either_release_order ( ) {
6100+ let preview = TabDragPreview {
6101+ context : TabDragContext {
6102+ project_id : 7 ,
6103+ source_id : 10 ,
6104+ generation : 4 ,
6105+ } ,
6106+ original_ids : vec ! [ 10 , 20 , 30 ] ,
6107+ ordered_ids : vec ! [ 20 , 30 , 10 ] ,
6108+ } ;
6109+ let fallback = TabDragCommitRequest :: from ( & preview) ;
6110+ let direct = fallback. clone ( ) ;
6111+
6112+ for ( first, second) in [
6113+ ( fallback. clone ( ) , direct. clone ( ) ) ,
6114+ ( direct. clone ( ) , fallback. clone ( ) ) ,
6115+ ] {
6116+ let mut current = Some ( preview. clone ( ) ) ;
6117+ let mut calls = 0 ;
6118+ let first_result = settle_tab_drag_commit_with (
6119+ & mut current,
6120+ & [ 10 , 20 , 30 ] ,
6121+ first,
6122+ |project_id, ordered_ids| {
6123+ calls += 1 ;
6124+ assert_eq ! ( project_id, 7 ) ;
6125+ assert_eq ! ( ordered_ids, [ 20 , 30 , 10 ] ) ;
6126+ Ok ( ( ) )
6127+ } ,
6128+ ) ;
6129+ assert_eq ! ( first_result, TabDragSettlement :: Settled ( Ok ( true ) ) ) ;
6130+ assert ! ( current. is_none( ) ) ;
6131+
6132+ let second_result =
6133+ settle_tab_drag_commit_with ( & mut current, & [ 10 , 20 , 30 ] , second, |_, _| {
6134+ panic ! ( "duplicate tab release dispatched" )
6135+ } ) ;
6136+ assert_eq ! ( second_result, TabDragSettlement :: Ignored ) ;
6137+ assert_eq ! ( calls, 1 ) ;
6138+ }
6139+ }
6140+
6141+ #[ test]
6142+ fn stale_or_unowned_release_does_not_clear_a_newer_preview ( ) {
6143+ let newer = TabDragPreview {
6144+ context : TabDragContext {
6145+ project_id : 7 ,
6146+ source_id : 20 ,
6147+ generation : 5 ,
6148+ } ,
6149+ original_ids : vec ! [ 10 , 20 , 30 ] ,
6150+ ordered_ids : vec ! [ 20 , 10 , 30 ] ,
6151+ } ;
6152+ let stale = TabDragCommitRequest {
6153+ context : TabDragContext {
6154+ project_id : 7 ,
6155+ source_id : 10 ,
6156+ generation : 4 ,
6157+ } ,
6158+ original_ids : vec ! [ 10 , 20 , 30 ] ,
6159+ ordered_ids : vec ! [ 20 , 30 , 10 ] ,
6160+ } ;
6161+ let mut current = Some ( newer. clone ( ) ) ;
6162+ assert_eq ! (
6163+ settle_tab_drag_commit_with( & mut current, & [ 10 , 20 , 30 ] , stale, |_, _| {
6164+ panic!( "stale release dispatched" )
6165+ } ) ,
6166+ TabDragSettlement :: Ignored
6167+ ) ;
6168+ assert_eq ! ( current, Some ( newer) ) ;
6169+
6170+ let mut absent = None ;
6171+ assert_eq ! (
6172+ settle_tab_drag_commit_with(
6173+ & mut absent,
6174+ & [ 10 , 20 , 30 ] ,
6175+ TabDragCommitRequest {
6176+ context: TabDragContext {
6177+ project_id: 7 ,
6178+ source_id: 10 ,
6179+ generation: 4 ,
6180+ } ,
6181+ original_ids: vec![ 10 , 20 , 30 ] ,
6182+ ordered_ids: vec![ 20 , 30 , 10 ] ,
6183+ } ,
6184+ |_, _| panic!( "unowned release dispatched" ) ,
6185+ ) ,
6186+ TabDragSettlement :: Ignored
6187+ ) ;
6188+ }
6189+
6190+ #[ test]
6191+ fn exact_subthreshold_end_clears_without_accepting_stale_or_moved_state ( ) {
6192+ let original = vec ! [ 10 , 20 , 30 ] ;
6193+ let context = TabDragContext {
6194+ project_id : 7 ,
6195+ source_id : 10 ,
6196+ generation : 4 ,
6197+ } ;
6198+ let preview = TabDragPreview {
6199+ context : context. clone ( ) ,
6200+ original_ids : original. clone ( ) ,
6201+ ordered_ids : original. clone ( ) ,
6202+ } ;
6203+ let mut exact = Some ( preview. clone ( ) ) ;
6204+ assert ! ( end_tab_drag_preview_if_owned(
6205+ & mut exact, & original, & context, & original,
6206+ ) ) ;
6207+ assert ! ( exact. is_none( ) ) ;
6208+
6209+ let mut stale = Some ( preview. clone ( ) ) ;
6210+ assert ! ( !end_tab_drag_preview_if_owned(
6211+ & mut stale,
6212+ & original,
6213+ & TabDragContext {
6214+ generation: 5 ,
6215+ ..context. clone( )
6216+ } ,
6217+ & original,
6218+ ) ) ;
6219+ assert_eq ! ( stale, Some ( preview. clone( ) ) ) ;
6220+
6221+ let moved = TabDragPreview {
6222+ ordered_ids : vec ! [ 20 , 10 , 30 ] ,
6223+ ..preview
6224+ } ;
6225+ let mut moved_state = Some ( moved. clone ( ) ) ;
6226+ assert ! ( !end_tab_drag_preview_if_owned(
6227+ & mut moved_state,
6228+ & original,
6229+ & context,
6230+ & original,
6231+ ) ) ;
6232+ assert_eq ! ( moved_state, Some ( moved) ) ;
6233+ }
6234+
6235+ #[ test]
6236+ fn crossed_threshold_return_to_origin_is_a_settled_noop ( ) {
6237+ let original = vec ! [ 10 , 20 , 30 ] ;
6238+ let preview = TabDragPreview {
6239+ context : TabDragContext {
6240+ project_id : 7 ,
6241+ source_id : 10 ,
6242+ generation : 4 ,
6243+ } ,
6244+ original_ids : original. clone ( ) ,
6245+ ordered_ids : original. clone ( ) ,
6246+ } ;
6247+ let request = TabDragCommitRequest :: from ( & preview) ;
6248+ let mut current = Some ( preview) ;
6249+ assert_eq ! (
6250+ settle_tab_drag_commit_with( & mut current, & original, request, |_, _| {
6251+ panic!( "return-to-origin commit dispatched a reorder" )
6252+ } ) ,
6253+ TabDragSettlement :: Settled ( Ok ( false ) )
6254+ ) ;
6255+ assert ! ( current. is_none( ) ) ;
6256+ }
6257+
59676258 #[ test]
59686259 fn tab_drag_commit_surfaces_the_authoritative_command_error_once ( ) {
59696260 let preview = TabDragPreview {
0 commit comments