The primary selection is kept in step with other applications, an application can paste either selection and hear the middle button, raw keys carry the text dead keys compose, and an application can ask for a cursor where the pointer is
A terminal drawn by the application itself needed four things ltk did not give it: the selection the middle button pastes, a way to read the clipboard, the text a key types once dead keys are applied, and a text cursor over its text area that does not hide the window's resize cursors. Text fields gain the primary selection along the way, and the symbol fallback and a missed field in the previous commit are fixed. The primary selection. `event_loop/primary_selection.rs` binds `zwp_primary_selection_device_manager_v1` when the compositor offers it, creates the device with the data device in `new_seat`, and holds the manager, the device, our source while we own the selection, its text and the inbox the reader threads post to. A selection offered by another client is read on a thread of its own as the clipboard is; a peer asking for ours is served from the cached text, and a cancelled source is dropped. `data_device.rs` gives both the pieces they share: `pick_text_mime`, `read_selection` and `write_selection`, so the clipboard behaves as before. Text fields. Once a press ends, `sync_text_field` publishes a new selection in the focused text field as the primary selection; password fields never publish theirs. It remembers what it last published rather than comparing with the current primary selection, so a selection made in another application is not taken back. A middle-button press on a text field focuses it, puts the cursor where it landed and inserts the primary selection there. The application's side. `App::take_primary_request` makes a text the primary selection, as `take_clipboard_request` does for the clipboard. `App::take_paste_request` names a `SelectionKind`, `Clipboard` or `Primary`, and the runtime answers with `App::on_paste` and that selection's text, never with an empty one. `App::on_pointer_middle` reports a middle-button press anywhere but on a text field. All default to doing nothing. Raw keys. `App::on_raw_key_with_alt` gives way to `App::on_raw_key_event`, which takes a `RawKey` with the keysym, the keycode, the press, Ctrl, Shift and Alt, and `text`: what sctk reports the key types once dead keys and compose sequences are applied, `None` on release, halfway through a sequence and for keys that type nothing. A terminal that took the character from the keysym typed `a` for an acute accent followed by `a`. The default still forwards to `on_raw_key`, and prompt was the only application overriding the old hook. Every key press now also records its serial in `last_input_serial`, so a selection set from a keyboard shortcut carries a serial a strict compositor accepts rather than the last click's. The cursor. `App::cursor_at( x, y )` is the cursor the application wants at a point of the main surface. It comes after the resize edges and `cursor_override`, and before the cursor of the widget under the pointer, so a terminal shows the text cursor over its cells and the resize cursors still show at the window's edges, which a `cursor_override` would hide. Fonts. Noto Sans Symbols 2 and Noto Sans Symbols follow DejaVu Sans in the fallback chain, for the media controls, arrows and shapes DejaVu lacks, which drew as the missing glyph. `TextEdit::map_msg` now carries `pad_h` over: the previous commit added the field but left it out there, and ltk did not build.
This commit is contained in:
+42
-3
@@ -363,6 +363,30 @@ pub enum BackAtRoot
|
|||||||
Ignore,
|
Ignore,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A key press or release as [`App::on_raw_key_event`] reports it.
|
||||||
|
#[ derive( Debug, Clone ) ]
|
||||||
|
pub struct RawKey
|
||||||
|
{
|
||||||
|
pub keysym: Keysym,
|
||||||
|
pub keycode: u32,
|
||||||
|
pub pressed: bool,
|
||||||
|
pub ctrl: bool,
|
||||||
|
pub shift: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
/// What the key types once dead keys and compose sequences are applied; `None` on release, halfway through a sequence and for keys that type nothing.
|
||||||
|
pub text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which of the two selections an app reads, through [`App::take_paste_request`].
|
||||||
|
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
|
||||||
|
pub enum SelectionKind
|
||||||
|
{
|
||||||
|
/// What Copy put there, pasted with Ctrl+V.
|
||||||
|
Clipboard,
|
||||||
|
/// The text last selected, pasted with the middle button.
|
||||||
|
Primary,
|
||||||
|
}
|
||||||
|
|
||||||
/// Trait that application types must implement to integrate with ltk.
|
/// Trait that application types must implement to integrate with ltk.
|
||||||
pub trait App: 'static
|
pub trait App: 'static
|
||||||
{
|
{
|
||||||
@@ -882,6 +906,15 @@ pub trait App: 'static
|
|||||||
/// Return `Some( text )` once to put it in the clipboard, for other applications as well.
|
/// Return `Some( text )` once to put it in the clipboard, for other applications as well.
|
||||||
fn take_clipboard_request( &mut self ) -> Option<String> { None }
|
fn take_clipboard_request( &mut self ) -> Option<String> { None }
|
||||||
|
|
||||||
|
/// Return `Some( text )` once to make it the primary selection, which the middle button pastes, here and in other applications.
|
||||||
|
fn take_primary_request( &mut self ) -> Option<String> { None }
|
||||||
|
|
||||||
|
/// Return `Some( kind )` once to have [`Self::on_paste`] called with the text that selection holds.
|
||||||
|
fn take_paste_request( &mut self ) -> Option<SelectionKind> { None }
|
||||||
|
|
||||||
|
/// The text of the selection [`Self::take_paste_request`] asked for; not called when it is empty.
|
||||||
|
fn on_paste( &mut self, _text: &str ) {}
|
||||||
|
|
||||||
/// Called on every pointer motion (mouse move, button press, button
|
/// Called on every pointer motion (mouse move, button press, button
|
||||||
/// release) and on every primary-finger touch down and motion. `x`, `y` are in physical pixels of the main surface,
|
/// release) and on every primary-finger touch down and motion. `x`, `y` are in physical pixels of the main surface,
|
||||||
/// the same coordinate space as widget rects. Default: no-op.
|
/// the same coordinate space as widget rects. Default: no-op.
|
||||||
@@ -982,6 +1015,9 @@ pub trait App: 'static
|
|||||||
/// A secondary-button press that no widget of ltk's own answered, in physical pixels. Default: no-op.
|
/// A secondary-button press that no widget of ltk's own answered, in physical pixels. Default: no-op.
|
||||||
fn on_pointer_secondary( &mut self, _x: f32, _y: f32 ) {}
|
fn on_pointer_secondary( &mut self, _x: f32, _y: f32 ) {}
|
||||||
|
|
||||||
|
/// A middle-button press, in physical pixels. Default: no-op.
|
||||||
|
fn on_pointer_middle( &mut self, _x: f32, _y: f32 ) {}
|
||||||
|
|
||||||
/// Called on every keyboard event (press *and* release) before the
|
/// Called on every keyboard event (press *and* release) before the
|
||||||
/// regular focus-aware dispatch ([`Self::on_key`] /
|
/// regular focus-aware dispatch ([`Self::on_key`] /
|
||||||
/// [`Self::on_key_with_modifiers`]) runs. Default: no-op.
|
/// [`Self::on_key_with_modifiers`]) runs. Default: no-op.
|
||||||
@@ -1001,10 +1037,10 @@ pub trait App: 'static
|
|||||||
/// input for the embedded system, and goes through [`Self::on_back`].
|
/// input for the embedded system, and goes through [`Self::on_back`].
|
||||||
fn on_raw_key( &mut self, _keysym: Keysym, _keycode: u32, _pressed: bool, _ctrl: bool, _shift: bool ) {}
|
fn on_raw_key( &mut self, _keysym: Keysym, _keycode: u32, _pressed: bool, _ctrl: bool, _shift: bool ) {}
|
||||||
|
|
||||||
/// [`Self::on_raw_key`] with the Alt state too; the runtime calls this one, which forwards to `on_raw_key` unless overridden. Override it instead when Alt matters, as in a terminal.
|
/// [`Self::on_raw_key`] with the Alt state and the typed text too; the runtime calls this one, which forwards to `on_raw_key` unless overridden. Override it instead when Alt or dead keys matter, as in a terminal.
|
||||||
fn on_raw_key_with_alt( &mut self, keysym: Keysym, keycode: u32, pressed: bool, ctrl: bool, shift: bool, _alt: bool )
|
fn on_raw_key_event( &mut self, key: &RawKey )
|
||||||
{
|
{
|
||||||
self.on_raw_key( keysym, keycode, pressed, ctrl, shift );
|
self.on_raw_key( key.keysym, key.keycode, key.pressed, key.ctrl, key.shift );
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `true` while every key belongs to [`Self::on_raw_key`], as in a terminal: ltk then neither dispatches keys to its widgets, so Enter and Space do not press the focused or hovered one, nor repeats them. `XF86_Back` still goes through [`Self::on_back`].
|
/// `true` while every key belongs to [`Self::on_raw_key`], as in a terminal: ltk then neither dispatches keys to its widgets, so Enter and Space do not press the focused or hovered one, nor repeats them. `XF86_Back` still goes through [`Self::on_back`].
|
||||||
@@ -1071,6 +1107,9 @@ pub trait App: 'static
|
|||||||
/// next iteration without any extra wiring.
|
/// next iteration without any extra wiring.
|
||||||
fn cursor_override( &self ) -> Option<crate::types::CursorShape> { None }
|
fn cursor_override( &self ) -> Option<crate::types::CursorShape> { None }
|
||||||
|
|
||||||
|
/// The cursor the app wants at `( x, y )`, in physical pixels of the main surface: below the window's resize edges and [`Self::cursor_override`], above the cursor of the widget under it. `None`, the default, leaves the widget's.
|
||||||
|
fn cursor_at( &self, _x: f32, _y: f32 ) -> Option<crate::types::CursorShape> { None }
|
||||||
|
|
||||||
/// Decide whether a given key participates in held-key repeat.
|
/// Decide whether a given key participates in held-key repeat.
|
||||||
///
|
///
|
||||||
/// Default: every non-modifier key repeats *except* `Escape`,
|
/// Default: every non-modifier key repeats *except* `Escape`,
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ pub struct AppData<A: App>
|
|||||||
pub clipboard_inbox_tx: std::sync::mpsc::Sender<String>,
|
pub clipboard_inbox_tx: std::sync::mpsc::Sender<String>,
|
||||||
/// Receiver half — drained once per run loop iteration.
|
/// Receiver half — drained once per run loop iteration.
|
||||||
pub clipboard_inbox_rx: std::sync::mpsc::Receiver<String>,
|
pub clipboard_inbox_rx: std::sync::mpsc::Receiver<String>,
|
||||||
|
pub primary: super::primary_selection::PrimarySelection,
|
||||||
|
|
||||||
/// Cross-application drag-and-drop: best mime negotiated on
|
/// Cross-application drag-and-drop: best mime negotiated on
|
||||||
/// `enter` and the last pointer position (in logical surface
|
/// `enter` and the last pointer position (in logical surface
|
||||||
|
|||||||
@@ -106,9 +106,10 @@ impl<A: App> AppData<A>
|
|||||||
/// state, wins over everything.
|
/// state, wins over everything.
|
||||||
/// 2. Active slider drag — `Grabbing` while the gesture machine
|
/// 2. Active slider drag — `Grabbing` while the gesture machine
|
||||||
/// holds a `dragging_slider`.
|
/// holds a `dragging_slider`.
|
||||||
/// 3. Cursor declared by the widget under the pointer
|
/// 3. [`crate::App::cursor_at`] for the pointer's position on the main surface.
|
||||||
|
/// 4. Cursor declared by the widget under the pointer
|
||||||
/// ([`crate::widget::LaidOutWidget::cursor`]).
|
/// ([`crate::widget::LaidOutWidget::cursor`]).
|
||||||
/// 4. `Default` — pointer is over surface chrome / empty space.
|
/// 5. `Default` — pointer is over surface chrome / empty space.
|
||||||
pub( crate ) fn dispatch_cursor_shape( &mut self, focus: SurfaceFocus )
|
pub( crate ) fn dispatch_cursor_shape( &mut self, focus: SurfaceFocus )
|
||||||
{
|
{
|
||||||
let target = {
|
let target = {
|
||||||
@@ -119,6 +120,9 @@ impl<A: App> AppData<A>
|
|||||||
.map( |w| w.cursor )
|
.map( |w| w.cursor )
|
||||||
.unwrap_or( CursorShape::Default );
|
.unwrap_or( CursorShape::Default );
|
||||||
let resize_cursor = self.resize_edge_under_pointer( focus ).map( resize_edge_to_cursor );
|
let resize_cursor = self.resize_edge_under_pointer( focus ).map( resize_edge_to_cursor );
|
||||||
|
let area_cursor = matches!( focus, SurfaceFocus::Main )
|
||||||
|
.then( || self.app.cursor_at( self.pointer_pos.x, self.pointer_pos.y ) )
|
||||||
|
.flatten();
|
||||||
|
|
||||||
// Resize-edge hover wins over the widget cursor and over a
|
// Resize-edge hover wins over the widget cursor and over a
|
||||||
// `Default` app override, but loses to a non-default app
|
// `Default` app override, but loses to a non-default app
|
||||||
@@ -133,7 +137,7 @@ impl<A: App> AppData<A>
|
|||||||
} else if dragging {
|
} else if dragging {
|
||||||
CursorShape::Grabbing
|
CursorShape::Grabbing
|
||||||
} else {
|
} else {
|
||||||
hover_cursor
|
area_cursor.unwrap_or( hover_cursor )
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Skip when the compositor already shows the right shape.
|
// Skip when the compositor already shows the right shape.
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ use std::io::Read;
|
|||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use smithay_client_toolkit::data_device_manager::data_device::{ DataDeviceData, DataDeviceHandler };
|
use smithay_client_toolkit::data_device_manager::data_device::{ DataDeviceData, DataDeviceHandler };
|
||||||
use smithay_client_toolkit::data_device_manager::data_offer::{ DataOfferHandler, DragOffer, SelectionOffer };
|
use smithay_client_toolkit::data_device_manager::data_offer::{ DataOfferHandler, DragOffer };
|
||||||
use smithay_client_toolkit::data_device_manager::data_source::DataSourceHandler;
|
use smithay_client_toolkit::data_device_manager::data_source::DataSourceHandler;
|
||||||
|
use smithay_client_toolkit::data_device_manager::{ ReadPipe, WritePipe };
|
||||||
use smithay_client_toolkit::reexports::client::
|
use smithay_client_toolkit::reexports::client::
|
||||||
{
|
{
|
||||||
protocol::
|
protocol::
|
||||||
@@ -167,9 +168,15 @@ impl<A: App> DataDeviceHandler for AppData<A>
|
|||||||
{
|
{
|
||||||
let Some( data ) = data_device.data::<DataDeviceData>() else { return };
|
let Some( data ) = data_device.data::<DataDeviceData>() else { return };
|
||||||
let Some( offer ) = data.selection_offer() else { return };
|
let Some( offer ) = data.selection_offer() else { return };
|
||||||
let Some( mime ) = pick_mime( &offer ) else { return };
|
let Some( mime ) = offer.with_mime_types( pick_text_mime ) else { return };
|
||||||
let Ok( pipe ) = offer.receive( mime ) else { return };
|
let Ok( pipe ) = offer.receive( mime ) else { return };
|
||||||
let tx = self.clipboard_inbox_tx.clone();
|
read_selection( pipe, self.clipboard_inbox_tx.clone() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain an offered selection on a thread of its own and post the text to `tx`.
|
||||||
|
pub( super ) fn read_selection( pipe: ReadPipe, tx: mpsc::Sender<String> )
|
||||||
|
{
|
||||||
std::thread::spawn( move ||
|
std::thread::spawn( move ||
|
||||||
{
|
{
|
||||||
let mut reader = std::fs::File::from( std::os::fd::OwnedFd::from( pipe ) );
|
let mut reader = std::fs::File::from( std::os::fd::OwnedFd::from( pipe ) );
|
||||||
@@ -181,7 +188,18 @@ impl<A: App> DataDeviceHandler for AppData<A>
|
|||||||
let _ = bounded.read_to_string( &mut buf );
|
let _ = bounded.read_to_string( &mut buf );
|
||||||
let _ = tx.send( buf );
|
let _ = tx.send( buf );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serve `text` to the peer that asked for one of our selections.
|
||||||
|
pub( super ) fn write_selection( pipe: WritePipe, text: &str )
|
||||||
|
{
|
||||||
|
// SCTK's `WritePipe` derefs to `OwnedFd`; convert to a `File`
|
||||||
|
// (no buffering — selection payloads are small). Errors (peer
|
||||||
|
// closed early, broken pipe) are ignored: we cannot recover and
|
||||||
|
// the compositor will issue a fresh source on the next copy anyway.
|
||||||
|
use std::io::Write;
|
||||||
|
let mut writer = std::fs::File::from( std::os::fd::OwnedFd::from( pipe ) );
|
||||||
|
let _ = writer.write_all( text.as_bytes() );
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: App> DataOfferHandler for AppData<A>
|
impl<A: App> DataOfferHandler for AppData<A>
|
||||||
@@ -194,7 +212,7 @@ impl<A: App> DataSourceHandler for AppData<A>
|
|||||||
{
|
{
|
||||||
fn accept_mime( &mut self, _c: &Connection, _qh: &QueueHandle<Self>, _source: &WlDataSource, _mime: Option<String> ) {}
|
fn accept_mime( &mut self, _c: &Connection, _qh: &QueueHandle<Self>, _source: &WlDataSource, _mime: Option<String> ) {}
|
||||||
|
|
||||||
fn send_request( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, source: &WlDataSource, _mime: String, fd: smithay_client_toolkit::data_device_manager::WritePipe )
|
fn send_request( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, source: &WlDataSource, _mime: String, fd: WritePipe )
|
||||||
{
|
{
|
||||||
// Only honour requests for our own active source — if a stale
|
// Only honour requests for our own active source — if a stale
|
||||||
// source is still hanging around (e.g. between a new copy and
|
// source is still hanging around (e.g. between a new copy and
|
||||||
@@ -205,14 +223,7 @@ impl<A: App> DataSourceHandler for AppData<A>
|
|||||||
.map( |s| s.inner() == source )
|
.map( |s| s.inner() == source )
|
||||||
.unwrap_or( false );
|
.unwrap_or( false );
|
||||||
if !is_active { return; }
|
if !is_active { return; }
|
||||||
// SCTK's `WritePipe` derefs to `OwnedFd`; convert to a `File`
|
write_selection( fd, &self.clipboard );
|
||||||
// (no buffering — selection payloads are small) and write the
|
|
||||||
// cached clipboard string. Errors (peer closed early, broken
|
|
||||||
// pipe) are ignored: we cannot recover and the compositor
|
|
||||||
// will issue a fresh source on the next copy anyway.
|
|
||||||
use std::io::Write;
|
|
||||||
let mut writer = std::fs::File::from( std::os::fd::OwnedFd::from( fd ) );
|
|
||||||
let _ = writer.write_all( self.clipboard.as_bytes() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancelled( &mut self, _c: &Connection, _qh: &QueueHandle<Self>, source: &WlDataSource )
|
fn cancelled( &mut self, _c: &Connection, _qh: &QueueHandle<Self>, source: &WlDataSource )
|
||||||
@@ -231,12 +242,11 @@ impl<A: App> DataSourceHandler for AppData<A>
|
|||||||
fn action( &mut self, _c: &Connection, _qh: &QueueHandle<Self>, _source: &WlDataSource, _action: DndAction ) {}
|
fn action( &mut self, _c: &Connection, _qh: &QueueHandle<Self>, _source: &WlDataSource, _action: DndAction ) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick the best-supported MIME type from a selection offer. Order
|
/// Pick the best-supported MIME type from those a selection offers.
|
||||||
/// matches `CLIPBOARD_MIMES` — UTF-8 first, then plain text, then
|
/// Order matches `CLIPBOARD_MIMES` — UTF-8 first, then plain text,
|
||||||
/// the legacy `UTF8_STRING`.
|
/// then the legacy `UTF8_STRING`.
|
||||||
fn pick_mime( offer: &SelectionOffer ) -> Option<String>
|
pub( super ) fn pick_text_mime( mimes: &[ String ] ) -> Option<String>
|
||||||
{
|
{
|
||||||
let mimes = offer.with_mime_types( |m| m.to_vec() );
|
|
||||||
CLIPBOARD_MIMES.iter()
|
CLIPBOARD_MIMES.iter()
|
||||||
.find( |c| mimes.iter().any( |m| m == *c ) )
|
.find( |c| mimes.iter().any( |m| m == *c ) )
|
||||||
.map( |s| s.to_string() )
|
.map( |s| s.to_string() )
|
||||||
|
|||||||
@@ -442,6 +442,7 @@ impl<A: App> SeatHandler for AppData<A>
|
|||||||
self.data_device = Some( ddm.get_data_device( qh, &seat ) );
|
self.data_device = Some( ddm.get_data_device( qh, &seat ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.bind_primary_selection( qh, &seat );
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_capability(
|
fn new_capability(
|
||||||
@@ -649,6 +650,7 @@ delegate_foreign_toplevel_list!( @<A: App> AppData<A> );
|
|||||||
delegate_registry!( @<A: App> AppData<A> );
|
delegate_registry!( @<A: App> AppData<A> );
|
||||||
smithay_client_toolkit::delegate_activation!( @<A: App> AppData<A> );
|
smithay_client_toolkit::delegate_activation!( @<A: App> AppData<A> );
|
||||||
smithay_client_toolkit::delegate_data_device!( @<A: App> AppData<A> );
|
smithay_client_toolkit::delegate_data_device!( @<A: App> AppData<A> );
|
||||||
|
smithay_client_toolkit::delegate_primary_selection!( @<A: App> AppData<A> );
|
||||||
|
|
||||||
// --- `xdg-activation-v1` handler ---
|
// --- `xdg-activation-v1` handler ---
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod handlers;
|
|||||||
pub( crate ) mod osk;
|
pub( crate ) mod osk;
|
||||||
pub( crate ) mod output_probe;
|
pub( crate ) mod output_probe;
|
||||||
pub( crate ) mod perf;
|
pub( crate ) mod perf;
|
||||||
|
pub( crate ) mod primary_selection;
|
||||||
pub( crate ) mod repeat;
|
pub( crate ) mod repeat;
|
||||||
pub( crate ) mod session;
|
pub( crate ) mod session;
|
||||||
pub( crate ) mod subsurface;
|
pub( crate ) mod subsurface;
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// 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 super::app_data::AppData;
|
||||||
|
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>,
|
||||||
|
pub text: String,
|
||||||
|
/// The selection of the focused text field when it was last looked at.
|
||||||
|
field: Option<String>,
|
||||||
|
inbox_tx: mpsc::Sender<String>,
|
||||||
|
inbox_rx: mpsc::Receiver<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimarySelection
|
||||||
|
{
|
||||||
|
pub( crate ) fn new( manager: Option<PrimarySelectionManagerState> ) -> Self
|
||||||
|
{
|
||||||
|
let ( inbox_tx, inbox_rx ) = mpsc::channel();
|
||||||
|
Self { manager, device: None, source: None, text: String::new(), field: None, inbox_tx, inbox_rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take what the reader threads brought from other applications.
|
||||||
|
pub( crate ) fn drain( &mut self )
|
||||||
|
{
|
||||||
|
while let Ok( text ) = self.inbox_rx.try_recv()
|
||||||
|
{
|
||||||
|
self.text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: App> AppData<A>
|
||||||
|
{
|
||||||
|
pub( crate ) fn bind_primary_selection( &mut self, qh: &QueueHandle<Self>, seat: &WlSeat )
|
||||||
|
{
|
||||||
|
if self.primary.device.is_some() { return; }
|
||||||
|
let Some( manager ) = &self.primary.manager else { return };
|
||||||
|
self.primary.device = Some( manager.get_selection_device( qh, seat ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make `text` the primary selection, for other applications as well when the compositor allows.
|
||||||
|
pub( crate ) fn publish_primary_selection( &mut self, text: String )
|
||||||
|
{
|
||||||
|
self.primary.text = text;
|
||||||
|
let ( Some( manager ), Some( device ) ) = ( &self.primary.manager, &self.primary.device ) else { return };
|
||||||
|
let source = manager.create_selection_source( &self.qh, CLIPBOARD_MIMES.iter().copied() );
|
||||||
|
source.set_selection( device, self.last_input_serial );
|
||||||
|
self.primary.source = Some( source );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a new selection in the focused text field once the press that makes it ends; a password field's never leaves it.
|
||||||
|
pub( crate ) fn sync_text_field( &mut self )
|
||||||
|
{
|
||||||
|
let focus = self.keyboard_focus;
|
||||||
|
if self.try_surface( focus ).is_none_or( |ss| ss.gesture.pressed_idx.is_some() ) { return; }
|
||||||
|
let secure = self.surface( focus ).focused_idx
|
||||||
|
.and_then( |idx| self.text_input_geometry( focus, idx ) )
|
||||||
|
.is_some_and( |( _, _, _, secure, _, _ )| secure );
|
||||||
|
let selected = if secure { None } else { self.focused_selection_text( focus ) };
|
||||||
|
if selected == self.primary.field { return; }
|
||||||
|
self.primary.field = selected.clone();
|
||||||
|
if let Some( text ) = selected { self.publish_primary_selection( text ); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn owns_primary( &self, source: &ZwpPrimarySelectionSourceV1 ) -> bool
|
||||||
|
{
|
||||||
|
self.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.primary.inbox_tx.clone() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.text ); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancelled( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, source: &ZwpPrimarySelectionSourceV1 )
|
||||||
|
{
|
||||||
|
if self.owns_primary( source ) { self.primary.source = None; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -249,6 +249,8 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
|
|||||||
let data_device_manager =
|
let data_device_manager =
|
||||||
smithay_client_toolkit::data_device_manager::DataDeviceManagerState::bind( &globals, &qh ).ok();
|
smithay_client_toolkit::data_device_manager::DataDeviceManagerState::bind( &globals, &qh ).ok();
|
||||||
let ( clipboard_inbox_tx, clipboard_inbox_rx ) = super::data_device::clipboard_inbox();
|
let ( clipboard_inbox_tx, clipboard_inbox_rx ) = super::data_device::clipboard_inbox();
|
||||||
|
let primary = super::primary_selection::PrimarySelection::new(
|
||||||
|
smithay_client_toolkit::primary_selection::PrimarySelectionManagerState::bind( &globals, &qh ).ok() );
|
||||||
let ( drop_inbox_tx, drop_inbox_rx ) = super::data_device::drop_inbox();
|
let ( drop_inbox_tx, drop_inbox_rx ) = super::data_device::drop_inbox();
|
||||||
// AT-SPI2 / accessibility. `try_new` returns `None` when the
|
// AT-SPI2 / accessibility. `try_new` returns `None` when the
|
||||||
// platform adapter cannot be created (no daemon on the bus,
|
// platform adapter cannot be created (no daemon on the bus,
|
||||||
@@ -330,6 +332,7 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
|
|||||||
clipboard_source: None,
|
clipboard_source: None,
|
||||||
clipboard_inbox_tx,
|
clipboard_inbox_tx,
|
||||||
clipboard_inbox_rx,
|
clipboard_inbox_rx,
|
||||||
|
primary,
|
||||||
drop_position: None,
|
drop_position: None,
|
||||||
drop_mime: None,
|
drop_mime: None,
|
||||||
drop_inbox_tx,
|
drop_inbox_tx,
|
||||||
@@ -574,6 +577,7 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
|
|||||||
{
|
{
|
||||||
data.clipboard = text;
|
data.clipboard = text;
|
||||||
}
|
}
|
||||||
|
data.primary.drain();
|
||||||
while let Ok( p ) = data.drop_inbox_rx.try_recv()
|
while let Ok( p ) = data.drop_inbox_rx.try_recv()
|
||||||
{
|
{
|
||||||
data.app.on_drop_received( p.x as f32, p.y as f32, &p.mime, &p.text );
|
data.app.on_drop_received( p.x as f32, p.y as f32, &p.mime, &p.text );
|
||||||
@@ -940,6 +944,20 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
|
|||||||
data.clipboard = text;
|
data.clipboard = text;
|
||||||
data.publish_clipboard_selection();
|
data.publish_clipboard_selection();
|
||||||
}
|
}
|
||||||
|
data.sync_text_field();
|
||||||
|
if let Some( text ) = data.app.take_primary_request()
|
||||||
|
{
|
||||||
|
data.publish_primary_selection( text );
|
||||||
|
}
|
||||||
|
if let Some( kind ) = data.app.take_paste_request()
|
||||||
|
{
|
||||||
|
let text = match kind
|
||||||
|
{
|
||||||
|
crate::app::SelectionKind::Clipboard => data.clipboard.clone(),
|
||||||
|
crate::app::SelectionKind::Primary => data.primary.text.clone(),
|
||||||
|
};
|
||||||
|
if !text.is_empty() { data.app.on_paste( &text ); }
|
||||||
|
}
|
||||||
|
|
||||||
// Focus the widget with the requested WidgetId if the app requests it.
|
// Focus the widget with the requested WidgetId if the app requests it.
|
||||||
// Walks main + every overlay because the targeted widget can live
|
// Walks main + every overlay because the targeted widget can live
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use smithay_client_toolkit::reexports::client::
|
|||||||
Connection, QueueHandle,
|
Connection, QueueHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::{ App, RawKey };
|
||||||
use crate::event_loop::{ AppData, SurfaceFocus };
|
use crate::event_loop::{ AppData, SurfaceFocus };
|
||||||
|
|
||||||
pub( super ) mod back;
|
pub( super ) mod back;
|
||||||
@@ -32,6 +32,23 @@ pub( super ) mod text_keys;
|
|||||||
pub( super ) mod shortcuts;
|
pub( super ) mod shortcuts;
|
||||||
pub( super ) mod nav;
|
pub( super ) mod nav;
|
||||||
|
|
||||||
|
impl<A: App> AppData<A>
|
||||||
|
{
|
||||||
|
fn raw_key( &self, event: &KeyEvent, pressed: bool ) -> RawKey
|
||||||
|
{
|
||||||
|
RawKey
|
||||||
|
{
|
||||||
|
keysym: event.keysym,
|
||||||
|
keycode: event.raw_code,
|
||||||
|
pressed,
|
||||||
|
ctrl: self.ctrl_pressed,
|
||||||
|
shift: self.shift_pressed,
|
||||||
|
alt: self.alt_pressed,
|
||||||
|
text: event.utf8.clone().filter( |text| pressed && !text.is_empty() ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<A: App> KeyboardHandler for AppData<A>
|
impl<A: App> KeyboardHandler for AppData<A>
|
||||||
{
|
{
|
||||||
fn enter(
|
fn enter(
|
||||||
@@ -71,17 +88,19 @@ impl<A: App> KeyboardHandler for AppData<A>
|
|||||||
_conn: &Connection,
|
_conn: &Connection,
|
||||||
_qh: &QueueHandle<Self>,
|
_qh: &QueueHandle<Self>,
|
||||||
_keyboard: &WlKeyboard,
|
_keyboard: &WlKeyboard,
|
||||||
_serial: u32,
|
serial: u32,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
self.last_input_serial = serial;
|
||||||
let focus = self.keyboard_focus;
|
let focus = self.keyboard_focus;
|
||||||
// Raw observer hook (e.g. forwarding to an embedded WPE view).
|
// Raw observer hook (e.g. forwarding to an embedded WPE view).
|
||||||
// Fires before the focus-aware dispatch. Back is a navigation
|
// Fires before the focus-aware dispatch. Back is a navigation
|
||||||
// request, not input for the embedded system.
|
// request, not input for the embedded system.
|
||||||
if event.keysym != Keysym::XF86_Back
|
if event.keysym != Keysym::XF86_Back
|
||||||
{
|
{
|
||||||
self.app.on_raw_key_with_alt( event.keysym, event.raw_code, true, self.ctrl_pressed, self.shift_pressed, self.alt_pressed );
|
let key = self.raw_key( &event, true );
|
||||||
|
self.app.on_raw_key_event( &key );
|
||||||
}
|
}
|
||||||
// A new press always cancels any in-flight repeat — the user
|
// A new press always cancels any in-flight repeat — the user
|
||||||
// has either released the previous key or is pressing a
|
// has either released the previous key or is pressing a
|
||||||
@@ -104,7 +123,8 @@ impl<A: App> KeyboardHandler for AppData<A>
|
|||||||
{
|
{
|
||||||
if event.keysym != Keysym::XF86_Back
|
if event.keysym != Keysym::XF86_Back
|
||||||
{
|
{
|
||||||
self.app.on_raw_key_with_alt( event.keysym, event.raw_code, false, self.ctrl_pressed, self.shift_pressed, self.alt_pressed );
|
let key = self.raw_key( &event, false );
|
||||||
|
self.app.on_raw_key_event( &key );
|
||||||
}
|
}
|
||||||
// Only stop if the released key matches the one currently
|
// Only stop if the released key matches the one currently
|
||||||
// repeating; releasing a non-repeating key (e.g. a shift that
|
// repeating; releasing a non-repeating key (e.g. a shift that
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ impl<A: App> PointerHandler for AppData<A>
|
|||||||
PointerEventKind::Motion { .. } => self.on_pointer_motion( conn, qh, pointer, event ),
|
PointerEventKind::Motion { .. } => self.on_pointer_motion( conn, qh, pointer, event ),
|
||||||
PointerEventKind::Press { button: 0x110, .. } => self.on_pointer_left_press( conn, qh, pointer, event ),
|
PointerEventKind::Press { button: 0x110, .. } => self.on_pointer_left_press( conn, qh, pointer, event ),
|
||||||
PointerEventKind::Press { button: 0x111, .. } => self.on_pointer_right_click( conn, qh, pointer, event ),
|
PointerEventKind::Press { button: 0x111, .. } => self.on_pointer_right_click( conn, qh, pointer, event ),
|
||||||
|
PointerEventKind::Press { button: 0x112, .. } => self.on_pointer_middle_press( qh, event ),
|
||||||
// BTN_SIDE / BTN_BACK: the mouse's back button.
|
// BTN_SIDE / BTN_BACK: the mouse's back button.
|
||||||
PointerEventKind::Press { button: 0x113 | 0x116, .. } => self.handle_back( focus, qh ),
|
PointerEventKind::Press { button: 0x113 | 0x116, .. } => self.handle_back( focus, qh ),
|
||||||
PointerEventKind::Release { button: 0x110, .. } => self.on_pointer_release( conn, qh, pointer, event ),
|
PointerEventKind::Release { button: 0x110, .. } => self.on_pointer_release( conn, qh, pointer, event ),
|
||||||
|
|||||||
@@ -70,6 +70,25 @@ impl<A: App> AppData<A>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paste the primary selection into the text field under the pointer, or tell the app.
|
||||||
|
pub( super ) fn on_pointer_middle_press( &mut self, qh: &QueueHandle<Self>, event: &PointerEvent )
|
||||||
|
{
|
||||||
|
let PointerEventKind::Press { serial, .. } = event.kind else { return };
|
||||||
|
self.last_pointer_serial = serial;
|
||||||
|
self.last_input_serial = serial;
|
||||||
|
let focus = self.focus_for_surface( &event.surface ).unwrap_or( SurfaceFocus::Main );
|
||||||
|
let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 );
|
||||||
|
self.pointer_pos = pos;
|
||||||
|
let rects = &self.surface( focus ).frame.widget_rects;
|
||||||
|
let field = find_widget_at( rects, pos ).filter( |&idx| find_handlers( rects, idx ).is_some_and( |h| h.is_text_input() ) );
|
||||||
|
let Some( idx ) = field else { return self.app.on_pointer_middle( pos.x, pos.y ) };
|
||||||
|
self.press_on_focused = self.surface( focus ).focused_idx == Some( idx );
|
||||||
|
if !self.press_on_focused { self.set_focus( focus, Some( idx ), qh ); }
|
||||||
|
self.handle_text_pointer_down( focus, idx, pos );
|
||||||
|
let text = self.primary.text.clone();
|
||||||
|
if !text.is_empty() { self.handle_text_insert( focus, &text ); }
|
||||||
|
}
|
||||||
|
|
||||||
pub( super ) fn on_pointer_left_press(
|
pub( super ) fn on_pointer_left_press(
|
||||||
&mut self,
|
&mut self,
|
||||||
_conn: &Connection,
|
_conn: &Connection,
|
||||||
|
|||||||
+1
-1
@@ -313,7 +313,7 @@ pub use app::
|
|||||||
{
|
{
|
||||||
Anchor, App, BackAtRoot, BackResponse, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec,
|
Anchor, App, BackAtRoot, BackResponse, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec,
|
||||||
SubsurfaceId, SubsurfaceSpec, SubsurfaceParent,
|
SubsurfaceId, SubsurfaceSpec, SubsurfaceParent,
|
||||||
InvalidationScope, SurfaceTarget, ToplevelEvent,
|
InvalidationScope, RawKey, SelectionKind, SurfaceTarget, ToplevelEvent,
|
||||||
};
|
};
|
||||||
pub use theme::
|
pub use theme::
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ const FALLBACK_FONT_CANDIDATES: &[ FallbackFontSpec ] =
|
|||||||
// symbol, arrow and box-drawing blocks. Ships almost everywhere.
|
// symbol, arrow and box-drawing blocks. Ships almost everywhere.
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", face: 0 },
|
FallbackFontSpec { path: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", face: 0 },
|
||||||
|
|
||||||
|
// Symbols DejaVu lacks: media controls, extra arrows and shapes.
|
||||||
|
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf", face: 0 },
|
||||||
|
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoSansSymbols-Regular.ttf", face: 0 },
|
||||||
|
|
||||||
// Colour emoji, flags included; its glyphs are bitmaps (FontFace::color_glyph).
|
// Colour emoji, flags included; its glyphs are bitmaps (FontFace::color_glyph).
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", face: 0 },
|
FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", face: 0 },
|
||||||
FallbackFontSpec { path: "/usr/share/fonts/noto/NotoColorEmoji.ttf", face: 0 },
|
FallbackFontSpec { path: "/usr/share/fonts/noto/NotoColorEmoji.ttf", face: 0 },
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ impl<Msg: Clone> TextEdit<Msg>
|
|||||||
align: self.align,
|
align: self.align,
|
||||||
borderless: self.borderless,
|
borderless: self.borderless,
|
||||||
fixed_width: self.fixed_width,
|
fixed_width: self.fixed_width,
|
||||||
|
pad_h: self.pad_h,
|
||||||
font_size: self.font_size,
|
font_size: self.font_size,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
select_on_focus: self.select_on_focus,
|
select_on_focus: self.select_on_focus,
|
||||||
|
|||||||
Reference in New Issue
Block a user