fix(segmented button): tab dnd

This commit is contained in:
Ashley Wulber 2026-01-21 19:21:46 -05:00 committed by Michael Murphy
parent 689f25be53
commit d71c42102d

View file

@ -297,11 +297,8 @@ where
} }
/// Enable drag-and-drop support for tabs using the provided payload builder. /// Enable drag-and-drop support for tabs using the provided payload builder.
pub fn enable_tab_drag( pub fn enable_tab_drag(mut self, mime: String) -> Self {
mut self, self.tab_drag = Some(TabDragSource::new(mime));
payload: impl Fn(Entity) -> Option<(String, Vec<u8>)> + 'static,
) -> Self {
self.tab_drag = Some(TabDragSource::new(payload));
self self
} }
@ -664,28 +661,29 @@ where
bounds: Rectangle, bounds: Rectangle,
cursor: Point, cursor: Point,
) -> Option<DropHint> { ) -> Option<DropHint> {
let dragging = state.dragging_tab?; let _ = state.dragging_tab?;
self.variant_bounds(state, bounds) self.variant_bounds(state, bounds)
.filter_map(|item| match item { .filter_map(|item| match item {
ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)), ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)),
_ => None, _ => None,
}) })
.find_map(|(entity, rect)| { .map(|(entity, rect)| {
let before = if Self::VERTICAL { let before = if Self::VERTICAL {
cursor.y < rect.center_y() cursor.y < rect.center_y()
} else { } else {
cursor.x < rect.center_x() cursor.x < rect.center_x()
}; };
Some(DropHint { DropHint {
entity, entity,
side: if before { side: if before {
DropSide::Before DropSide::Before
} else { } else {
DropSide::After DropSide::After
}, },
}) }
}) })
.next()
} }
fn start_tab_drag( fn start_tab_drag(
@ -713,33 +711,24 @@ where
tab_drag.threshold tab_drag.threshold
); );
let Some((mime, data)) = (tab_drag.payload)(entity) else { let data_len = 0;
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"start_tab_drag aborted entity={:?}: payload builder returned None",
entity
);
return false;
};
let data_len = data.len();
let mime_label = mime.clone();
iced_core::clipboard::start_dnd::<crate::Theme, crate::Renderer>( iced_core::clipboard::start_dnd::<crate::Theme, crate::Renderer>(
clipboard, clipboard,
false, false,
Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())), Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())),
None, None,
Box::new(SimpleDragData::new(mime, data)), Box::new(SimpleDragData::new(tab_drag.mime.clone(), vec![1])),
DndAction::Move, DndAction::Move,
); );
log::trace!( log::trace!(
target: TAB_REORDER_LOG_TARGET, target: TAB_REORDER_LOG_TARGET,
"tab drag started entity={:?} mime={} bytes={}", "tab drag started entity={:?} mime={} bytes={}",
entity, entity,
mime_label, tab_drag.mime,
data_len data_len
); );
state.dragging_tab = Some(entity); state.dragging_tab = Some(entity);
state.tab_drag_candidate = None; state.tab_drag_candidate = None;
state.pressed_item = None; state.pressed_item = None;
@ -815,6 +804,7 @@ where
tab_drag_candidate: None, tab_drag_candidate: None,
dragging_tab: None, dragging_tab: None,
drop_hint: None, drop_hint: None,
offer_mimes: Vec::new(),
}) })
} }
@ -966,26 +956,29 @@ where
"offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}"
); );
let on_dnd_enter = let on_dnd_enter = self
self.on_dnd_enter .on_dnd_enter
.as_ref() .as_ref()
.zip(entity) .zip(entity)
.map(|(on_enter, entity)| { .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes));
move |_, _, mime_types| on_enter(entity, mime_types) let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime)
}); && mime_types.is_empty()
{
vec![mime.clone()]
} else {
mime_types.clone()
};
state.offer_mimes = mimes.clone();
_ = state.dnd_state.on_enter::<Message>( _ = state
*x, .dnd_state
*y, .on_enter::<Message>(*x, *y, mimes, on_dnd_enter, entity);
mime_types.clone(),
on_dnd_enter,
entity,
);
} }
DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {}
DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination)
if Some(my_id) == *id => if Some(my_id) == *id =>
{ {
state.dragging_tab = None;
state.drop_hint = None; state.drop_hint = None;
self.emit_drop_hint(shell, state.drop_hint); self.emit_drop_hint(shell, state.drop_hint);
if let Some(Some(entity)) = entity { if let Some(Some(entity)) = entity {
@ -999,7 +992,6 @@ where
); );
_ = state.dnd_state.on_leave::<Message>(None); _ = state.dnd_state.on_leave::<Message>(None);
} }
DndEvent::Offer(_, OfferEvent::Leave | OfferEvent::LeaveDestination) => {}
DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => {
log::trace!( log::trace!(
target: TAB_REORDER_LOG_TARGET, target: TAB_REORDER_LOG_TARGET,
@ -1034,7 +1026,7 @@ where
.as_ref() .as_ref()
.map(|dnd| dnd.selected_action); .map(|dnd| dnd.selected_action);
if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() { if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() {
shell.publish(on_dnd_enter(new_entity, Vec::new())); shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone()));
} }
if let Some(dnd) = state.dnd_state.drag_offer.as_mut() { if let Some(dnd) = state.dnd_state.drag_offer.as_mut() {
dnd.data = Some(new_entity); dnd.data = Some(new_entity);
@ -1097,7 +1089,11 @@ where
.drag_offer .drag_offer
.as_ref() .as_ref()
.is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); .is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
let pending_reorder = if allow_reorder && self.on_reorder.is_some() { let pending_reorder = if allow_reorder
&& self.on_reorder.is_some()
&& self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
&& state.dragging_tab.is_some()
{
drop_entity.and_then(|target| self.reorder_event_for_drop(state, target)) drop_entity.and_then(|target| self.reorder_event_for_drop(state, target))
} else { } else {
None None
@ -1122,6 +1118,8 @@ where
shell.publish(msg); shell.publish(msg);
} }
state.drop_hint = None; state.drop_hint = None;
state.dragging_tab = None;
self.emit_drop_hint(shell, state.drop_hint); self.emit_drop_hint(shell, state.drop_hint);
if let Some(event) = pending_reorder { if let Some(event) = pending_reorder {
if let Some(on_reorder) = self.on_reorder.as_ref() { if let Some(on_reorder) = self.on_reorder.as_ref() {
@ -1135,6 +1133,8 @@ where
"data received without entity id={my_id:?}" "data received without entity id={my_id:?}"
); );
state.drop_hint = None; state.drop_hint = None;
state.dragging_tab = None;
self.emit_drop_hint(shell, state.drop_hint); self.emit_drop_hint(shell, state.drop_hint);
if let Some(event) = pending_reorder { if let Some(event) = pending_reorder {
if let Some(on_reorder) = self.on_reorder.as_ref() { if let Some(on_reorder) = self.on_reorder.as_ref() {
@ -2118,6 +2118,36 @@ where
} }
} }
if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) {
for item in self.variant_bounds(local_state, layout.bounds()) {
if let ItemBounds::Button(_entity, rect) = item {
pushed = true;
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
my_id,
rect.x,
rect.y,
rect.width,
rect.height,
mime
);
dnd_rectangles.push(DndDestinationRectangle {
id: my_id,
rectangle: dnd::Rectangle {
x: f64::from(rect.x),
y: f64::from(rect.y),
width: f64::from(rect.width),
height: f64::from(rect.height),
},
mime_types: vec![Cow::Owned(mime.clone())],
actions: DndAction::Copy | DndAction::Move,
preferred: DndAction::Move,
});
}
}
}
if !pushed { if !pushed {
let bounds = layout.bounds(); let bounds = layout.bounds();
log::trace!( log::trace!(
@ -2165,15 +2195,15 @@ where
} }
struct TabDragSource<Message> { struct TabDragSource<Message> {
payload: Box<dyn Fn(Entity) -> Option<(String, Vec<u8>)>>, mime: String,
threshold: f32, threshold: f32,
_marker: PhantomData<Message>, _marker: PhantomData<Message>,
} }
impl<Message> TabDragSource<Message> { impl<Message> TabDragSource<Message> {
fn new(payload: impl Fn(Entity) -> Option<(String, Vec<u8>)> + 'static) -> Self { fn new(mime: String) -> Self {
Self { Self {
payload: Box::new(payload), mime,
threshold: 8.0, threshold: 8.0,
_marker: PhantomData, _marker: PhantomData,
} }
@ -2254,6 +2284,8 @@ pub struct LocalState {
wheel_timestamp: Option<Instant>, wheel_timestamp: Option<Instant>,
/// Dnd state /// Dnd state
pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>, pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>,
/// Dnd state
pub offer_mimes: Vec<String>,
/// Tracks multi-touch events /// Tracks multi-touch events
fingers_pressed: HashSet<Finger>, fingers_pressed: HashSet<Finger>,
/// The currently pressed item /// The currently pressed item
@ -2391,6 +2423,7 @@ mod tests {
tab_drag_candidate: None, tab_drag_candidate: None,
dragging_tab: Some(dragging), dragging_tab: Some(dragging),
drop_hint: None, drop_hint: None,
offer_mimes: Vec::new(),
}; };
state.buttons_visible = len; state.buttons_visible = len;
state.known_length = len; state.known_length = len;