diff --git a/src/app.rs b/src/app.rs index de9ef48..c632374 100644 --- a/src/app.rs +++ b/src/app.rs @@ -363,6 +363,30 @@ pub enum BackAtRoot 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, +} + +/// 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. 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. fn take_clipboard_request( &mut self ) -> Option { 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 { None } + + /// Return `Some( kind )` once to have [`Self::on_paste`] called with the text that selection holds. + fn take_paste_request( &mut self ) -> Option { 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 /// 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. @@ -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. 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 /// regular focus-aware dispatch ([`Self::on_key`] / /// [`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`]. 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. - fn on_raw_key_with_alt( &mut self, keysym: Keysym, keycode: u32, pressed: bool, ctrl: bool, shift: bool, _alt: bool ) + /// [`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_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`]. @@ -1071,6 +1107,9 @@ pub trait App: 'static /// next iteration without any extra wiring. fn cursor_override( &self ) -> Option { 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 { None } + /// Decide whether a given key participates in held-key repeat. /// /// Default: every non-modifier key repeats *except* `Escape`, diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 1a5571c..e2c7a4d 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -134,6 +134,7 @@ pub struct AppData pub clipboard_inbox_tx: std::sync::mpsc::Sender, /// Receiver half — drained once per run loop iteration. pub clipboard_inbox_rx: std::sync::mpsc::Receiver, + pub primary: super::primary_selection::PrimarySelection, /// Cross-application drag-and-drop: best mime negotiated on /// `enter` and the last pointer position (in logical surface diff --git a/src/event_loop/cursor_shape.rs b/src/event_loop/cursor_shape.rs index a876ef5..d0d32bc 100644 --- a/src/event_loop/cursor_shape.rs +++ b/src/event_loop/cursor_shape.rs @@ -106,9 +106,10 @@ impl AppData /// state, wins over everything. /// 2. Active slider drag — `Grabbing` while the gesture machine /// 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`]). - /// 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 ) { let target = { @@ -119,6 +120,9 @@ impl AppData .map( |w| w.cursor ) .unwrap_or( CursorShape::Default ); 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 // `Default` app override, but loses to a non-default app @@ -133,7 +137,7 @@ impl AppData } else if dragging { CursorShape::Grabbing } else { - hover_cursor + area_cursor.unwrap_or( hover_cursor ) } }; // Skip when the compositor already shows the right shape. diff --git a/src/event_loop/data_device.rs b/src/event_loop/data_device.rs index 25349ec..1dc4887 100644 --- a/src/event_loop/data_device.rs +++ b/src/event_loop/data_device.rs @@ -34,8 +34,9 @@ use std::io::Read; use std::sync::mpsc; 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::{ ReadPipe, WritePipe }; use smithay_client_toolkit::reexports::client:: { protocol:: @@ -167,23 +168,40 @@ impl DataDeviceHandler for AppData { let Some( data ) = data_device.data::() 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 tx = self.clipboard_inbox_tx.clone(); - std::thread::spawn( move || - { - let mut reader = std::fs::File::from( std::os::fd::OwnedFd::from( pipe ) ); - let mut buf = String::new(); - // 16 MiB cap. Compositors are free to keep typing into the - // pipe forever; a runaway selection should not exhaust - // memory or stall the worker thread indefinitely. - let mut bounded = (&mut reader).take( 16 * 1024 * 1024 ); - let _ = bounded.read_to_string( &mut buf ); - let _ = tx.send( buf ); - } ); + 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 ) +{ + std::thread::spawn( move || + { + let mut reader = std::fs::File::from( std::os::fd::OwnedFd::from( pipe ) ); + let mut buf = String::new(); + // 16 MiB cap. Compositors are free to keep typing into the + // pipe forever; a runaway selection should not exhaust + // memory or stall the worker thread indefinitely. + let mut bounded = (&mut reader).take( 16 * 1024 * 1024 ); + let _ = bounded.read_to_string( &mut 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 DataOfferHandler for AppData { fn source_actions( &mut self, _c: &Connection, _qh: &QueueHandle, _offer: &mut DragOffer, _actions: DndAction ) {} @@ -194,7 +212,7 @@ impl DataSourceHandler for AppData { fn accept_mime( &mut self, _c: &Connection, _qh: &QueueHandle, _source: &WlDataSource, _mime: Option ) {} - fn send_request( &mut self, _conn: &Connection, _qh: &QueueHandle, source: &WlDataSource, _mime: String, fd: smithay_client_toolkit::data_device_manager::WritePipe ) + fn send_request( &mut self, _conn: &Connection, _qh: &QueueHandle, source: &WlDataSource, _mime: String, fd: WritePipe ) { // Only honour requests for our own active source — if a stale // source is still hanging around (e.g. between a new copy and @@ -205,14 +223,7 @@ impl DataSourceHandler for AppData .map( |s| s.inner() == source ) .unwrap_or( false ); if !is_active { return; } - // SCTK's `WritePipe` derefs to `OwnedFd`; convert to a `File` - // (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() ); + write_selection( fd, &self.clipboard ); } fn cancelled( &mut self, _c: &Connection, _qh: &QueueHandle, source: &WlDataSource ) @@ -231,12 +242,11 @@ impl DataSourceHandler for AppData fn action( &mut self, _c: &Connection, _qh: &QueueHandle, _source: &WlDataSource, _action: DndAction ) {} } -/// Pick the best-supported MIME type from a selection offer. Order -/// matches `CLIPBOARD_MIMES` — UTF-8 first, then plain text, then -/// the legacy `UTF8_STRING`. -fn pick_mime( offer: &SelectionOffer ) -> Option +/// Pick the best-supported MIME type from those a selection offers. +/// Order matches `CLIPBOARD_MIMES` — UTF-8 first, then plain text, +/// then the legacy `UTF8_STRING`. +pub( super ) fn pick_text_mime( mimes: &[ String ] ) -> Option { - let mimes = offer.with_mime_types( |m| m.to_vec() ); CLIPBOARD_MIMES.iter() .find( |c| mimes.iter().any( |m| m == *c ) ) .map( |s| s.to_string() ) diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index ce2b902..8ce5374 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -442,6 +442,7 @@ impl SeatHandler for AppData self.data_device = Some( ddm.get_data_device( qh, &seat ) ); } } + self.bind_primary_selection( qh, &seat ); } fn new_capability( @@ -649,6 +650,7 @@ delegate_foreign_toplevel_list!( @ AppData ); delegate_registry!( @ AppData ); smithay_client_toolkit::delegate_activation!( @ AppData ); smithay_client_toolkit::delegate_data_device!( @ AppData ); +smithay_client_toolkit::delegate_primary_selection!( @ AppData ); // --- `xdg-activation-v1` handler --- // diff --git a/src/event_loop/mod.rs b/src/event_loop/mod.rs index da0ed5d..9bac02a 100644 --- a/src/event_loop/mod.rs +++ b/src/event_loop/mod.rs @@ -12,6 +12,7 @@ mod handlers; pub( crate ) mod osk; pub( crate ) mod output_probe; pub( crate ) mod perf; +pub( crate ) mod primary_selection; pub( crate ) mod repeat; pub( crate ) mod session; pub( crate ) mod subsurface; diff --git a/src/event_loop/primary_selection.rs b/src/event_loop/primary_selection.rs new file mode 100644 index 0000000..dfa4f2f --- /dev/null +++ b/src/event_loop/primary_selection.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: LGPL-2.1-only +// Copyright (C) 2026 Liberux Labs, S. L. + +//! `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, + device: Option, + /// Ours while we own the selection; dropping it gives the selection up. + source: Option, + pub text: String, + /// The selection of the focused text field when it was last looked at. + field: Option, + inbox_tx: mpsc::Sender, + inbox_rx: mpsc::Receiver, +} + +impl PrimarySelection +{ + pub( crate ) fn new( manager: Option ) -> 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 AppData +{ + pub( crate ) fn bind_primary_selection( &mut self, qh: &QueueHandle, 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 PrimarySelectionDeviceHandler for AppData +{ + fn selection( &mut self, _conn: &Connection, _qh: &QueueHandle, device: &ZwpPrimarySelectionDeviceV1 ) + { + let Some( offer ) = device.data::().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 PrimarySelectionSourceHandler for AppData +{ + fn send_request( &mut self, _conn: &Connection, _qh: &QueueHandle, 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, source: &ZwpPrimarySelectionSourceV1 ) + { + if self.owns_primary( source ) { self.primary.source = None; } + } +} diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index 00a6bd5..7b54e69 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -249,6 +249,8 @@ pub( crate ) fn try_run( mut app: A ) -> Result<(), RunError> let data_device_manager = smithay_client_toolkit::data_device_manager::DataDeviceManagerState::bind( &globals, &qh ).ok(); 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(); // AT-SPI2 / accessibility. `try_new` returns `None` when the // platform adapter cannot be created (no daemon on the bus, @@ -330,6 +332,7 @@ pub( crate ) fn try_run( mut app: A ) -> Result<(), RunError> clipboard_source: None, clipboard_inbox_tx, clipboard_inbox_rx, + primary, drop_position: None, drop_mime: None, drop_inbox_tx, @@ -574,6 +577,7 @@ pub( crate ) fn try_run( mut app: A ) -> Result<(), RunError> { data.clipboard = text; } + data.primary.drain(); 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 ); @@ -940,6 +944,20 @@ pub( crate ) fn try_run( mut app: A ) -> Result<(), RunError> data.clipboard = text; 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. // Walks main + every overlay because the targeted widget can live diff --git a/src/input/keyboard/mod.rs b/src/input/keyboard/mod.rs index 9bad86f..8eba403 100644 --- a/src/input/keyboard/mod.rs +++ b/src/input/keyboard/mod.rs @@ -23,7 +23,7 @@ use smithay_client_toolkit::reexports::client:: Connection, QueueHandle, }; -use crate::app::App; +use crate::app::{ App, RawKey }; use crate::event_loop::{ AppData, SurfaceFocus }; pub( super ) mod back; @@ -32,6 +32,23 @@ pub( super ) mod text_keys; pub( super ) mod shortcuts; pub( super ) mod nav; +impl AppData +{ + 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 KeyboardHandler for AppData { fn enter( @@ -71,17 +88,19 @@ impl KeyboardHandler for AppData _conn: &Connection, _qh: &QueueHandle, _keyboard: &WlKeyboard, - _serial: u32, + serial: u32, event: KeyEvent, ) { + self.last_input_serial = serial; let focus = self.keyboard_focus; // Raw observer hook (e.g. forwarding to an embedded WPE view). // Fires before the focus-aware dispatch. Back is a navigation // request, not input for the embedded system. 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 // has either released the previous key or is pressing a @@ -104,7 +123,8 @@ impl KeyboardHandler for AppData { 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 // repeating; releasing a non-repeating key (e.g. a shift that diff --git a/src/input/pointer/mod.rs b/src/input/pointer/mod.rs index ec52b72..d6e3a13 100644 --- a/src/input/pointer/mod.rs +++ b/src/input/pointer/mod.rs @@ -61,6 +61,7 @@ impl PointerHandler for AppData 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: 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. PointerEventKind::Press { button: 0x113 | 0x116, .. } => self.handle_back( focus, qh ), PointerEventKind::Release { button: 0x110, .. } => self.on_pointer_release( conn, qh, pointer, event ), diff --git a/src/input/pointer/press.rs b/src/input/pointer/press.rs index 5c097b2..1947e00 100644 --- a/src/input/pointer/press.rs +++ b/src/input/pointer/press.rs @@ -70,6 +70,25 @@ impl AppData } } + /// 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, 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( &mut self, _conn: &Connection, diff --git a/src/lib.rs b/src/lib.rs index 02b754d..d03682b 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -313,7 +313,7 @@ pub use app:: { Anchor, App, BackAtRoot, BackResponse, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec, SubsurfaceId, SubsurfaceSpec, SubsurfaceParent, - InvalidationScope, SurfaceTarget, ToplevelEvent, + InvalidationScope, RawKey, SelectionKind, SurfaceTarget, ToplevelEvent, }; pub use theme:: { diff --git a/src/system_fonts.rs b/src/system_fonts.rs index b25b78a..a785cfb 100644 --- a/src/system_fonts.rs +++ b/src/system_fonts.rs @@ -91,6 +91,10 @@ const FALLBACK_FONT_CANDIDATES: &[ FallbackFontSpec ] = // symbol, arrow and box-drawing blocks. Ships almost everywhere. 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). FallbackFontSpec { path: "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", face: 0 }, FallbackFontSpec { path: "/usr/share/fonts/noto/NotoColorEmoji.ttf", face: 0 }, diff --git a/src/widget/text_edit/mod.rs b/src/widget/text_edit/mod.rs index fef3fb9..83da2bf 100644 --- a/src/widget/text_edit/mod.rs +++ b/src/widget/text_edit/mod.rs @@ -604,6 +604,7 @@ impl TextEdit align: self.align, borderless: self.borderless, fixed_width: self.fixed_width, + pad_h: self.pad_h, font_size: self.font_size, height: self.height, select_on_focus: self.select_on_focus,