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",
|
"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"
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue