use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use smithay_client_toolkit::compositor::{CompositorHandler, CompositorState};
use smithay_client_toolkit::data_device_manager::data_device::{DataDevice, DataDeviceHandler};
use smithay_client_toolkit::data_device_manager::data_offer::{DataOfferHandler, DragOffer};
use smithay_client_toolkit::data_device_manager::data_source::{DataSourceHandler, DragSource};
use smithay_client_toolkit::data_device_manager::{DataDeviceManagerState, WritePipe};
use smithay_client_toolkit::output::{OutputHandler, OutputState};
use smithay_client_toolkit::reexports::calloop::{
EventLoop as CalloopLoop, LoopHandle, PostAction,
};
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
use smithay_client_toolkit::seat::keyboard::{
KeyEvent as WlKeyEvent, KeyboardHandler, Keysym, Modifiers as WlModifiers,
};
use smithay_client_toolkit::seat::pointer::{
AxisScroll, PointerData, PointerEvent, PointerEventKind, PointerHandler,
};
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
use smithay_client_toolkit::shell::WaylandSurface;
use smithay_client_toolkit::shell::xdg::popup::{Popup, PopupConfigure, PopupHandler};
use smithay_client_toolkit::shell::xdg::window::{
Window as XdgWindow, WindowConfigure, WindowDecorations, WindowHandler,
};
use smithay_client_toolkit::shell::xdg::{XdgShell, XdgSurface};
use smithay_client_toolkit::shm::slot::{Buffer, SlotPool};
use smithay_client_toolkit::shm::{Shm, ShmHandler};
use smithay_client_toolkit::{
delegate_compositor, delegate_data_device, delegate_keyboard, delegate_output,
delegate_pointer, delegate_registry, delegate_seat, delegate_shm, delegate_xdg_popup,
delegate_xdg_shell, delegate_xdg_window, registry_handlers,
};
use wayland_client::globals::registry_queue_init;
use wayland_client::protocol::wl_data_device::WlDataDevice;
use wayland_client::protocol::wl_data_device_manager::DndAction;
use wayland_client::protocol::wl_data_source::WlDataSource;
use wayland_client::protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{
Shape, WpCursorShapeDeviceV1,
};
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1;
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1;
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::{
self, WpFractionalScaleV1,
};
use wayland_protocols::xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1;
use wayland_protocols::xdg::dialog::v1::client::xdg_wm_dialog_v1::XdgWmDialogV1;
use wayland_protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity, XdgPositioner};
use wayland_protocols::xdg::shell::client::xdg_surface::XdgSurface as XdgSurfaceObj;
use crate::app::{App, KeySwallow};
use crate::background::BackgroundState;
use crate::event::{
DragData, Event, EventCtx, Key, Modifiers, MouseButton, NamedKey, SCROLL_PIXELS_PER_LINE,
WHEEL_LINES_PER_DETENT,
};
use crate::font::Font;
use crate::geometry::{Color, Point, Rect, Size};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::{PopupKind, PopupRequest, Widget};
pub(crate) fn run(app: App) {
let (window_cfg, theme, root) = app.into_parts();
let conn = Connection::connect_to_env().expect("saudade: Wayland connect failed");
let (globals, event_queue) =
registry_queue_init::<State>(&conn).expect("saudade: registry init failed");
let qh: QueueHandle<State> = event_queue.handle();
let mut event_loop: CalloopLoop<State> =
CalloopLoop::try_new().expect("saudade: calloop init failed");
let loop_handle = event_loop.handle();
WaylandSource::new(conn.clone(), event_queue)
.insert(loop_handle)
.expect("saudade: WaylandSource insert failed");
let compositor =
CompositorState::bind(&globals, &qh).expect("saudade: wl_compositor not available");
let xdg_shell = XdgShell::bind(&globals, &qh).expect("saudade: xdg_shell not available");
let shm = Shm::bind(&globals, &qh).expect("saudade: wl_shm not available");
let xdg_dialog_mgr: Option<XdgWmDialogV1> =
globals.bind::<XdgWmDialogV1, _, _>(&qh, 1..=1, ()).ok();
let fractional_mgr: Option<WpFractionalScaleManagerV1> = globals
.bind::<WpFractionalScaleManagerV1, _, _>(&qh, 1..=1, ())
.ok();
let data_device_manager = DataDeviceManagerState::bind(&globals, &qh).ok();
let cursor_shape_mgr: Option<WpCursorShapeManagerV1> = globals
.bind::<WpCursorShapeManagerV1, _, _>(&qh, 1..=1, ())
.ok();
let surface = compositor.create_surface(&qh);
let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh);
window.set_title(&window_cfg.title);
window.set_app_id(format!("saudade.{}", sanitize(&window_cfg.title)));
let fractional_scale_obj = fractional_mgr
.as_ref()
.map(|mgr| mgr.get_fractional_scale(window.wl_surface(), &qh, ()));
let initial_w = window_cfg.size.w.max(1) as u32;
let initial_h = window_cfg.size.h.max(1) as u32;
if window_cfg.resizable {
window.set_min_size(Some((100, 60)));
} else {
window.set_min_size(Some((initial_w, initial_h)));
window.set_max_size(Some((initial_w, initial_h)));
}
window.commit();
let pool = SlotPool::new((initial_w * initial_h * 4) as usize * 4, &shm)
.expect("saudade: slot pool init failed");
let mut state = State {
registry_state: RegistryState::new(&globals),
seat_state: SeatState::new(&globals, &qh),
output_state: OutputState::new(&globals, &qh),
compositor,
shm,
xdg_shell,
xdg_dialog_mgr,
window,
root,
theme,
font: Font::load_system(),
mono_font: Font::load_monospace(),
pool,
surface_w: initial_w,
surface_h: initial_h,
scale: 1,
fractional_scale_obj,
fractional_scale: None,
resizable: window_cfg.resizable,
configured: false,
needs_redraw: true,
exit: false,
keyboard: None,
pointer: None,
data_device_manager,
data_device: None,
drag: None,
drag_grab_serial: None,
drag_origin_surface: None,
drag_source: None,
drag_payload: Vec::new(),
drag_icon: None,
cursor_shape_mgr,
cursor_shape_device: None,
modifiers: Modifiers::default(),
bg: BackgroundState::from_env(),
cursor: None,
popups: Vec::new(),
qh: qh.clone(),
loop_handle: event_loop.handle(),
swallow: KeySwallow::default(),
};
drop(conn);
while !state.exit {
event_loop
.dispatch(Duration::from_millis(16), &mut state)
.expect("saudade: dispatch failed");
state.tick();
}
}
struct State {
registry_state: RegistryState,
seat_state: SeatState,
output_state: OutputState,
compositor: CompositorState,
shm: Shm,
xdg_shell: XdgShell,
xdg_dialog_mgr: Option<XdgWmDialogV1>,
window: XdgWindow,
root: Box<dyn Widget>,
theme: Theme,
font: Option<Font>,
mono_font: Option<Font>,
pool: SlotPool,
surface_w: u32,
surface_h: u32,
scale: i32,
#[allow(dead_code)]
fractional_scale_obj: Option<WpFractionalScaleV1>,
fractional_scale: Option<f32>,
resizable: bool,
configured: bool,
needs_redraw: bool,
exit: bool,
keyboard: Option<wl_keyboard::WlKeyboard>,
pointer: Option<wl_pointer::WlPointer>,
data_device_manager: Option<DataDeviceManagerState>,
data_device: Option<DataDevice>,
drag: Option<DragSession>,
drag_grab_serial: Option<u32>,
drag_origin_surface: Option<wl_surface::WlSurface>,
drag_source: Option<DragSource>,
drag_payload: Vec<u8>,
drag_icon: Option<DragIcon>,
cursor_shape_mgr: Option<WpCursorShapeManagerV1>,
cursor_shape_device: Option<WpCursorShapeDeviceV1>,
modifiers: Modifiers,
bg: BackgroundState,
cursor: Option<Point>,
popups: Vec<PopupState>,
qh: QueueHandle<State>,
loop_handle: LoopHandle<'static, State>,
swallow: KeySwallow,
}
enum ChildSurface {
Popup(Popup),
Dialog {
window: XdgWindow,
dialog_v1: Option<XdgDialogV1>,
},
}
impl ChildSurface {
fn wl_surface(&self) -> &wl_surface::WlSurface {
match self {
ChildSurface::Popup(p) => p.wl_surface(),
ChildSurface::Dialog { window, .. } => window.wl_surface(),
}
}
fn kind(&self) -> PopupKind {
match self {
ChildSurface::Popup(_) => PopupKind::Popup,
ChildSurface::Dialog { .. } => PopupKind::Dialog,
}
}
fn xdg_surface(&self) -> &XdgSurfaceObj {
match self {
ChildSurface::Popup(p) => p.xdg_surface(),
ChildSurface::Dialog { window, .. } => window.xdg_surface(),
}
}
}
impl Drop for ChildSurface {
fn drop(&mut self) {
if let ChildSurface::Dialog {
dialog_v1: Some(d), ..
} = self
{
d.destroy();
}
}
}
struct PopupState {
surface: ChildSurface,
pool: SlotPool,
anchor: Rect,
surface_w: u32,
surface_h: u32,
configured: bool,
needs_redraw: bool,
cursor: Option<Point>,
}
struct DragSession {
anchor: Point,
pos: Point,
accepted: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DragFeedback {
Copy,
NoDrop,
}
struct DragIcon {
surface: wl_surface::WlSurface,
pool: SlotPool,
label: String,
logical_w: i32,
logical_h: i32,
badge: i32,
scale: i32,
feedback: DragFeedback,
}
impl State {
fn tick(&mut self) {
self.sync_popup();
if self.root.wants_ticks() {
self.dispatch(Event::Tick);
}
if self.configured && self.needs_redraw {
self.draw_main();
self.needs_redraw = false;
}
for idx in 0..self.popups.len() {
let should_draw = self.popups[idx].configured && self.popups[idx].needs_redraw;
if should_draw && self.draw_popup(idx) {
self.popups[idx].needs_redraw = false;
}
}
}
fn mark_popups_dirty(&mut self) {
for p in &mut self.popups {
p.needs_redraw = true;
}
}
fn relayout(&mut self) {
self.root.layout(Rect::new(
0,
0,
self.surface_w.max(1) as i32,
self.surface_h.max(1) as i32,
));
}
fn dispatch(&mut self, event: Event) -> EventCtx {
let mut ctx = EventCtx::new();
self.root.event(&event, &mut ctx);
if ctx.paint_requested {
self.needs_redraw = true;
self.mark_popups_dirty();
}
if ctx.close_requested {
self.exit = true;
}
if let Some(size) = ctx.resize_request {
self.apply_resize(size);
}
if let Some(data) = ctx.drag_request.take() {
self.begin_drag(data);
}
ctx
}
fn begin_drag(&mut self, data: DragData) {
let (Some(mgr), Some(dd), Some(serial)) = (
self.data_device_manager.as_ref(),
self.data_device.as_ref(),
self.drag_grab_serial,
) else {
return;
};
if data.paths.is_empty() {
return;
}
let origin = self
.drag_origin_surface
.clone()
.unwrap_or_else(|| self.window.wl_surface().clone());
let icon = self.create_drag_icon(&data);
let source = mgr.create_drag_and_drop_source(&self.qh, [URI_LIST_MIME], DndAction::Copy);
source.start_drag(dd, &origin, icon.as_ref().map(|i| &i.surface), serial);
self.drag_payload = paths_to_uri_list(&data.paths).into_bytes();
self.drag_source = Some(source);
if let Some(mut icon) = icon {
self.paint_drag_icon(&mut icon);
self.drag_icon = Some(icon);
}
self.set_drag_cursor(Shape::NoDrop);
}
fn create_drag_icon(&self, data: &DragData) -> Option<DragIcon> {
let label = drag_icon_label(&data.paths);
let font = self.font.as_ref()?;
let (text_w, text_h) = font.measure(&label, self.theme.font_size);
let text_h = text_h.ceil() as i32;
let badge = text_h.max(DRAG_ICON_BADGE_MIN);
let logical_w =
DRAG_ICON_PAD + badge + DRAG_ICON_GAP + text_w.ceil() as i32 + DRAG_ICON_PAD;
let logical_h = DRAG_ICON_PAD + badge.max(text_h) + DRAG_ICON_PAD;
let scale = self.scale.max(1);
let pool_bytes = (logical_w * scale * logical_h * scale * 4) as usize * 2;
let pool = SlotPool::new(pool_bytes, &self.shm).ok()?;
let surface = self.compositor.create_surface(&self.qh);
Some(DragIcon {
surface,
pool,
label,
logical_w,
logical_h,
badge,
scale,
feedback: DragFeedback::NoDrop,
})
}
fn paint_drag_icon(&self, icon: &mut DragIcon) {
let buf_w = icon.logical_w * icon.scale;
let buf_h = icon.logical_h * icon.scale;
let stride = buf_w * 4;
let Ok((buffer, _)) =
icon.pool
.create_buffer(buf_w, buf_h, stride, wl_shm::Format::Argb8888)
else {
return;
};
let Some(canvas) = icon.pool.canvas(&buffer) else {
return;
};
let pixels = bytes_as_u32_mut(canvas);
{
let mut painter = Painter::with_popup_anchor(
pixels,
buf_w,
buf_h,
icon.scale as f32,
0,
0,
self.font.as_ref(),
self.mono_font.as_ref(),
None,
);
painter.set_system_scale(self.fractional_scale.unwrap_or(icon.scale as f32));
let area = Rect::new(0, 0, icon.logical_w, icon.logical_h);
painter.fill_rect(area, self.theme.background);
painter.raised_bevel(area, self.theme.highlight, self.theme.shadow);
let badge = Rect::new(DRAG_ICON_PAD, DRAG_ICON_PAD, icon.badge, icon.badge);
draw_drag_badge(&mut painter, badge, icon.feedback);
painter.text(
DRAG_ICON_PAD + icon.badge + DRAG_ICON_GAP,
DRAG_ICON_PAD,
&icon.label,
self.theme.font_size,
self.theme.text,
);
}
let _ = buffer.attach_to(&icon.surface);
icon.surface.set_buffer_scale(icon.scale);
icon.surface.damage_buffer(0, 0, buf_w, buf_h);
icon.surface.commit();
}
fn update_drag_feedback(&mut self, feedback: DragFeedback) {
let Some(mut icon) = self.drag_icon.take() else {
return;
};
if icon.feedback != feedback {
icon.feedback = feedback;
self.paint_drag_icon(&mut icon);
}
self.drag_icon = Some(icon);
}
fn end_drag_source(&mut self, source: &WlDataSource) {
if self
.drag_source
.as_ref()
.is_some_and(|s| s.inner() == source)
{
if let Some(s) = self.drag_source.take() {
s.inner().destroy();
}
if let Some(icon) = self.drag_icon.take() {
icon.surface.destroy();
}
self.drag_payload.clear();
self.set_drag_cursor(Shape::Default);
}
}
fn set_drag_cursor(&self, shape: Shape) {
let (Some(device), Some(pointer)) =
(self.cursor_shape_device.as_ref(), self.pointer.as_ref())
else {
return;
};
if let Some(serial) = pointer
.data::<PointerData>()
.and_then(|d| d.latest_enter_serial())
{
device.set_shape(serial, shape);
}
}
fn apply_resize(&mut self, size: Size) {
let w = size.w.max(1) as u32;
let h = size.h.max(1) as u32;
if w == self.surface_w && h == self.surface_h {
return;
}
if !self.resizable {
self.window.set_min_size(Some((w, h)));
self.window.set_max_size(Some((w, h)));
}
self.surface_w = w;
self.surface_h = h;
self.window.commit();
self.relayout();
self.needs_redraw = true;
self.mark_popups_dirty();
}
fn draw_main(&mut self) {
let scale = self.scale.max(1);
let buf_w = (self.surface_w.max(1) * scale as u32) as i32;
let buf_h = (self.surface_h.max(1) * scale as u32) as i32;
let stride = buf_w * 4;
let buffer = match self
.pool
.create_buffer(buf_w, buf_h, stride, wl_shm::Format::Argb8888)
{
Ok((b, _)) => b,
Err(_) => return,
};
let canvas = match self.pool.canvas(&buffer) {
Some(c) => c,
None => return,
};
let pixels = bytes_as_u32_mut(canvas);
let mut painter = Painter::with_popup_anchor(
pixels,
buf_w,
buf_h,
scale as f32,
0,
0,
self.font.as_ref(),
self.mono_font.as_ref(),
None,
);
painter.set_system_scale(self.fractional_scale.unwrap_or(scale as f32));
painter.fill_pattern(self.theme.background, self.bg.pattern, self.bg.color);
self.root.paint(&mut painter, &self.theme);
let surface = self.window.wl_surface();
let _ = buffer.attach_to(surface);
surface.damage_buffer(0, 0, buf_w, buf_h);
surface.set_buffer_scale(scale);
surface.frame(&self.qh, surface.clone());
surface.commit();
}
fn draw_popup(&mut self, idx: usize) -> bool {
let scale = self.scale.max(1);
let Some(p) = self.popups.get_mut(idx) else {
return false;
};
let buf_w = (p.surface_w.max(1) * scale as u32) as i32;
let buf_h = (p.surface_h.max(1) * scale as u32) as i32;
let stride = buf_w * 4;
let buffer = match p
.pool
.create_buffer(buf_w, buf_h, stride, wl_shm::Format::Argb8888)
{
Ok((b, _)) => b,
Err(_) => return false,
};
let canvas = match p.pool.canvas(&buffer) {
Some(c) => c,
None => return false,
};
let pixels = bytes_as_u32_mut(canvas);
let scale_f = scale as f32;
let anchor = p.anchor;
let origin_x = -((anchor.x as f32 * scale_f).round() as i32);
let origin_y = -((anchor.y as f32 * scale_f).round() as i32);
let clip_w = (anchor.w as f32 * scale_f).round() as i32;
let clip_h = (anchor.h as f32 * scale_f).round() as i32;
let mut painter = Painter::with_popup_anchor(
pixels,
buf_w,
buf_h,
scale_f,
origin_x,
origin_y,
self.font.as_ref(),
self.mono_font.as_ref(),
Some(anchor),
);
painter.set_system_scale(self.fractional_scale.unwrap_or(scale_f));
painter.fill(self.theme.background);
painter.set_clip_phys(0, 0, clip_w, clip_h);
self.root.paint(&mut painter, &self.theme);
painter.clear_clip();
let surface = p.surface.wl_surface();
let _ = buffer.attach_to(surface);
surface.damage_buffer(0, 0, buf_w, buf_h);
surface.set_buffer_scale(scale);
surface.frame(&self.qh, surface.clone());
surface.commit();
true
}
fn sync_popup(&mut self) {
let mut requests = Vec::new();
self.root.collect_popups(&mut requests);
let keep = self
.popups
.iter()
.zip(requests.iter())
.take_while(|(p, req)| p.anchor == req.rect && p.surface.kind() == req.kind)
.count();
self.popups.truncate(keep);
for req in requests.into_iter().skip(keep) {
let made = match self.popups.last() {
Some(parent) => {
let parent_anchor = parent.anchor;
let parent_xdg = parent.surface.xdg_surface();
self.create_popup(&req, parent_anchor, Some(parent_xdg))
}
None => self.create_popup(&req, Rect::new(0, 0, 0, 0), None),
};
match made {
Some(p) => self.popups.push(p),
None => break,
}
}
}
fn create_popup(
&self,
request: &PopupRequest,
parent_anchor: Rect,
parent_xdg: Option<&XdgSurfaceObj>,
) -> Option<PopupState> {
let anchor = request.rect;
let phys_w = anchor.w.max(1) as u32;
let phys_h = anchor.h.max(1) as u32;
let surface = match request.kind {
PopupKind::Popup => {
let rel_x = anchor.x - parent_anchor.x;
let rel_y = anchor.y - parent_anchor.y;
let positioner: XdgPositioner =
self.xdg_shell.xdg_wm_base().create_positioner(&self.qh, ());
positioner.set_size(anchor.w.max(1), anchor.h.max(1));
positioner.set_anchor_rect(rel_x, rel_y, 1, 1);
positioner.set_anchor(Anchor::BottomLeft);
positioner.set_gravity(Gravity::BottomRight);
let parent = parent_xdg.unwrap_or_else(|| self.window.xdg_surface());
let popup = match Popup::new(
parent,
&positioner,
&self.qh,
&self.compositor,
&self.xdg_shell,
) {
Ok(p) => p,
Err(_) => return None,
};
positioner.destroy();
ChildSurface::Popup(popup)
}
PopupKind::Dialog => {
let dialog_surface = self.compositor.create_surface(&self.qh);
let dialog = self.xdg_shell.create_window(
dialog_surface,
WindowDecorations::RequestServer,
&self.qh,
);
let title = request.title.as_deref().unwrap_or("Dialog");
dialog.set_title(title);
dialog.set_parent(Some(&self.window));
dialog.set_min_size(Some((phys_w, phys_h)));
dialog.set_max_size(Some((phys_w, phys_h)));
let dialog_v1 = self.xdg_dialog_mgr.as_ref().map(|mgr| {
let d = mgr.get_xdg_dialog(dialog.xdg_toplevel(), &self.qh, ());
d.set_modal();
d
});
dialog.commit();
ChildSurface::Dialog {
window: dialog,
dialog_v1,
}
}
};
let max_scale = self.scale.max(1) as u32;
let pool_bytes = (phys_w * phys_h * max_scale * max_scale * 4) as usize * 2;
let pool = match SlotPool::new(pool_bytes, &self.shm) {
Ok(p) => p,
Err(_) => return None,
};
Some(PopupState {
surface,
pool,
anchor,
surface_w: phys_w,
surface_h: phys_h,
configured: false,
needs_redraw: true,
cursor: None,
})
}
fn physical_to_logical(&self, surface_x: f64, surface_y: f64) -> Point {
let s = self.scale.max(1) as f64;
let _ = s;
Point::new(surface_x.floor() as i32, surface_y.floor() as i32)
}
fn surface_anchor(&self, surface: &wl_surface::WlSurface) -> Point {
self.popups
.iter()
.find(|p| p.surface.wl_surface().id() == surface.id())
.map(|p| Point::new(p.anchor.x, p.anchor.y))
.unwrap_or(Point::new(0, 0))
}
fn apply_drag_offer(&self, offer: &DragOffer, accepted: bool, conn: &Connection) {
if accepted {
offer.set_actions(DndAction::Copy, DndAction::Copy);
offer.accept_mime_type(offer.serial, Some(URI_LIST_MIME.to_string()));
} else {
offer.set_actions(DndAction::empty(), DndAction::empty());
offer.accept_mime_type(offer.serial, None);
}
let _ = conn.flush();
}
fn ensure_data_device(&mut self, qh: &QueueHandle<Self>, seat: &wl_seat::WlSeat) {
if self.data_device.is_none()
&& let Some(mgr) = self.data_device_manager.as_ref()
{
self.data_device = Some(mgr.get_data_device(qh, seat));
}
}
}
impl CompositorHandler for State {
fn scale_factor_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
new_factor: i32,
) {
let new = new_factor.max(1);
if new == self.scale {
return;
}
self.scale = new;
self.needs_redraw = true;
self.mark_popups_dirty();
self.relayout();
}
fn transform_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_new_transform: wl_output::Transform,
) {
}
fn frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_time: u32,
) {
}
fn surface_enter(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
fn surface_leave(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
}
impl OutputHandler for State {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
}
impl WindowHandler for State {
fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, window: &XdgWindow) {
if window.xdg_toplevel() == self.window.xdg_toplevel() {
let mut ctx = EventCtx::new();
self.root.on_cancel(&mut ctx);
self.exit = true;
return;
}
if self.popups.iter().any(|p| {
matches!(&p.surface, ChildSurface::Dialog { window: dialog, .. }
if dialog.xdg_toplevel() == window.xdg_toplevel())
}) {
let mods = self.modifiers;
self.dispatch(Event::KeyDown {
key: Key::Named(NamedKey::Escape),
modifiers: mods,
});
}
}
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
window: &XdgWindow,
configure: WindowConfigure,
_serial: u32,
) {
if window.xdg_toplevel() == self.window.xdg_toplevel() {
let w = configure
.new_size
.0
.map(|v| v.get())
.unwrap_or(self.surface_w.max(1));
let h = configure
.new_size
.1
.map(|v| v.get())
.unwrap_or(self.surface_h.max(1));
self.surface_w = w;
self.surface_h = h;
let first_configure = !self.configured;
self.configured = true;
self.needs_redraw = true;
self.relayout();
if first_configure {
self.root.focus_first();
}
return;
}
if let Some(p) = self.popups.iter_mut().find(|p| {
matches!(&p.surface, ChildSurface::Dialog { window: dialog, .. }
if dialog.xdg_toplevel() == window.xdg_toplevel())
}) {
p.configured = true;
p.needs_redraw = true;
}
}
}
impl PopupHandler for State {
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
popup: &Popup,
configure: PopupConfigure,
) {
if let Some(p) = self.popups.iter_mut().find(|p| {
matches!(&p.surface, ChildSurface::Popup(existing)
if existing.xdg_popup() == popup.xdg_popup())
}) {
p.surface_w = configure.width.max(1) as u32;
p.surface_h = configure.height.max(1) as u32;
p.configured = true;
p.needs_redraw = true;
}
}
fn done(&mut self, _: &Connection, _: &QueueHandle<Self>, _popup: &Popup) {
let mods = self.modifiers;
self.dispatch(Event::KeyDown {
key: Key::Named(NamedKey::Escape),
modifiers: mods,
});
}
}
impl SeatHandler for State {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _conn: &Connection, qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
self.ensure_data_device(qh, &seat);
}
fn new_capability(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
capability: Capability,
) {
self.ensure_data_device(qh, &seat);
if capability == Capability::Keyboard && self.keyboard.is_none() {
self.keyboard = self
.seat_state
.get_keyboard_with_repeat(
qh,
&seat,
None,
self.loop_handle.clone(),
Box::new(|state: &mut State, _kbd, event| {
state.handle_key(event, true);
}),
)
.ok();
}
if capability == Capability::Pointer && self.pointer.is_none() {
self.pointer = self.seat_state.get_pointer(qh, &seat).ok();
if let (Some(mgr), Some(ptr)) = (self.cursor_shape_mgr.as_ref(), self.pointer.as_ref())
{
self.cursor_shape_device = Some(mgr.get_pointer(ptr, qh, ()));
}
}
}
fn remove_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Keyboard
&& let Some(k) = self.keyboard.take()
{
k.release();
}
if capability == Capability::Pointer
&& let Some(p) = self.pointer.take()
{
p.release();
}
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
}
impl KeyboardHandler for State {
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_keyboard: &wl_keyboard::WlKeyboard,
_surface: &wl_surface::WlSurface,
_serial: u32,
_raw: &[u32],
_keysyms: &[Keysym],
) {
}
fn leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: &wl_surface::WlSurface,
_: u32,
) {
}
fn press_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
event: WlKeyEvent,
) {
self.handle_key(event, true);
}
fn release_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
event: WlKeyEvent,
) {
self.handle_key(event, false);
}
fn update_modifiers(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
modifiers: WlModifiers,
_layout: u32,
) {
self.modifiers = Modifiers {
shift: modifiers.shift,
control: modifiers.ctrl,
alt: modifiers.alt,
alt_graph: false,
logo: modifiers.logo,
};
}
}
impl State {
fn handle_key(&mut self, event: WlKeyEvent, pressed: bool) {
let modifiers = self.modifiers;
let mapped = map_keysym(event.keysym);
if pressed {
if self.swallow.drops_press(mapped) {
return;
}
if let Some(m) = mapped {
let ctx = self.dispatch(Event::KeyDown { key: m, modifiers });
if ctx.swallow_key {
self.swallow.begin(mapped);
return;
}
}
if !modifiers.has_command()
&& let Some(utf8) = event.utf8.as_deref()
{
for ch in utf8.chars() {
if (ch.is_control() && ch != '\t' && ch != '\n') || ch == '\r' {
continue;
}
let ctx = self.dispatch(Event::Char { ch, modifiers });
if ctx.swallow_key {
self.swallow.begin(mapped);
return;
}
}
}
} else if self.swallow.ends_on_release(mapped) {
} else if let Some(m) = mapped {
self.dispatch(Event::KeyUp { key: m, modifiers });
}
}
}
impl PointerHandler for State {
fn pointer_frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_pointer: &wl_pointer::WlPointer,
events: &[PointerEvent],
) {
for event in events {
let popup_idx = self
.popups
.iter()
.position(|p| p.surface.wl_surface().id() == event.surface.id());
let pos = match popup_idx {
Some(i) => {
let anchor = self.popups[i].anchor;
Point::new(
event.position.0.floor() as i32 + anchor.x,
event.position.1.floor() as i32 + anchor.y,
)
}
None => self.physical_to_logical(event.position.0, event.position.1),
};
match event.kind {
PointerEventKind::Enter { .. } | PointerEventKind::Motion { .. } => {
match popup_idx {
Some(i) => self.popups[i].cursor = Some(pos),
None => self.cursor = Some(pos),
}
self.dispatch(Event::PointerMove { pos });
self.mark_popups_dirty();
}
PointerEventKind::Leave { .. } => {
match popup_idx {
Some(i) => self.popups[i].cursor = None,
None => self.cursor = None,
}
self.dispatch(Event::PointerLeave);
}
PointerEventKind::Press { button, serial, .. } => {
let Some(b) = map_button(button) else {
continue;
};
self.drag_grab_serial = Some(serial);
self.drag_origin_surface = Some(event.surface.clone());
self.dispatch(Event::PointerDown { pos, button: b });
self.mark_popups_dirty();
}
PointerEventKind::Release { button, .. } => {
let Some(b) = map_button(button) else {
continue;
};
self.dispatch(Event::PointerUp { pos, button: b });
self.mark_popups_dirty();
}
PointerEventKind::Axis {
horizontal,
vertical,
..
} => {
let lines = |axis: AxisScroll| {
if axis.discrete != 0 {
axis.discrete as f32 * WHEEL_LINES_PER_DETENT
} else {
axis.absolute as f32 / SCROLL_PIXELS_PER_LINE
}
};
let delta_x = lines(horizontal);
let delta_y = lines(vertical);
if delta_x != 0.0 || delta_y != 0.0 {
self.dispatch(Event::Scroll {
pos,
delta_x,
delta_y,
});
self.mark_popups_dirty();
}
}
}
}
}
}
impl DataDeviceHandler for State {
fn enter(
&mut self,
conn: &Connection,
_qh: &QueueHandle<Self>,
_data_device: &WlDataDevice,
x: f64,
y: f64,
surface: &wl_surface::WlSurface,
) {
let offer = match self.data_device.as_ref() {
Some(dd) => dd.data().drag_offer(),
None => None,
};
let Some(offer) = offer else { return };
let has_uri = offer.with_mime_types(|mimes| mimes.iter().any(|m| m == URI_LIST_MIME));
if !has_uri {
offer.accept_mime_type(offer.serial, None);
return;
}
let anchor = self.surface_anchor(surface);
let pos = Point::new(x.floor() as i32 + anchor.x, y.floor() as i32 + anchor.y);
self.drag = Some(DragSession {
anchor,
pos,
accepted: false,
});
let accepted = self.dispatch(Event::DragEnter { pos }).accepts_drop;
if let Some(d) = self.drag.as_mut() {
d.accepted = accepted;
}
self.apply_drag_offer(&offer, accepted, conn);
self.mark_popups_dirty();
}
fn leave(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _data_device: &WlDataDevice) {
if self.drag.take().is_some() {
self.dispatch(Event::DragLeave);
self.mark_popups_dirty();
}
}
fn motion(
&mut self,
conn: &Connection,
_qh: &QueueHandle<Self>,
_data_device: &WlDataDevice,
x: f64,
y: f64,
) {
let Some(anchor) = self.drag.as_ref().map(|d| d.anchor) else {
return;
};
let pos = Point::new(x.floor() as i32 + anchor.x, y.floor() as i32 + anchor.y);
if let Some(drag) = self.drag.as_mut() {
drag.pos = pos;
}
let accepted = self.dispatch(Event::DragMove { pos }).accepts_drop;
let changed = self.drag.as_ref().is_some_and(|d| d.accepted != accepted);
if let Some(drag) = self.drag.as_mut() {
drag.accepted = accepted;
}
if changed
&& let Some(offer) = self
.data_device
.as_ref()
.and_then(|dd| dd.data().drag_offer())
{
self.apply_drag_offer(&offer, accepted, conn);
}
self.mark_popups_dirty();
}
fn selection(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_data_device: &WlDataDevice,
) {
}
fn drop_performed(
&mut self,
conn: &Connection,
_qh: &QueueHandle<Self>,
_data_device: &WlDataDevice,
) {
let Some(drag) = self.drag.take() else { return };
let pos = drag.pos;
let offer = match self.data_device.as_ref() {
Some(dd) => dd.data().drag_offer(),
None => None,
};
let Some(offer) = offer else {
self.dispatch(Event::Drop {
pos,
data: DragData::default(),
});
self.mark_popups_dirty();
return;
};
let read_pipe = match offer.receive(URI_LIST_MIME.to_string()) {
Ok(p) => p,
Err(_) => {
offer.finish();
offer.destroy();
self.dispatch(Event::Drop {
pos,
data: DragData::default(),
});
self.mark_popups_dirty();
return;
}
};
let _ = conn.flush();
let mut buf: Vec<u8> = Vec::new();
let inserted =
self.loop_handle
.insert_source(read_pipe, move |_event, file, state: &mut State| {
use std::io::Read;
let f: &mut std::fs::File = unsafe { file.get_mut() };
let mut chunk = [0u8; 4096];
match f.read(&mut chunk) {
Ok(0) => {
let text = String::from_utf8_lossy(&buf);
let paths = parse_uri_list(text.as_ref());
offer.finish();
offer.destroy();
state.dispatch(Event::Drop {
pos,
data: DragData::from_paths(paths),
});
state.mark_popups_dirty();
PostAction::Remove
}
Ok(n) => {
buf.extend_from_slice(&chunk[..n]);
PostAction::Continue
}
Err(ref e)
if matches!(
e.kind(),
std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
) =>
{
PostAction::Continue
}
Err(_) => {
offer.finish();
offer.destroy();
PostAction::Remove
}
}
});
if inserted.is_err() {
self.dispatch(Event::Drop {
pos,
data: DragData::default(),
});
self.mark_popups_dirty();
}
}
}
impl DataOfferHandler for State {
fn source_actions(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
offer: &mut DragOffer,
_actions: DndAction,
) {
offer.set_actions(DndAction::Copy, DndAction::Copy);
}
fn selected_action(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_offer: &mut DragOffer,
_actions: DndAction,
) {
}
}
impl DataSourceHandler for State {
fn accept_mime(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
source: &WlDataSource,
mime: Option<String>,
) {
if self
.drag_source
.as_ref()
.is_some_and(|s| s.inner() == source)
{
self.update_drag_feedback(if mime.is_some() {
DragFeedback::Copy
} else {
DragFeedback::NoDrop
});
}
}
fn send_request(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
source: &WlDataSource,
mime: String,
write_pipe: WritePipe,
) {
let ours = self
.drag_source
.as_ref()
.is_some_and(|s| s.inner() == source);
if !ours || mime != URI_LIST_MIME {
return;
}
use std::io::Write;
use std::os::fd::OwnedFd;
let mut file = std::fs::File::from(OwnedFd::from(write_pipe));
let _ = file.write_all(&self.drag_payload);
}
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, source: &WlDataSource) {
self.end_drag_source(source);
}
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
}
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, source: &WlDataSource) {
self.end_drag_source(source);
}
fn action(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
source: &WlDataSource,
action: DndAction,
) {
if !self
.drag_source
.as_ref()
.is_some_and(|s| s.inner() == source)
{
return;
}
let valid = action.contains(DndAction::Copy) || action.contains(DndAction::Move);
self.update_drag_feedback(if valid {
DragFeedback::Copy
} else {
DragFeedback::NoDrop
});
let shape = if action.contains(DndAction::Move) {
Shape::Move
} else if valid {
Shape::Copy
} else {
Shape::NoDrop
};
self.set_drag_cursor(shape);
}
}
impl ShmHandler for State {
fn shm_state(&mut self) -> &mut Shm {
&mut self.shm
}
}
impl ProvidesRegistryState for State {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
delegate_compositor!(State);
delegate_output!(State);
delegate_shm!(State);
delegate_seat!(State);
delegate_data_device!(State);
delegate_keyboard!(State);
delegate_pointer!(State);
delegate_xdg_shell!(State);
delegate_xdg_window!(State);
delegate_xdg_popup!(State);
delegate_registry!(State);
fn sanitize(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'-'
}
})
.collect()
}
const URI_LIST_MIME: &str = "text/uri-list";
fn parse_uri_list(text: &str) -> Vec<PathBuf> {
text.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(file_uri_to_path)
.collect()
}
fn file_uri_to_path(uri: &str) -> Option<PathBuf> {
let rest = uri.strip_prefix("file://")?;
let path = &rest[rest.find('/')?..];
Some(PathBuf::from(percent_decode(path)))
}
fn percent_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hi = (bytes[i + 1] as char).to_digit(16);
let lo = (bytes[i + 2] as char).to_digit(16);
if let (Some(h), Some(l)) = (hi, lo) {
out.push((h * 16 + l) as u8);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn paths_to_uri_list(paths: &[PathBuf]) -> String {
use std::os::unix::ffi::OsStrExt;
let mut out = String::new();
for path in paths {
if !path.is_absolute() {
continue;
}
out.push_str("file://");
out.push_str(&percent_encode_path(path.as_os_str().as_bytes()));
out.push_str("\r\n");
}
out
}
fn percent_encode_path(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(bytes.len());
for &b in bytes {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
out.push(b as char);
} else {
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
}
out
}
const DRAG_ICON_PAD: i32 = 6;
const DRAG_ICON_GAP: i32 = 6;
const DRAG_ICON_BADGE_MIN: i32 = 12;
fn draw_drag_badge(painter: &mut Painter, area: Rect, feedback: DragFeedback) {
let (bg, accept) = match feedback {
DragFeedback::Copy => (Color::GREEN, true),
DragFeedback::NoDrop => (Color::RED, false),
};
painter.fill_rect(area, bg);
painter.physical(area, |p, r| {
let stroke = (r.w / 7).max(2);
let at = |fx: f32, fy: f32| -> (i32, i32) {
(
r.x + (r.w as f32 * fx).round() as i32,
r.y + (r.h as f32 * fy).round() as i32,
)
};
if accept {
let (ax, ay) = at(0.20, 0.52);
let (bx, by) = at(0.42, 0.72);
let (cx, cy) = at(0.80, 0.26);
draw_thick_line(p, ax, ay, bx, by, stroke, Color::WHITE);
draw_thick_line(p, bx, by, cx, cy, stroke, Color::WHITE);
} else {
let (ax, ay) = at(0.27, 0.27);
let (bx, by) = at(0.73, 0.73);
let (cx, cy) = at(0.73, 0.27);
let (dx, dy) = at(0.27, 0.73);
draw_thick_line(p, ax, ay, bx, by, stroke, Color::WHITE);
draw_thick_line(p, cx, cy, dx, dy, stroke, Color::WHITE);
}
});
}
fn draw_thick_line(
painter: &mut Painter,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
thickness: i32,
color: Color,
) {
let steps = (x1 - x0).abs().max((y1 - y0).abs()).max(1);
let half = thickness / 2;
for i in 0..=steps {
let t = i as f32 / steps as f32;
let x = x0 + ((x1 - x0) as f32 * t).round() as i32;
let y = y0 + ((y1 - y0) as f32 * t).round() as i32;
painter.fill_rect(Rect::new(x - half, y - half, thickness, thickness), color);
}
}
fn drag_icon_label(paths: &[PathBuf]) -> String {
match paths {
[] => String::new(),
[one] => one
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| one.display().to_string()),
many => format!("{} items", many.len()),
}
}
fn map_button(button: u32) -> Option<MouseButton> {
match button {
0x110 => Some(MouseButton::Left),
0x111 => Some(MouseButton::Right),
0x112 => Some(MouseButton::Middle),
_ => None,
}
}
fn map_keysym(keysym: Keysym) -> Option<Key> {
use Keysym as K;
let named = match keysym {
K::Return | K::KP_Enter => NamedKey::Enter,
K::BackSpace => NamedKey::Backspace,
K::Delete | K::KP_Delete => NamedKey::Delete,
K::Tab | K::ISO_Left_Tab => NamedKey::Tab,
K::Escape => NamedKey::Escape,
K::space => NamedKey::Space,
K::Left | K::KP_Left => NamedKey::Left,
K::Right | K::KP_Right => NamedKey::Right,
K::Up | K::KP_Up => NamedKey::Up,
K::Down | K::KP_Down => NamedKey::Down,
K::Home | K::KP_Home => NamedKey::Home,
K::End | K::KP_End => NamedKey::End,
K::Page_Up | K::KP_Page_Up => NamedKey::PageUp,
K::Page_Down | K::KP_Page_Down => NamedKey::PageDown,
_ => {
let ch = keysym.key_char()?;
return Some(Key::Char(ch));
}
};
Some(Key::Named(named))
}
fn bytes_as_u32_mut(bytes: &mut [u8]) -> &mut [u32] {
let len = bytes.len() / 4;
unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut u32, len) }
}
impl Dispatch<XdgPositioner, ()> for State {
fn event(
_state: &mut Self,
_proxy: &XdgPositioner,
_event: <XdgPositioner as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<XdgWmDialogV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &XdgWmDialogV1,
_event: <XdgWmDialogV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<XdgDialogV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &XdgDialogV1,
_event: <XdgDialogV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WpCursorShapeManagerV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpCursorShapeManagerV1,
_event: <WpCursorShapeManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WpCursorShapeDeviceV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpCursorShapeDeviceV1,
_event: <WpCursorShapeDeviceV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WpFractionalScaleManagerV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &WpFractionalScaleManagerV1,
_event: <WpFractionalScaleManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WpFractionalScaleV1, ()> for State {
fn event(
state: &mut Self,
_proxy: &WpFractionalScaleV1,
event: wp_fractional_scale_v1::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event {
let f = scale as f32 / 120.0;
if state.fractional_scale != Some(f) {
state.fractional_scale = Some(f);
state.needs_redraw = true;
state.mark_popups_dirty();
}
}
}
}
#[allow(dead_code)]
fn _unused(_b: Buffer, _arc: Arc<()>) {}
#[cfg(test)]
mod tests {
use super::{
file_uri_to_path, parse_uri_list, paths_to_uri_list, percent_decode, percent_encode_path,
};
use std::path::PathBuf;
#[test]
fn parses_a_uri_list_into_paths() {
let payload = "#comment\r\nfile:///home/rob/a.txt\r\nfile:///tmp/b.png\r\n";
assert_eq!(
parse_uri_list(payload),
vec![
PathBuf::from("/home/rob/a.txt"),
PathBuf::from("/tmp/b.png"),
]
);
}
#[test]
fn percent_escapes_are_decoded() {
assert_eq!(
file_uri_to_path("file:///tmp/a%20b%2Bc.txt"),
Some(PathBuf::from("/tmp/a b+c.txt"))
);
assert_eq!(percent_decode("%41%42"), "AB");
assert_eq!(percent_decode("100%"), "100%");
}
#[test]
fn drops_authority_and_non_file_uris() {
assert_eq!(
file_uri_to_path("file://localhost/etc/hosts"),
Some(PathBuf::from("/etc/hosts"))
);
assert_eq!(file_uri_to_path("https://example.com/x"), None);
assert!(parse_uri_list("https://example.com/x\n\n").is_empty());
}
#[test]
fn serializes_paths_to_a_crlf_uri_list() {
let list = paths_to_uri_list(&[
PathBuf::from("/home/rob/a.txt"),
PathBuf::from("/tmp/a b+c.txt"),
]);
assert_eq!(
list,
"file:///home/rob/a.txt\r\nfile:///tmp/a%20b%2Bc.txt\r\n"
);
}
#[test]
fn skips_relative_paths_that_cant_be_file_uris() {
assert_eq!(paths_to_uri_list(&[PathBuf::from("relative/x")]), "");
}
#[test]
fn encoding_round_trips_through_the_parser() {
let paths = vec![
PathBuf::from("/tmp/plain.txt"),
PathBuf::from("/tmp/with space & symbols#1.txt"),
PathBuf::from("/home/rob/Bilder/Größe.png"),
];
assert_eq!(parse_uri_list(&paths_to_uri_list(&paths)), paths);
}
#[test]
fn keeps_unreserved_and_slash_but_escapes_the_rest() {
assert_eq!(percent_encode_path(b"/a-b_c.d~e/f"), "/a-b_c.d~e/f");
assert_eq!(percent_encode_path(b"a b"), "a%20b");
assert_eq!(percent_encode_path(b"#?%"), "%23%3F%25");
}
}