Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Ashley Wulber
17a2f62437
Revert "segmented button: support tab drag + drop"
This reverts commit 7f321cb0a3.
2025-12-19 13:29:54 -05:00
Ashley Wulber
4bb0d69ce1
Revert "tests: fix env guard and pipe read for tab dnd"
This reverts commit ce0868582b.
2025-12-19 13:29:22 -05:00
7 changed files with 46 additions and 977 deletions

View file

@ -122,7 +122,6 @@ image = { version = "0.25.8", default-features = false, features = [
"png", "png",
] } ] }
libc = { version = "0.2.175", optional = true } libc = { version = "0.2.175", optional = true }
log = "0.4"
mime = { version = "0.3.17", optional = true } mime = { version = "0.3.17", optional = true }
palette = "0.7.6" palette = "0.7.6"
raw-window-handle = "0.6" raw-window-handle = "0.6"

View file

@ -881,9 +881,7 @@ mod tests {
impl EnvVarGuard { impl EnvVarGuard {
fn set(key: &'static str, value: &Path) -> Self { fn set(key: &'static str, value: &Path) -> Self {
let original = env::var(key).ok(); let original = env::var(key).ok();
// std::env::{set_var, remove_var} are unsafe on newer toolchains; std::env::set_var(key, value);
// we limit scope here to the test helper that toggles a single key.
unsafe { std::env::set_var(key, value) };
Self { key, original } Self { key, original }
} }
} }
@ -891,9 +889,9 @@ mod tests {
impl Drop for EnvVarGuard { impl Drop for EnvVarGuard {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(ref original) = self.original { if let Some(ref original) = self.original {
unsafe { std::env::set_var(self.key, original) }; std::env::set_var(self.key, original);
} else { } else {
unsafe { std::env::remove_var(self.key) }; std::env::remove_var(self.key);
} }
} }
} }
@ -1110,8 +1108,7 @@ Icon=vmware-workstation\n\
let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default());
assert!(resolved.icon().is_some()); assert!(resolved.icon().is_some());
assert!(resolved.exec().is_some()); assert!(resolved.exec().is_some());
let expected = format!("crx_{}", id); assert_eq!(resolved.startup_wm_class(), Some(&format!("crx_{}", id)));
assert_eq!(resolved.startup_wm_class(), Some(expected.as_str()));
} }
#[test] #[test]

View file

@ -9,28 +9,18 @@ use std::process::{Command, Stdio, exit};
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
async fn read_from_pipe(read: OwnedFd) -> Option<u32> {
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
{ async fn read_from_pipe(read: OwnedFd) -> Option<u32> {
let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap(); let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap();
return read.read_u32().await.ok(); read.read_u32().await.ok()
} }
#[cfg(all(feature = "smol", not(feature = "tokio")))] #[cfg(all(feature = "smol", not(feature = "tokio")))]
{ async fn read_from_pipe(read: OwnedFd) -> Option<u32> {
let mut read = smol::Async::new(std::fs::File::from(read)).unwrap(); let mut read = smol::Async::new(std::fs::File::from(read)).unwrap();
let mut bytes = [0; 4]; let mut bytes = [0; 4];
read.read_exact(&mut bytes).await.ok()?; read.read_exact(&mut bytes).await.ok()?;
return Some(u32::from_be_bytes(bytes)); Some(u32::from_be_bytes(bytes))
}
#[cfg(not(any(feature = "tokio", feature = "smol")))]
{
use rustix::fd::AsFd;
let mut bytes = [0u8; 4];
rustix::io::read(&read, &mut bytes).ok()?;
return Some(u32::from_be_bytes(bytes));
}
} }
/// Performs a double fork with setsid to spawn and detach a command. /// Performs a double fork with setsid to spawn and detach a command.

View file

