-
Notifications
You must be signed in to change notification settings - Fork 892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wayland: support basic Drag&Drop #2429
base: master
Are you sure you want to change the base?
Changes from all commits
9092ade
e0b6a93
611c82d
cd489be
3ab24c6
ad9468e
13b3605
1b54682
cde68dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
use std::io::{self, BufRead, Read}; | ||
use std::os::unix::prelude::{AsRawFd, RawFd}; | ||
use std::path::PathBuf; | ||
use std::str; | ||
|
||
use percent_encoding::percent_decode_str; | ||
use sctk::data_device::{DataOffer, DndEvent, ReadPipe}; | ||
use sctk::reexports::calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction}; | ||
|
||
use crate::dpi::PhysicalPosition; | ||
use crate::event::WindowEvent; | ||
use crate::platform_impl::wayland::{event_loop::WinitState, make_wid, DeviceId}; | ||
|
||
use super::DndInner; | ||
|
||
const MIME_TYPE: &str = "text/uri-list"; | ||
|
||
pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: &mut WinitState) { | ||
match event { | ||
DndEvent::Enter { | ||
offer: Some(offer), | ||
surface, | ||
x, | ||
y, | ||
.. | ||
} => { | ||
let window_id = make_wid(&surface); | ||
inner.window_id = Some(window_id); | ||
offer.accept(Some(MIME_TYPE.into())); | ||
let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| { | ||
if !paths.is_empty() { | ||
winit_state.event_sink.push_window_event( | ||
WindowEvent::CursorEntered { | ||
device_id: crate::event::DeviceId( | ||
crate::platform_impl::DeviceId::Wayland(DeviceId), | ||
), | ||
}, | ||
window_id, | ||
); | ||
winit_state.event_sink.push_window_event( | ||
WindowEvent::CursorMoved { | ||
device_id: crate::event::DeviceId( | ||
Comment on lines
+32
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto. |
||
crate::platform_impl::DeviceId::Wayland(DeviceId), | ||
), | ||
position: PhysicalPosition::new(x, y), | ||
modifiers: Default::default(), | ||
}, | ||
window_id, | ||
); | ||
|
||
for path in paths { | ||
winit_state | ||
.event_sink | ||
.push_window_event(WindowEvent::HoveredFile(path), window_id); | ||
} | ||
} | ||
}); | ||
} | ||
DndEvent::Drop { offer: Some(offer) } => { | ||
if let Some(window_id) = inner.window_id { | ||
inner.window_id = None; | ||
|
||
let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| { | ||
for path in paths { | ||
winit_state | ||
.event_sink | ||
.push_window_event(WindowEvent::DroppedFile(path), window_id); | ||
} | ||
}); | ||
} | ||
} | ||
DndEvent::Leave => { | ||
if let Some(window_id) = inner.window_id { | ||
inner.window_id = None; | ||
|
||
winit_state | ||
.event_sink | ||
.push_window_event(WindowEvent::HoveredFileCancelled, window_id); | ||
winit_state.event_sink.push_window_event( | ||
WindowEvent::CursorLeft { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also not sure how it's related, I'm pretty sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That doesn't seem to happen at least on KDE and GNOME. Only when no file is being dragged over. |
||
device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( | ||
DeviceId, | ||
)), | ||
}, | ||
window_id, | ||
); | ||
} | ||
} | ||
DndEvent::Motion { x, y, .. } => { | ||
if let Some(window_id) = inner.window_id { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why it generates that event, could you elaborate? |
||
winit_state.event_sink.push_window_event( | ||
WindowEvent::CursorMoved { | ||
device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( | ||
DeviceId, | ||
)), | ||
position: PhysicalPosition::new(x, y), | ||
modifiers: Default::default(), | ||
}, | ||
window_id, | ||
); | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
|
||
fn parse_offer( | ||
loop_handle: &LoopHandle<'static, WinitState>, | ||
offer: &DataOffer, | ||
mut callback: impl FnMut(Vec<PathBuf>, &mut WinitState) + 'static, | ||
) -> io::Result<()> { | ||
let can_accept = offer.with_mime_types(|types| types.iter().any(|s| s == MIME_TYPE)); | ||
if can_accept { | ||
let pipe = offer.receive(MIME_TYPE.into())?; | ||
SludgePhD marked this conversation as resolved.
Show resolved
Hide resolved
|
||
read_pipe_nonblocking(pipe, loop_handle, move |bytes, winit_state| { | ||
// Format: https://www.iana.org/assignments/media-types/text/uri-list | ||
let mut paths = Vec::new(); | ||
for line in bytes.lines() { | ||
let line = match line { | ||
Ok(line) => line, | ||
Err(_) => continue, | ||
}; | ||
|
||
if line.starts_with('#') { | ||
continue; | ||
} | ||
|
||
let decoded = match percent_decode_str(&line).decode_utf8() { | ||
Ok(decoded) => decoded, | ||
Err(_) => continue, | ||
}; | ||
if let Some(path) = decoded.strip_prefix("file://") { | ||
paths.push(PathBuf::from(path)); | ||
} | ||
} | ||
callback(paths, winit_state); | ||
})?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
fn read_pipe_nonblocking( | ||
pipe: ReadPipe, | ||
loop_handle: &LoopHandle<'static, WinitState>, | ||
mut callback: impl FnMut(Vec<u8>, &mut WinitState) + 'static, | ||
) -> io::Result<()> { | ||
unsafe { | ||
make_fd_nonblocking(pipe.as_raw_fd())?; | ||
} | ||
|
||
let mut content = Vec::<u8>::with_capacity(u16::MAX as usize); | ||
let mut reader_buffer = [0; u16::MAX as usize]; | ||
let reader = Generic::new(pipe, Interest::READ, Mode::Level); | ||
|
||
let _ = loop_handle.insert_source(reader, move |_, file, winit_state| { | ||
let action = loop { | ||
match file.read(&mut reader_buffer) { | ||
Ok(0) => { | ||
let data = std::mem::take(&mut content); | ||
callback(data, winit_state); | ||
break PostAction::Remove; | ||
} | ||
Ok(n) => content.extend_from_slice(&reader_buffer[..n]), | ||
Err(err) if err.kind() == io::ErrorKind::WouldBlock => break PostAction::Continue, | ||
Err(_) => break PostAction::Remove, | ||
} | ||
}; | ||
|
||
Ok(action) | ||
}); | ||
Ok(()) | ||
} | ||
|
||
unsafe fn make_fd_nonblocking(raw_fd: RawFd) -> io::Result<()> { | ||
let flags = libc::fcntl(raw_fd, libc::F_GETFL); | ||
if flags < 0 { | ||
return Err(io::Error::from_raw_os_error(flags)); | ||
} | ||
let result = libc::fcntl(raw_fd, libc::F_SETFL, flags | libc::O_NONBLOCK); | ||
if result < 0 { | ||
return Err(io::Error::from_raw_os_error(result)); | ||
} | ||
|
||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
use sctk::{data_device::DataDevice, reexports::calloop::LoopHandle}; | ||
use wayland_client::protocol::{wl_data_device_manager::WlDataDeviceManager, wl_seat::WlSeat}; | ||
use wayland_client::Attached; | ||
|
||
use crate::platform_impl::{wayland::event_loop::WinitState, WindowId}; | ||
|
||
mod handlers; | ||
|
||
pub(crate) struct Dnd { | ||
_data_device: DataDevice, | ||
} | ||
|
||
impl Dnd { | ||
pub fn new( | ||
seat: &Attached<WlSeat>, | ||
manager: &WlDataDeviceManager, | ||
loop_handle: LoopHandle<'static, WinitState>, | ||
) -> Self { | ||
let mut inner = DndInner { | ||
loop_handle, | ||
window_id: None, | ||
}; | ||
let data_device = | ||
DataDevice::init_for_seat(manager, seat, move |event, mut dispatch_data| { | ||
let winit_state = dispatch_data.get::<WinitState>().unwrap(); | ||
handlers::handle_dnd(event, &mut inner, winit_state); | ||
}); | ||
Self { | ||
_data_device: data_device, | ||
} | ||
} | ||
} | ||
|
||
struct DndInner { | ||
loop_handle: LoopHandle<'static, WinitState>, | ||
/// Window ID of the currently hovered window. | ||
window_id: Option<WindowId>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of these is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The callback emits the
HoveredFile
event though?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yeah. Sure. I'm not sure why anyone would need it. but ok.