Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17a2f62437 | ||
|
|
4bb0d69ce1 |
7 changed files with 46 additions and 977 deletions
|
|
@ -122,7 +122,6 @@ image = { version = "0.25.8", default-features = false, features = [
|
|||
"png",
|
||||
] }
|
||||
libc = { version = "0.2.175", optional = true }
|
||||
log = "0.4"
|
||||
mime = { version = "0.3.17", optional = true }
|
||||
palette = "0.7.6"
|
||||
raw-window-handle = "0.6"
|
||||
|
|
|
|||
|
|
@ -881,9 +881,7 @@ mod tests {
|
|||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &Path) -> Self {
|
||||
let original = env::var(key).ok();
|
||||
// std::env::{set_var, remove_var} are unsafe on newer toolchains;
|
||||
// we limit scope here to the test helper that toggles a single key.
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
std::env::set_var(key, value);
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
|
@ -891,9 +889,9 @@ mod tests {
|
|||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(ref original) = self.original {
|
||||
unsafe { std::env::set_var(self.key, original) };
|
||||
std::env::set_var(self.key, original);
|
||||
} 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());
|
||||
assert!(resolved.icon().is_some());
|
||||
assert!(resolved.exec().is_some());
|
||||
let expected = format!("crx_{}", id);
|
||||
assert_eq!(resolved.startup_wm_class(), Some(expected.as_str()));
|
||||
assert_eq!(resolved.startup_wm_class(), Some(&format!("crx_{}", id)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -9,28 +9,18 @@ use std::process::{Command, Stdio, exit};
|
|||
#[cfg(feature = "tokio")]
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
async fn read_from_pipe(read: OwnedFd) -> Option<u32> {
|
||||
#[cfg(feature = "tokio")]
|
||||
{
|
||||
let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap();
|
||||
return read.read_u32().await.ok();
|
||||
}
|
||||
let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap();
|
||||
read.read_u32().await.ok()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smol", not(feature = "tokio")))]
|
||||
{
|
||||
let mut read = smol::Async::new(std::fs::File::from(read)).unwrap();
|
||||
let mut bytes = [0; 4];
|
||||
read.read_exact(&mut bytes).await.ok()?;
|
||||
return 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));
|
||||
}
|
||||
#[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 bytes = [0; 4];
|
||||
read.read_exact(&mut bytes).await.ok()?;
|
||||
Some(u32::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Performs a double fork with setsid to spawn and detach a command.
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>(
|
|||
}
|
||||
|
||||
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)]
|
||||
pub struct DragId(pub u128);
|
||||
|
|
@ -76,12 +75,6 @@ pub struct 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 {
|
||||
Self {
|
||||
id: Id::unique(),
|
||||
|
|
@ -331,12 +324,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
|||
|
||||
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 {
|
||||
Event::Dnd(DndEvent::Offer(
|
||||
id,
|
||||
|
|
@ -344,18 +331,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
|||
x, y, mime_types, ..
|
||||
},
|
||||
)) 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(
|
||||
x,
|
||||
y,
|
||||
|
|
@ -385,11 +360,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
|||
return event::Status::Captured;
|
||||
}
|
||||
Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => {
|
||||
log::trace!(
|
||||
target: DND_DEST_LOG_TARGET,
|
||||
"offer leave id={:?}",
|
||||
my_id
|
||||
);
|
||||
if let Some(msg) =
|
||||
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;
|
||||
}
|
||||
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(
|
||||
x,
|
||||
y,
|
||||
|
|
@ -447,11 +413,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
|||
return event::Status::Captured;
|
||||
}
|
||||
Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => {
|
||||
log::trace!(
|
||||
target: DND_DEST_LOG_TARGET,
|
||||
"offer leave-destination id={:?}",
|
||||
my_id
|
||||
);
|
||||
if let Some(msg) =
|
||||
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;
|
||||
}
|
||||
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) =
|
||||
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)))
|
||||
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(
|
||||
action,
|
||||
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 }))
|
||||
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(
|
||||
mime_type,
|
||||
data,
|
||||
|
|
@ -573,16 +521,6 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
|||
) {
|
||||
let bounds = layout.bounds();
|
||||
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 {
|
||||
id: my_id,
|
||||
rectangle: dnd::Rectangle {
|
||||
|
|
@ -597,14 +535,12 @@ impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
|||
};
|
||||
dnd_rectangles.push(my_dest);
|
||||
|
||||
if let Some(child_layout) = layout.children().next() {
|
||||
self.container.as_widget().drag_destinations(
|
||||
&state.children[0],
|
||||
child_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
renderer,
|
||||
dnd_rectangles,
|
||||
);
|
||||
}
|
||||
self.container.as_widget().drag_destinations(
|
||||
&state.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
dnd_rectangles,
|
||||
);
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<Id> {
|
||||
|
|
@ -760,71 +696,3 @@ impl<'a, Message: 'static> From<DndDestination<'a, Message>> for Element<'a, Mes
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,19 +88,6 @@ pub use self::style::{Appearance, ItemAppearance, ItemStatusAppearance, StyleShe
|
|||
pub use self::vertical::{VerticalSegmentedButton, vertical};
|
||||
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.
|
||||
///
|
||||
/// The secondary map internally uses a `Vec`, so should only be used for data that
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ mod selection;
|
|||
pub use self::selection::{MultiSelect, Selectable, SingleSelect};
|
||||
|
||||
use crate::widget::Icon;
|
||||
use crate::widget::segmented_button::InsertPosition;
|
||||
use slotmap::{SecondaryMap, SlotMap};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::borrow::Cow;
|
||||
|
|
@ -411,36 +410,6 @@ where
|
|||
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.
|
||||
///
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::model::{Entity, Model, Selectable};
|
||||
use super::{InsertPosition, ReorderEvent};
|
||||
use crate::iced_core::id::Internal;
|
||||
use crate::theme::{SegmentedButton as Style, THEME};
|
||||
use crate::widget::dnd_destination::DragId;
|
||||
|
|
@ -13,9 +12,7 @@ use crate::widget::menu::{
|
|||
use crate::widget::{Icon, icon};
|
||||
use crate::{Element, Renderer};
|
||||
use derive_setters::Setters;
|
||||
use iced::clipboard::dnd::{
|
||||
self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent, SourceEvent,
|
||||
};
|
||||
use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent};
|
||||
use iced::clipboard::mime::AllowedMimeTypes;
|
||||
use iced::touch::Finger;
|
||||
use iced::{
|
||||
|
|
@ -44,8 +41,6 @@ thread_local! {
|
|||
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.
|
||||
pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
|
||||
task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0))))
|
||||
|
|
@ -56,27 +51,6 @@ pub enum ItemBounds {
|
|||
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`].
|
||||
pub trait SegmentedVariant {
|
||||
const VERTICAL: bool;
|
||||
|
|
@ -183,12 +157,6 @@ where
|
|||
#[setters(strip_option)]
|
||||
pub(super) drag_id: Option<DragId>,
|
||||
#[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
|
||||
variant: PhantomData<Variant>,
|
||||
}
|
||||
|
|
@ -236,9 +204,6 @@ where
|
|||
mimes: Vec::new(),
|
||||
variant: PhantomData,
|
||||
drag_id: None,
|
||||
tab_drag: None,
|
||||
on_drop_hint: None,
|
||||
on_reorder: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -296,77 +261,6 @@ where
|
|||
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.
|
||||
fn is_enabled(&self, key: Entity) -> bool {
|
||||
self.model.items.get(key).is_some_and(|item| item.enabled)
|
||||
|
|
@ -651,101 +545,6 @@ where
|
|||
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.
|
||||
///
|
||||
/// # Panics
|
||||
|
|
@ -812,9 +611,6 @@ where
|
|||
dnd_state: Default::default(),
|
||||
fingers_pressed: Default::default(),
|
||||
pressed_item: None,
|
||||
tab_drag_candidate: None,
|
||||
dragging_tab: None,
|
||||
drop_hint: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -905,7 +701,7 @@ where
|
|||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &iced::Rectangle,
|
||||
) -> event::Status {
|
||||
|
|
@ -921,26 +717,7 @@ where
|
|||
.drag_offer
|
||||
.as_ref()
|
||||
.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 {
|
||||
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(
|
||||
id,
|
||||
OfferEvent::Enter {
|
||||
|
|
@ -955,16 +732,6 @@ where
|
|||
})
|
||||
.find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
|
||||
.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 =
|
||||
self.on_dnd_enter
|
||||
|
|
@ -983,28 +750,15 @@ where
|
|||
);
|
||||
}
|
||||
DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {}
|
||||
DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination)
|
||||
if Some(my_id) == *id =>
|
||||
{
|
||||
state.drop_hint = None;
|
||||
self.emit_drop_hint(shell, state.drop_hint);
|
||||
DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) => {
|
||||
if let Some(Some(entity)) = entity {
|
||||
if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
|
||||
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);
|
||||
}
|
||||
DndEvent::Offer(_, OfferEvent::Leave | OfferEvent::LeaveDestination) => {}
|
||||
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
|
||||
.variant_bounds(state, bounds)
|
||||
.filter_map(|item| match item {
|
||||
|
|
@ -1021,12 +775,6 @@ where
|
|||
None::<fn(_, _, _) -> Message>,
|
||||
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 {
|
||||
let prev_action = state
|
||||
.dnd_state
|
||||
|
|
@ -1044,12 +792,6 @@ where
|
|||
}
|
||||
}
|
||||
} 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>(
|
||||
*x,
|
||||
*y,
|
||||
|
|
@ -1065,81 +807,32 @@ where
|
|||
}
|
||||
}
|
||||
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
|
||||
.dnd_state
|
||||
.on_drop::<Message>(None::<fn(_, _) -> Message>);
|
||||
}
|
||||
DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => {
|
||||
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
|
||||
.dnd_state
|
||||
.on_action_selected::<Message>(*action, None::<fn(_) -> Message>);
|
||||
}
|
||||
}
|
||||
DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => {
|
||||
log::trace!(
|
||||
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 {
|
||||
if let Some(Some(entity)) = entity {
|
||||
let on_drop = self.on_dnd_drop.as_ref();
|
||||
let on_drop = on_drop.map(|on_drop| {
|
||||
|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(data),
|
||||
None::<fn(_, _) -> Message>,
|
||||
on_drop,
|
||||
);
|
||||
if let Some(msg) = maybe_msg {
|
||||
log::trace!(
|
||||
target: TAB_REORDER_LOG_TARGET,
|
||||
"publishing drop message entity={entity:?}"
|
||||
);
|
||||
) {
|
||||
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;
|
||||
} 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));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1204,16 +897,12 @@ where
|
|||
// Record that the mouse is hovering over this button.
|
||||
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 self.model.items[key].closable {
|
||||
// Emit close message if the close button is pressed.
|
||||
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)
|
||||
|| (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) {
|
||||
state.unfocus();
|
||||
}
|
||||
|
|
@ -1387,42 +1046,6 @@ where
|
|||
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 let Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::Tab),
|
||||
|
|
@ -1497,7 +1120,6 @@ where
|
|||
) {
|
||||
let state = tree.state.downcast_mut::<LocalState>();
|
||||
operation.focusable(state, Some(&self.id.0));
|
||||
operation.custom(state, Some(&self.id.0));
|
||||
|
||||
if let Item::Set = state.focused_item {
|
||||
if self.prev_tab_sensitive(state) {
|
||||
|
|
@ -1558,12 +1180,6 @@ where
|
|||
let appearance = Self::variant_appearance(theme, &self.style);
|
||||
let bounds: Rectangle = layout.bounds();
|
||||
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.
|
||||
if let Some(background) = appearance.background {
|
||||
|
|
@ -1689,8 +1305,6 @@ where
|
|||
|
||||
// Draw each of the items in the widget.
|
||||
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| {
|
||||
let (key, mut bounds) = match item {
|
||||
// Draw a button
|
||||
|
|
@ -1718,27 +1332,8 @@ where
|
|||
}
|
||||
};
|
||||
|
||||
let original_bounds = bounds;
|
||||
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 = || {
|
||||
state.show_context == Some(key)
|
||||
&& !tree.children.is_empty()
|
||||
|
|
@ -1803,6 +1398,7 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
let original_bounds = bounds;
|
||||
bounds.x += f32::from(self.button_padding[0]);
|
||||
bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
@ -2081,68 +1659,27 @@ where
|
|||
|
||||
fn drag_destinations(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
_state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
|
||||
) {
|
||||
let local_state = tree.state.downcast_ref::<LocalState>();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
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();
|
||||
log::trace!(
|
||||
target: TAB_REORDER_LOG_TARGET,
|
||||
"register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
|
||||
my_id,
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
self.mimes
|
||||
);
|
||||
dnd_rectangles.push(DndDestinationRectangle {
|
||||
id: my_id,
|
||||
rectangle: dnd::Rectangle {
|
||||
x: f64::from(bounds.x),
|
||||
y: f64::from(bounds.y),
|
||||
width: f64::from(bounds.width),
|
||||
height: f64::from(bounds.height),
|
||||
},
|
||||
mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
|
||||
actions: DndAction::Copy | DndAction::Move,
|
||||
preferred: DndAction::Move,
|
||||
});
|
||||
}
|
||||
let dnd_rect = DndDestinationRectangle {
|
||||
id: my_id,
|
||||
rectangle: dnd::Rectangle {
|
||||
x: f64::from(bounds.x),
|
||||
y: f64::from(bounds.y),
|
||||
width: f64::from(bounds.width),
|
||||
height: f64::from(bounds.height),
|
||||
},
|
||||
mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
|
||||
actions: DndAction::Copy | 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)]
|
||||
struct Focus {
|
||||
updated_at: Instant,
|
||||
|
|
@ -2258,12 +1747,6 @@ pub struct LocalState {
|
|||
fingers_pressed: HashSet<Finger>,
|
||||
/// The currently pressed 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)]
|
||||
|
|
@ -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 {
|
||||
fn is_focused(&self) -> bool {
|
||||
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 {
|
||||
matches!(
|
||||
event,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue