ltk/platform/wayland/
primary_selection.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! `zwp_primary_selection_device_manager_v1`: the selection the middle
//! button pastes, kept in step with other applications as the clipboard
//! is in [`super::data_device`].

use std::sync::mpsc;

use smithay_client_toolkit::data_device_manager::WritePipe;
use smithay_client_toolkit::primary_selection::PrimarySelectionManagerState;
use smithay_client_toolkit::primary_selection::device::{ PrimarySelectionDevice, PrimarySelectionDeviceData, PrimarySelectionDeviceHandler };
use smithay_client_toolkit::primary_selection::selection::{ PrimarySelectionSource, PrimarySelectionSourceHandler };
use smithay_client_toolkit::reexports::client::
{
	protocol::wl_seat::WlSeat,
	Connection, Proxy, QueueHandle,
};
use smithay_client_toolkit::reexports::protocols::wp::primary_selection::zv1::client::
{
	zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
	zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1,
};

use crate::app::App;
use crate::event_loop::AppData;
use super::WaylandPlatform;
use super::data_device::{ pick_text_mime, read_selection, write_selection, CLIPBOARD_MIMES };

pub( crate ) struct PrimarySelection
{
	/// `None` when the compositor does not offer primary selections.
	manager:  Option<PrimarySelectionManagerState>,
	device:   Option<PrimarySelectionDevice>,
	/// Ours while we own the selection; dropping it gives the selection up.
	source:   Option<PrimarySelectionSource>,
	inbox_tx: mpsc::Sender<String>,
	inbox_rx: mpsc::Receiver<String>,
	/// The reader threads started and not yet heard from.
	reading:  usize,
}

impl PrimarySelection
{
	pub( crate ) fn new( manager: Option<PrimarySelectionManagerState> ) -> Self
	{
		let ( inbox_tx, inbox_rx ) = mpsc::channel();
		Self { manager, device: None, source: None, inbox_tx, inbox_rx, reading: 0 }
	}

	/// The latest selection the reader threads brought from other applications, if any arrived.
	pub( crate ) fn drain( &mut self ) -> Option<String>
	{
		let arrived: Vec<String> = self.inbox_rx.try_iter().collect();
		self.reading = self.reading.saturating_sub( arrived.len() );
		arrived.into_iter().last()
	}

	/// Whether a selection another application offered is still being read.
	pub( crate ) fn pending( &self ) -> bool
	{
		self.reading > 0
	}
}

/// Offer the primary selection to other applications; `AppData::primary_selection` holds its text.
pub( crate ) fn publish<A: App>( platform: &mut WaylandPlatform<A> )
{
	let ( Some( manager ), Some( device ) ) = ( &platform.primary.manager, &platform.primary.device ) else { return };
	let source = manager.create_selection_source( &platform.qh, CLIPBOARD_MIMES.iter().copied() );
	source.set_selection( device, platform.last_input_serial );
	platform.primary.source = Some( source );
}

impl<A: App> AppData<A>
{
	pub( crate ) fn bind_primary_selection( &mut self, qh: &QueueHandle<Self>, seat: &WlSeat )
	{
		let primary = &mut self.platform.primary;
		if primary.device.is_some() { return; }
		let Some( manager ) = &primary.manager else { return };
		primary.device = Some( manager.get_selection_device( qh, seat ) );
	}

	fn owns_primary( &self, source: &ZwpPrimarySelectionSourceV1 ) -> bool
	{
		self.platform.primary.source.as_ref().is_some_and( |own| own.inner() == source )
	}
}

impl<A: App> PrimarySelectionDeviceHandler for AppData<A>
{
	fn selection( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, device: &ZwpPrimarySelectionDeviceV1 )
	{
		let Some( offer ) = device.data::<PrimarySelectionDeviceData>().and_then( |data| data.selection_offer() ) else { return };
		let Some( mime )  = offer.with_mime_types( pick_text_mime ) else { return };
		let Ok( pipe )    = offer.receive( mime ) else { return };
		read_selection( pipe, self.platform.primary.inbox_tx.clone() );
		self.platform.primary.reading += 1;
	}
}

impl<A: App> PrimarySelectionSourceHandler for AppData<A>
{
	fn send_request( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, source: &ZwpPrimarySelectionSourceV1, _mime: String, pipe: WritePipe )
	{
		if self.owns_primary( source ) { write_selection( pipe, &self.primary_selection ); }
	}

	fn cancelled( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, source: &ZwpPrimarySelectionSourceV1 )
	{
		if self.owns_primary( source ) { self.platform.primary.source = None; }
	}
}