@ -39,7 +39,6 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>(
} }
static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DragId(pub u128); pub struct DragId(pub u128);
@ -76,12 +75,6 @@ pub struct DndDestination<'a, Message> {
} }
impl<'a, Message: 'static> DndDestination<'a, Message> { impl<'a, Message: 'static> DndDestination<'a, Message> {
fn mime_matches(&self, offered: &[String]) -> bool {
self.mime_types.is_empty()
|| offered
.iter()
.any(|mime| self.mime_types.iter().any(|allowed| allowed == mime))
}
pub fn new(child: impl Into<Element<'a, Message>>, mimes: Vec<Cow<'static, str>>) -> Self { pub fn new(child: impl Into<Element<'a, Message>>, mimes: Vec<Cow<'static, str>>) -> Self {
Self { Self {
id: Id::unique(), id: Id::unique(),
@ -331,12 +324,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
let my_id = self.get_drag_id(); let my_id = self.get_drag_id();
log::trace!(
target: DND_DEST_LOG_TARGET,
"dnd_destination id={:?}: event {:?}",
self.drag_id.unwrap_or_default(),
event
);
match event { match event {
Event::Dnd(DndEvent::Offer( Event::Dnd(DndEvent::Offer(
id, id,
@ -344,18 +331,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
x, y, mime_types, .. x, y, mime_types, ..
}, },
)) if id == Some(my_id) => { )) if id == Some(my_id) => {
if !self.mime_matches(&mime_types) {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})",
self.mime_types
);
return event::Status::Ignored;
}
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}"
);
if let Some(msg) = state.on_enter( if let Some(msg) = state.on_enter(
x, x,
y, y,
@ -385,11 +360,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
return event::Status::Captured; return event::Status::Captured;
} }
Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer leave id={:?}",
my_id
);
if let Some(msg) = if let Some(msg) =
state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref))
{ {
@ -413,10 +383,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
return event::Status::Ignored; return event::Status::Ignored;
} }
Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => { Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer motion id={my_id:?} coords=({x},{y})"
);
if let Some(msg) = state.on_motion( if let Some(msg) = state.on_motion(
x, x,
y, y,
@ -447,11 +413,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
return event::Status::Captured; return event::Status::Captured;
} }
Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer leave-destination id={:?}",
my_id
);
if let Some(msg) = if let Some(msg) =
state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref))
{ {
@ -460,10 +421,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
return event::Status::Ignored; return event::Status::Ignored;
} }
Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => { Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer drop id={my_id:?}"
);
if let Some(msg) = if let Some(msg) =
state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref)) state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref))
{ {
@ -474,10 +431,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action)))
if id == Some(my_id) => if id == Some(my_id) =>
{ {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer selected-action id={my_id:?} action={action:?}"
);
if let Some(msg) = state.on_action_selected( if let Some(msg) = state.on_action_selected(
action, action,
self.on_action_selected self.on_action_selected
@ -491,11 +444,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type }))
if id == Some(my_id) => if id == Some(my_id) =>
{ {
log::trace!(
target: DND_DEST_LOG_TARGET,
"offer data id={my_id:?} mime={mime_type:?} bytes={}",
data.len()
);
if let (Some(msg), ret) = state.on_data_received( if let (Some(msg), ret) = state.on_data_received(
mime_type, mime_type,
data, data,
@ -573,16 +521,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
) { ) {
let bounds = layout.bounds(); let bounds = layout.bounds();
let my_id = self.get_drag_id(); let my_id = self.get_drag_id();
log::trace!(
target: DND_DEST_LOG_TARGET,
"register destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
my_id,
bounds.x,
bounds.y,
bounds.width,
bounds.height,
self.mime_types
);
let my_dest = DndDestinationRectangle { let my_dest = DndDestinationRectangle {
id: my_id, id: my_id,
rectangle: dnd::Rectangle { rectangle: dnd::Rectangle {
@ -597,15 +535,13 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
}; };
dnd_rectangles.push(my_dest); dnd_rectangles.push(my_dest);
if let Some(child_layout) = layout.children().next() {
self.container.as_widget().drag_destinations( self.container.as_widget().drag_destinations(
&state.children[0], &state.children[0],
child_layout.with_virtual_offset(layout.virtual_offset()), layout,
renderer, renderer,
dnd_rectangles, dnd_rectangles,
); );
} }
}
fn id(&self) -> Option<Id> { fn id(&self) -> Option<Id> {
Some(self.id.clone()) Some(self.id.clone())
@ -760,71 +696,3 @@ impl<'a, Message: 'static> From<DndDestination<'a, Message>> for Element<'a, Mes
Element::new(wrapper) Element::new(wrapper)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Copy, Debug, PartialEq)]
enum TestMsg {
Data,
Finished,
}
#[test]
fn data_before_drop_invokes_data_handler_only() {
let mut state: State<()> = State::new();
assert!(state.drag_offer.is_none());
state.on_enter::<TestMsg>(
4.0,
2.0,
vec!["text/plain".into()],
Option::<fn(_, _, _) -> TestMsg>::None,
(),
);
let (message, status) = state.on_data_received(
"text/plain".into(),
vec![1],
Some(|mime, data| {
assert_eq!(mime, "text/plain");
assert_eq!(data, vec![1]);
TestMsg::Data
}),
Option::<fn(_, _, _, _, _) -> TestMsg>::None,
);
assert!(matches!(message, Some(TestMsg::Data)));
assert_eq!(status, event::Status::Captured);
assert!(state.drag_offer.is_some());
}
#[test]
fn finish_only_emits_after_drop() {
let mut state: State<()> = State::new();
state.on_enter::<TestMsg>(
5.0,
-1.0,
vec![],
Option::<fn(_, _, _) -> TestMsg>::None,
(),
);
state.on_action_selected::<TestMsg>(DndAction::Move, Option::<fn(_) -> TestMsg>::None);
state.on_drop::<TestMsg>(Option::<fn(_, _) -> TestMsg>::None);
let (message, status) = state.on_data_received(
"application/x-test".into(),
vec![7],
Option::<fn(_, _) -> TestMsg>::None,
Some(|mime, data, action, x, y| {
assert_eq!(mime, "application/x-test");
assert_eq!(data, vec![7]);
assert_eq!(action, DndAction::Move);
assert_eq!(x, 5.0);
assert_eq!(y, -1.0);
TestMsg::Finished
}),
);
assert!(matches!(message, Some(TestMsg::Finished)));
assert_eq!(status, event::Status::Captured);
assert!(state.drag_offer.is_none());
}
}

View file

@ -88,19 +88,6 @@ pub use self::style::{Appearance, ItemAppearance, ItemStatusAppearance, StyleShe
pub use self::vertical::{VerticalSegmentedButton, vertical}; pub use self::vertical::{VerticalSegmentedButton, vertical};
pub use self::widget::{Id, SegmentedButton, SegmentedVariant, focus}; pub use self::widget::{Id, SegmentedButton, SegmentedVariant, focus};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InsertPosition {
Before,
After,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReorderEvent {
pub dragged: Entity,
pub target: Entity,
pub position: InsertPosition,
}
/// Associates extra data with an external secondary map. /// Associates extra data with an external secondary map.
/// ///
/// The secondary map internally uses a `Vec`, so should only be used for data that /// The secondary map internally uses a `Vec`, so should only be used for data that

View file

@ -11,7 +11,6 @@ mod selection;
pub use self::selection::{MultiSelect, Selectable, SingleSelect}; pub use self::selection::{MultiSelect, Selectable, SingleSelect};
use crate::widget::Icon; use crate::widget::Icon;
use crate::widget::segmented_button::InsertPosition;
use slotmap::{SecondaryMap, SlotMap}; use slotmap::{SecondaryMap, SlotMap};
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
use std::borrow::Cow; use std::borrow::Cow;
@ -411,36 +410,6 @@ where
true true
} }
/// Reorder `dragged` relative to `target` based on the provided position.
///
/// Returns `true` if the model changed, or `false` if the move was invalid.
pub fn reorder(&mut self, dragged: Entity, target: Entity, position: InsertPosition) -> bool {
if !self.contains_item(dragged) || !self.contains_item(target) || dragged == target {
return false;
}
let len = self.iter().count();
let target_pos = self.position(target).map(|pos| pos as usize).unwrap_or(len);
let from_pos = self
.position(dragged)
.map(|pos| pos as usize)
.unwrap_or(target_pos);
let mut insert_pos = match position {
InsertPosition::Before => target_pos,
InsertPosition::After => target_pos.saturating_add(1),
};
if from_pos < insert_pos {
insert_pos = insert_pos.saturating_sub(1);
}
if len > 0 {
insert_pos = insert_pos.min(len.saturating_sub(1));
}
self.position_set(dragged, insert_pos as u16);
self.activate(dragged);
true
}
/// Removes an item from the model. /// Removes an item from the model.
/// ///
/// The generation of the slot for the ID will be incremented, so this ID will no /// The generation of the slot for the ID will be incremented, so this ID will no
@ -500,43 +469,3 @@ where
self.text.remove(id) self.text.remove(id)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn sample_model() -> (Model<SingleSelect>, Vec<Entity>) {
let mut ids = Vec::new();
let model = Model::builder()
.insert(|b| b.text("Tab1").with_id(|id| ids.push(id)))
.insert(|b| b.text("Tab2").with_id(|id| ids.push(id)))
.insert(|b| b.text("Tab3").with_id(|id| ids.push(id)))
.insert(|b| b.text("Tab4").with_id(|id| ids.push(id)))
.build();
(model, ids)
}
fn order_of(model: &Model<SingleSelect>) -> Vec<Entity> {
model.iter().collect()
}
#[test]
fn reorder_inserts_before_target() {
let (mut model, ids) = sample_model();
assert!(model.reorder(ids[3], ids[1], InsertPosition::Before));
assert_eq!(order_of(&model), vec![ids[0], ids[3], ids[1], ids[2]]);
}
#[test]
fn reorder_inserts_after_target() {
let (mut model, ids) = sample_model();
assert!(model.reorder(ids[0], ids[2], InsertPosition::After));
assert_eq!(order_of(&model), vec![ids[1], ids[2], ids[0], ids[3]]);
}
#[test]
fn reorder_rejects_invalid_entities() {
let (mut model, ids) = sample_model();
assert!(!model.reorder(ids[0], ids[0], InsertPosition::After));
}
}

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use super::model::{Entity, Model, Selectable}; use super::model::{Entity, Model, Selectable};
use super::{InsertPosition, ReorderEvent};
use crate::iced_core::id::Internal; use crate::iced_core::id::Internal;
use crate::theme::{SegmentedButton as Style, THEME}; use crate::theme::{SegmentedButton as Style, THEME};
use crate::widget::dnd_destination::DragId; use crate::widget::dnd_destination::DragId;
@ -13,9 +12,7 @@ use crate::widget::menu::{
use crate::widget::{Icon, icon}; use crate::widget::{Icon, icon};
use crate::{Element, Renderer}; use crate::{Element, Renderer};
use derive_setters::Setters; use derive_setters::Setters;
use iced::clipboard::dnd::{ use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent};
self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent, SourceEvent,
};
use iced::clipboard::mime::AllowedMimeTypes; use iced::clipboard::mime::AllowedMimeTypes;
use iced::touch::Finger; use iced::touch::Finger;
use iced::{ use iced::{
@ -44,8 +41,6 @@ thread_local! {
static LAST_FOCUS_UPDATE: LazyCell<Cell<Instant>> = LazyCell::new(|| Cell::new(Instant::now())); static LAST_FOCUS_UPDATE: LazyCell<Cell<Instant>> = LazyCell::new(|| Cell::new(Instant::now()));
} }
const TAB_REORDER_LOG_TARGET: &str = "libcosmic::widget::tab_reorder";
/// A command that focuses a segmented item stored in a widget. /// A command that focuses a segmented item stored in a widget.
pub fn focus<Message: 'static>(id: Id) -> Task<Message> { pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0)))) task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0))))
@ -56,27 +51,6 @@ pub enum ItemBounds {
Divider(Rectangle, bool), Divider(Rectangle, bool),
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DropSide {
Before,
After,
}
impl From<DropSide> for InsertPosition {
fn from(side: DropSide) -> Self {
match side {
DropSide::Before => InsertPosition::Before,
DropSide::After => InsertPosition::After,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct DropHint {
entity: Entity,
side: DropSide,
}
/// Isolates variant-specific behaviors from [`SegmentedButton`]. /// Isolates variant-specific behaviors from [`SegmentedButton`].
pub trait SegmentedVariant { pub trait SegmentedVariant {
const VERTICAL: bool; const VERTICAL: bool;
@ -183,12 +157,6 @@ where
#[setters(strip_option)] #[setters(strip_option)]
pub(super) drag_id: Option<DragId>, pub(super) drag_id: Option<DragId>,
#[setters(skip)] #[setters(skip)]
pub(super) tab_drag: Option<TabDragSource<Message>>,
#[setters(skip)]
pub(super) on_drop_hint: Option<Box<dyn Fn(Option<(Entity, bool)>) -> Message + 'static>>,
#[setters(skip)]
pub(super) on_reorder: Option<Box<dyn Fn(ReorderEvent) -> Message + 'static>>,
#[setters(skip)]
/// Defines the implementation of this struct /// Defines the implementation of this struct
variant: PhantomData<Variant>, variant: PhantomData<Variant>,
} }
@ -236,9 +204,6 @@ where
mimes: Vec::new(), mimes: Vec::new(),
variant: PhantomData, variant: PhantomData,
drag_id: None, drag_id: None,
tab_drag: None,
on_drop_hint: None,
on_reorder: None,
} }
} }
@ -296,77 +261,6 @@ where
self self
} }
/// Enable drag-and-drop support for tabs using the provided payload builder.
pub fn enable_tab_drag(
mut self,
payload: impl Fn(Entity) -> Option<(String, Vec<u8>)> + 'static,
) -> Self {
self.tab_drag = Some(TabDragSource::new(payload));
self
}
/// Receive drop hint updates during drag-and-drop.
pub fn on_drop_hint(
mut self,
callback: impl Fn(Option<(Entity, bool)>) -> Message + 'static,
) -> Self {
self.on_drop_hint = Some(Box::new(callback));
self
}
/// Emit a message when a tab drag is dropped inside this widget.
pub fn on_reorder(mut self, callback: impl Fn(ReorderEvent) -> Message + 'static) -> Self {
self.on_reorder = Some(Box::new(callback));
self
}
/// Set the pointer distance threshold before a drag is started.
pub fn tab_drag_threshold(mut self, threshold: f32) -> Self {
if let Some(tab_drag) = self.tab_drag.as_mut() {
tab_drag.threshold = threshold.max(1.0);
}
self
}
fn reorder_event_for_drop(&self, state: &LocalState, target: Entity) -> Option<ReorderEvent> {
let dragged = state.dragging_tab?;
if dragged == target
|| !self.model.contains_item(dragged)
|| !self.model.contains_item(target)
{
return None;
}
let position = state
.drop_hint
.filter(|hint| hint.entity == target)
.map(|hint| InsertPosition::from(hint.side))
.unwrap_or_else(|| self.default_insert_position(dragged, target));
Some(ReorderEvent {
dragged,
target,
position,
})
}
fn default_insert_position(&self, dragged: Entity, target: Entity) -> InsertPosition {
let len = self.model.len();
let target_pos = self
.model
.position(target)
.map(|pos| pos as usize)
.unwrap_or(len);
let from_pos = self
.model
.position(dragged)
.map(|pos| pos as usize)
.unwrap_or(target_pos);
if from_pos < target_pos {
InsertPosition::After
} else {
InsertPosition::Before
}
}
/// Check if an item is enabled. /// Check if an item is enabled.
fn is_enabled(&self, key: Entity) -> bool { fn is_enabled(&self, key: Entity) -> bool {
self.model.items.get(key).is_some_and(|item| item.enabled) self.model.items.get(key).is_some_and(|item| item.enabled)
@ -651,101 +545,6 @@ where
state.pressed_item == Some(Item::Tab(key)) state.pressed_item == Some(Item::Tab(key))
} }
fn emit_drop_hint(&self, shell: &mut Shell<'_, Message>, hint: Option<DropHint>) {
if let Some(on_hint) = self.on_drop_hint.as_ref() {
let mapped = hint.map(|hint| (hint.entity, matches!(hint.side, DropSide::After)));
shell.publish(on_hint(mapped));
}
}
fn drop_hint_for_position(
&self,
state: &LocalState,
bounds: Rectangle,
cursor: Point,
) -> Option<DropHint> {
let dragging = state.dragging_tab?;
self.variant_bounds(state, bounds)
.filter_map(|item| match item {
ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)),
_ => None,
})
.find_map(|(entity, rect)| {
let before = if Self::VERTICAL {
cursor.y < rect.center_y()
} else {
cursor.x < rect.center_x()
};
Some(DropHint {
entity,
side: if before {
DropSide::Before
} else {
DropSide::After
},
})
})
}
fn start_tab_drag(
&self,
state: &mut LocalState,
entity: Entity,
bounds: Rectangle,
cursor: Point,
clipboard: &mut dyn Clipboard,
) -> bool {
let Some(tab_drag) = self.tab_drag.as_ref() else {
return false;
};
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"start_tab_drag requested entity={:?} cursor=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
entity,
cursor.x,
cursor.y,
bounds.x,
bounds.y,
bounds.width,
bounds.height,
tab_drag.threshold
);
let Some((mime, data)) = (tab_drag.payload)(entity) else {
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>(
clipboard,
false,
Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())),
None,
Box::new(SimpleDragData::new(mime, data)),
DndAction::Move,
);
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"tab drag started entity={:?} mime={} bytes={}",
entity,
mime_label,
data_len
);
state.dragging_tab = Some(entity);
state.tab_drag_candidate = None;
state.pressed_item = None;
true
}
/// Returns the drag id of the destination. /// Returns the drag id of the destination.
/// ///
/// # Panics /// # Panics
@ -812,9 +611,6 @@ where
dnd_state: Default::default(), dnd_state: Default::default(),
fingers_pressed: Default::default(), fingers_pressed: Default::default(),
pressed_item: None, pressed_item: None,
tab_drag_candidate: None,
dragging_tab: None,
drop_hint: None,
}) })
} }
@ -905,7 +701,7 @@ where
layout: Layout<'_>, layout: Layout<'_>,
cursor_position: mouse::Cursor, cursor_position: mouse::Cursor,
_renderer: &Renderer, _renderer: &Renderer,
clipboard: &mut dyn Clipboard, _clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &iced::Rectangle, _viewport: &iced::Rectangle,
) -> event::Status { ) -> event::Status {
@ -921,26 +717,7 @@ where
.drag_offer .drag_offer
.as_ref() .as_ref()
.map(|dnd_state| dnd_state.data); .map(|dnd_state| dnd_state.data);
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"segmented button {:?} received DnD event: {:?} entity={entity:?}",
my_id,
e
);
match e { match e {
DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => {
if state.dragging_tab.take().is_some() {
state.tab_drag_candidate = None;
state.drop_hint = None;
self.emit_drop_hint(shell, state.drop_hint);
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"tab drag source finished id={:?}",
my_id
);
return event::Status::Captured;
}
}
DndEvent::Offer( DndEvent::Offer(
id, id,
OfferEvent::Enter { OfferEvent::Enter {
@ -955,16 +732,6 @@ where
}) })
.find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32))) .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
.map(|(key, _)| key); .map(|(key, _)| key);
state.drop_hint = self.drop_hint_for_position(
state,
bounds,
Point::new(*x as f32, *y as f32),
);
self.emit_drop_hint(shell, state.drop_hint);
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}"
);
let on_dnd_enter = let on_dnd_enter =
self.on_dnd_enter self.on_dnd_enter
@ -983,28 +750,15 @@ where
); );
} }
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 =>
{
state.drop_hint = None;
self.emit_drop_hint(shell, state.drop_hint);
if let Some(Some(entity)) = entity { if let Some(Some(entity)) = entity {
if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
shell.publish(on_dnd_leave(entity)); shell.publish(on_dnd_leave(entity));
} }
} }
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"offer leave id={my_id:?} entity={entity:?}"
);
_ = 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!(
target: TAB_REORDER_LOG_TARGET,
"offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}"
);
let new = self let new = self
.variant_bounds(state, bounds) .variant_bounds(state, bounds)
.filter_map(|item| match item { .filter_map(|item| match item {
@ -1021,12 +775,6 @@ where
None::<fn(_, _, _) -> Message>, None::<fn(_, _, _) -> Message>,
Some(new_entity), Some(new_entity),
); );
state.drop_hint = self.drop_hint_for_position(
state,
bounds,
Point::new(*x as f32, *y as f32),
);
self.emit_drop_hint(shell, state.drop_hint);
if Some(Some(new_entity)) != entity { if Some(Some(new_entity)) != entity {
let prev_action = state let prev_action = state
.dnd_state .dnd_state
@ -1044,12 +792,6 @@ where
} }
} }
} else if entity.is_some() { } else if entity.is_some() {
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"offer motion leaving id={my_id:?}"
);
state.drop_hint = None;
self.emit_drop_hint(shell, state.drop_hint);
state.dnd_state.on_motion::<Message>( state.dnd_state.on_motion::<Message>(
*x, *x,
*y, *y,
@ -1065,81 +807,32 @@ where
} }
} }
DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => { DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => {
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"offer drop id={my_id:?} entity={entity:?}"
);
_ = state _ = state
.dnd_state .dnd_state
.on_drop::<Message>(None::<fn(_, _) -> Message>); .on_drop::<Message>(None::<fn(_, _) -> Message>);
} }
DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => { DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => {
if state.dnd_state.drag_offer.is_some() { if state.dnd_state.drag_offer.is_some() {
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"offer selected action id={my_id:?} action={action:?} entity={entity:?}"
);
_ = state _ = state
.dnd_state .dnd_state
.on_action_selected::<Message>(*action, None::<fn(_) -> Message>); .on_action_selected::<Message>(*action, None::<fn(_) -> Message>);
} }
} }
DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => { DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => {
log::trace!( if let Some(Some(entity)) = entity {
target: TAB_REORDER_LOG_TARGET,
"offer data id={my_id:?} entity={entity:?} mime={mime_type:?}"
);
let drop_entity = entity
.flatten()
.or_else(|| state.drop_hint.map(|hint| hint.entity));
let allow_reorder = state
.dnd_state
.drag_offer
.as_ref()
.is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
let pending_reorder = if allow_reorder && self.on_reorder.is_some() {
drop_entity.and_then(|target| self.reorder_event_for_drop(state, target))
} else {
None
};
if let Some(entity) = drop_entity {
let on_drop = self.on_dnd_drop.as_ref(); let on_drop = self.on_dnd_drop.as_ref();
let on_drop = on_drop.map(|on_drop| { let on_drop = on_drop.map(|on_drop| {
|mime, data, action, _, _| on_drop(entity, data, mime, action) |mime, data, action, _, _| on_drop(entity, data, mime, action)
}); });
let (maybe_msg, ret) = state.dnd_state.on_data_received( if let (Some(msg), ret) = state.dnd_state.on_data_received(
mem::take(mime_type), mem::take(mime_type),
mem::take(data), mem::take(data),
None::<fn(_, _) -> Message>, None::<fn(_, _) -> Message>,
on_drop, on_drop,
); ) {
if let Some(msg) = maybe_msg {
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"publishing drop message entity={entity:?}"
);
shell.publish(msg); shell.publish(msg);
}
state.drop_hint = None;
self.emit_drop_hint(shell, state.drop_hint);
if let Some(event) = pending_reorder {
if let Some(on_reorder) = self.on_reorder.as_ref() {
shell.publish(on_reorder(event));
}
}
return ret; return ret;
} else {
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"data received without entity id={my_id:?}"
);
state.drop_hint = None;
self.emit_drop_hint(shell, state.drop_hint);
if let Some(event) = pending_reorder {
if let Some(on_reorder) = self.on_reorder.as_ref() {
shell.publish(on_reorder(event));
}
} }
} }
} }
@ -1204,16 +897,12 @@ where
// Record that the mouse is hovering over this button. // Record that the mouse is hovering over this button.
state.hovered = Item::Tab(key); state.hovered = Item::Tab(key);
let close_button_bounds =
close_bounds(bounds, f32::from(self.close_icon.size));
let over_close_button = self.model.items[key].closable
&& cursor_position.is_over(close_button_bounds);
// If marked as closable, show a close icon. // If marked as closable, show a close icon.
if self.model.items[key].closable { if self.model.items[key].closable {
// Emit close message if the close button is pressed. // Emit close message if the close button is pressed.
if let Some(on_close) = self.on_close.as_ref() { if let Some(on_close) = self.on_close.as_ref() {
if over_close_button if cursor_position
.is_over(close_bounds(bounds, f32::from(self.close_icon.size)))
&& (left_button_released(&event) && (left_button_released(&event)
|| (touch_lifted(&event) && fingers_pressed == 1)) || (touch_lifted(&event) && fingers_pressed == 1))
{ {
@ -1238,36 +927,6 @@ where
} }
} }
if self.tab_drag.is_some()
&& matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
)
&& !over_close_button
{
if let Some(position) = cursor_position.position() {
state.tab_drag_candidate = Some(TabDragCandidate {
entity: key,
bounds,
origin: position,
});
if let Some(tab_drag) = self.tab_drag.as_ref() {
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
key,
position.x,
position.y,
bounds.x,
bounds.y,
bounds.width,
bounds.height,
tab_drag.threshold
);
}
}
}
if is_lifted(&event) { if is_lifted(&event) {
state.unfocus(); state.unfocus();
} }
@ -1387,42 +1046,6 @@ where
state.pressed_item = None; state.pressed_item = None;
} }
if let (Some(tab_drag), Some(candidate)) =
(self.tab_drag.as_ref(), state.tab_drag_candidate)
{
if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event {
if let Some(position) = cursor_position.position() {
if position.distance(candidate.origin) >= tab_drag.threshold {
if let Some(candidate) = state.tab_drag_candidate.take() {
log::trace!(
target: TAB_REORDER_LOG_TARGET,
"tab drag threshold met entity={:?} distance={:.2} threshold={}",
candidate.entity,
position.distance(candidate.origin),
tab_drag.threshold
);
if self.start_tab_drag(
state,
candidate.entity,
candidate.bounds,
position,
clipboard,
) {
return event::Status::Captured;
}
}
}
}
}
}
if matches!(
event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
) {
state.tab_drag_candidate = None;
}
if state.is_focused() { if state.is_focused() {
if let Event::Keyboard(keyboard::Event::KeyPressed { if let Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Tab), key: keyboard::Key::Named(keyboard::key::Named::Tab),
@ -1497,7 +1120,6 @@ where
) { ) {
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
operation.focusable(state, Some(&self.id.0)); operation.focusable(state, Some(&self.id.0));
operation.custom(state, Some(&self.id.0));
if let Item::Set = state.focused_item { if let Item::Set = state.focused_item {
if self.prev_tab_sensitive(state) { if self.prev_tab_sensitive(state) {
@ -1558,12 +1180,6 @@ where
let appearance = Self::variant_appearance(theme, &self.style); let appearance = Self::variant_appearance(theme, &self.style);
let bounds: Rectangle = layout.bounds(); let bounds: Rectangle = layout.bounds();
let button_amount = self.model.items.len(); let button_amount = self.model.items.len();
let show_drop_hint = state.dragging_tab.is_some();
let drop_hint = if show_drop_hint {
state.drop_hint
} else {
None
};
// Draw the background, if a background was defined. // Draw the background, if a background was defined.
if let Some(background) = appearance.background { if let Some(background) = appearance.background {
@ -1689,8 +1305,6 @@ where
// Draw each of the items in the widget. // Draw each of the items in the widget.
let mut nth = 0; let mut nth = 0;
let drop_hint_marker = drop_hint;
let show_drop_hint_marker = show_drop_hint;
self.variant_bounds(state, bounds).for_each(move |item| { self.variant_bounds(state, bounds).for_each(move |item| {
let (key, mut bounds) = match item { let (key, mut bounds) = match item {
// Draw a button // Draw a button
@ -1718,27 +1332,8 @@ where
} }
}; };
let original_bounds = bounds;
let center_y = bounds.center_y(); let center_y = bounds.center_y();
if show_drop_hint_marker {
if matches!(
drop_hint_marker,
Some(DropHint {
entity,
side: DropSide::Before
}) if entity == key
) {
draw_drop_indicator(
renderer,
original_bounds,
DropSide::Before,
Self::VERTICAL,
appearance.active.text_color,
);
}
}
let menu_open = || { let menu_open = || {
state.show_context == Some(key) state.show_context == Some(key)
&& !tree.children.is_empty() && !tree.children.is_empty()
@ -1803,6 +1398,7 @@ where
); );
} }
let original_bounds = bounds;
bounds.x += f32::from(self.button_padding[0]); bounds.x += f32::from(self.button_padding[0]);
bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
let mut indent_padding = 0.0; let mut indent_padding = 0.0;
@ -2000,24 +1596,6 @@ where
); );
} }
if show_drop_hint_marker {
if matches!(
drop_hint_marker,
Some(DropHint {
entity,
side: DropSide::After
}) if entity == key
) {
draw_drop_indicator(
renderer,
original_bounds,
DropSide::After,
Self::VERTICAL,
appearance.active.text_color,
);
}
}
nth += 1; nth += 1;
}); });
} }
@ -2081,56 +1659,15 @@ where
fn drag_destinations( fn drag_destinations(
&self, &self,
tree: &Tree, _state: &Tree,
layout: Layout<'_>, layout: Layout<'_>,
_renderer: &Renderer, _renderer: &Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) { ) {
let local_state = tree.state.downcast_ref::<LocalState>();
let my_id = self.get_drag_id();
let mut pushed = false;
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,
self.mimes
);
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: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
actions: DndAction::Copy | DndAction::Move,
preferred: DndAction::Move,
});
}
}
if !pushed {
let bounds = layout.bounds(); let bounds = layout.bounds();
log::trace!(
target: TAB_REORDER_LOG_TARGET, let my_id = self.get_drag_id();
"register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", let dnd_rect = DndDestinationRectangle {
my_id,
bounds.x,
bounds.y,
bounds.width,
bounds.height,
self.mimes
);
dnd_rectangles.push(DndDestinationRectangle {
id: my_id, id: my_id,
rectangle: dnd::Rectangle { rectangle: dnd::Rectangle {
x: f64::from(bounds.x), x: f64::from(bounds.x),
@ -2141,8 +1678,8 @@ where
mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
actions: DndAction::Copy | DndAction::Move, actions: DndAction::Copy | DndAction::Move,
preferred: DndAction::Move, preferred: DndAction::Move,
}); };
} dnd_rectangles.push(dnd_rect);
} }
} }
@ -2164,54 +1701,6 @@ where
} }
} }
struct TabDragSource<Message> {
payload: Box<dyn Fn(Entity) -> Option<(String, Vec<u8>)>>,
threshold: f32,
_marker: PhantomData<Message>,
}
impl<Message> TabDragSource<Message> {
fn new(payload: impl Fn(Entity) -> Option<(String, Vec<u8>)> + 'static) -> Self {
Self {
payload: Box::new(payload),
threshold: 8.0,
_marker: PhantomData,
}
}
}
struct SimpleDragData {
mime: String,
bytes: Vec<u8>,
}
impl SimpleDragData {
fn new(mime: String, bytes: Vec<u8>) -> Self {
Self { mime, bytes }
}
}
impl iced::clipboard::mime::AsMimeTypes for SimpleDragData {
fn available(&self) -> Cow<'static, [String]> {
Cow::Owned(vec![self.mime.clone()])
}
fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
if mime_type == self.mime {
Some(Cow::Owned(self.bytes.clone()))
} else {
None
}
}
}
#[derive(Clone, Copy)]
struct TabDragCandidate {
entity: Entity,
bounds: Rectangle,
origin: Point,
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct Focus { struct Focus {
updated_at: Instant, updated_at: Instant,
@ -2258,12 +1747,6 @@ pub struct LocalState {
fingers_pressed: HashSet<Finger>, fingers_pressed: HashSet<Finger>,
/// The currently pressed item /// The currently pressed item
pressed_item: Option<Item>, pressed_item: Option<Item>,
/// Pending tab drag candidate data
tab_drag_candidate: Option<TabDragCandidate>,
/// Currently dragging tab entity
dragging_tab: Option<Entity>,
/// Current drop hint for drag-and-drop indicator
drop_hint: Option<DropHint>,
} }
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
@ -2288,143 +1771,6 @@ impl LocalState {
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::segmented_button::{self, Appearance as SegAppearance};
use iced::Size;
use slotmap::SecondaryMap;
use std::collections::HashSet;
#[derive(Clone, Debug)]
enum TestMessage {}
struct TestVariant;
impl<SelectionMode, Message> SegmentedVariant
for SegmentedButton<'_, TestVariant, SelectionMode, Message>
where
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
const VERTICAL: bool = false;
fn variant_appearance(
_theme: &crate::Theme,
_style: &crate::theme::SegmentedButton,
) -> SegAppearance {
SegAppearance::default()
}
fn variant_bounds<'b>(
&'b self,
_state: &'b LocalState,
bounds: Rectangle,
) -> Box<dyn Iterator<Item = ItemBounds> + 'b> {
let len = self.model.order.len();
if len == 0 {
return Box::new(std::iter::empty());
}
let width = bounds.width / len as f32;
Box::new(
self.model
.order
.iter()
.copied()
.enumerate()
.map(move |(idx, entity)| {
let rect = Rectangle {
x: bounds.x + (idx as f32) * width,
y: bounds.y,
width,
height: bounds.height,
};
ItemBounds::Button(entity, rect)
}),
)
}
fn variant_layout(
&self,
_state: &mut LocalState,
_renderer: &crate::Renderer,
_limits: &layout::Limits,
) -> Size {
Size::ZERO
}
}
fn sample_model() -> (
segmented_button::SingleSelectModel,
Vec<segmented_button::Entity>,
) {
let mut entities = Vec::new();
let model = segmented_button::Model::builder()
.insert(|b| b.text("One").with_id(|id| entities.push(id)))
.insert(|b| b.text("Two").with_id(|id| entities.push(id)))
.insert(|b| b.text("Three").with_id(|id| entities.push(id)))
.build();
(model, entities)
}
fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState {
let mut state = LocalState {
menu_state: MenuBarState::default(),
paragraphs: SecondaryMap::new(),
text_hashes: SecondaryMap::new(),
buttons_visible: 0,
buttons_offset: 0,
collapsed: false,
focused: None,
focused_item: Item::default(),
focused_visible: false,
hovered: Item::default(),
known_length: 0,
middle_clicked: None,
internal_layout: Vec::new(),
context_cursor: Point::ORIGIN,
show_context: None,
wheel_timestamp: None,
dnd_state: crate::widget::dnd_destination::State::<Option<Entity>>::new(),
fingers_pressed: HashSet::new(),
pressed_item: None,
tab_drag_candidate: None,
dragging_tab: Some(dragging),
drop_hint: None,
};
state.buttons_visible = len;
state.known_length = len;
state
}
#[test]
fn drop_hint_reports_before_and_after() {
let (model, ids) = sample_model();
let button =
SegmentedButton::<TestVariant, segmented_button::SingleSelect, TestMessage>::new(
&model,
);
let state = test_state(ids[0], model.order.len());
let bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 300.0,
height: 30.0,
};
let before = button
.drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0))
.expect("hint");
assert_eq!(before.entity, ids[0]);
assert!(matches!(before.side, DropSide::Before));
let after = button
.drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0))
.expect("hint");
assert_eq!(after.entity, ids[2]);
assert!(matches!(after.side, DropSide::After));
}
}
impl operation::Focusable for LocalState { impl operation::Focusable for LocalState {
fn is_focused(&self) -> bool { fn is_focused(&self) -> bool {
self.focused self.focused
@ -2537,53 +1883,6 @@ fn draw_icon<Message: 'static>(
); );
} }
fn draw_drop_indicator(
renderer: &mut Renderer,
bounds: Rectangle,
side: DropSide,
vertical: bool,
color: Color,
) {
let thickness = 4.0;
let quad_bounds = if vertical {
let y = match side {
DropSide::Before => bounds.y - thickness / 2.0,
DropSide::After => bounds.y + bounds.height - thickness / 2.0,
};
Rectangle {
x: bounds.x,
y,
width: bounds.width,
height: thickness,
}
} else {
let x = match side {
DropSide::Before => bounds.x - thickness / 2.0,
DropSide::After => bounds.x + bounds.width - thickness / 2.0,
};
Rectangle {
x,
y: bounds.y,
width: thickness,
height: bounds.height,
}
};
renderer.fill_quad(
renderer::Quad {
bounds: quad_bounds,
border: Border {
radius: 2.0.into(),
..Default::default()
},
shadow: Shadow::default(),
},
Background::Color(color),
);
}
fn left_button_released(event: &Event) -> bool { fn left_button_released(event: &Event) -> bool {
matches!( matches!(
event, event,