Skip to content

Commit 0abd3e4

Browse files
committed
fix(iced): settle tab drags across reflow
1 parent abe3997 commit 0abd3e4

5 files changed

Lines changed: 852 additions & 74 deletions

File tree

crates/roost-iced/src/app.rs

Lines changed: 299 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
564587
fn 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+
596662
fn 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

Comments
 (0)