Skip to main content

blinc_layout/
text_selection.rs

1//! Global text selection state for clipboard support
2//!
3//! Provides a centralized location to track what text is currently selected
4//! across all text input widgets. This enables clipboard operations (copy/cut/paste)
5//! to work with any focused text input.
6
7use std::sync::{Arc, Mutex, OnceLock};
8
9/// Source of the selected text
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum SelectionSource {
12    /// Selection from a text input widget
13    TextInput,
14    /// Selection from a text area widget
15    TextArea,
16    /// Selection from static/label text
17    StaticText,
18}
19
20/// Global text selection state
21#[derive(Debug, Clone, Default)]
22pub struct TextSelection {
23    /// The currently selected text (if any)
24    pub text: Option<String>,
25    /// Source widget type
26    pub source: Option<SelectionSource>,
27    /// Whether the selection can be cut (vs just copied)
28    pub can_cut: bool,
29}
30
31impl TextSelection {
32    /// Create an empty selection
33    pub fn empty() -> Self {
34        Self::default()
35    }
36
37    /// Create a new selection
38    pub fn new(text: String, source: SelectionSource, can_cut: bool) -> Self {
39        Self {
40            text: Some(text),
41            source: Some(source),
42            can_cut,
43        }
44    }
45
46    /// Check if there's any text selected
47    pub fn has_selection(&self) -> bool {
48        self.text.as_ref().is_some_and(|t| !t.is_empty())
49    }
50
51    /// Get the selected text
52    pub fn selected_text(&self) -> Option<&str> {
53        self.text.as_deref()
54    }
55
56    /// Clear the selection
57    pub fn clear(&mut self) {
58        self.text = None;
59        self.source = None;
60        self.can_cut = false;
61    }
62}
63
64/// Thread-safe handle to the global text selection state
65pub type SharedTextSelection = Arc<Mutex<TextSelection>>;
66
67/// Get the global text selection state
68///
69/// This is a singleton that persists for the lifetime of the application.
70/// Use this to check what text is currently selected for clipboard operations.
71pub fn global_selection() -> SharedTextSelection {
72    static GLOBAL_SELECTION: OnceLock<SharedTextSelection> = OnceLock::new();
73    Arc::clone(GLOBAL_SELECTION.get_or_init(|| Arc::new(Mutex::new(TextSelection::empty()))))
74}
75
76/// Set the global text selection
77///
78/// Call this when a text input's selection changes.
79pub fn set_selection(text: String, source: SelectionSource, can_cut: bool) {
80    let selection = global_selection();
81    let mut guard = selection.lock().unwrap();
82    *guard = TextSelection::new(text, source, can_cut);
83}
84
85/// Clear the global text selection
86///
87/// Call this when focus leaves a text input or selection is cleared.
88pub fn clear_selection() {
89    let selection = global_selection();
90    let mut guard = selection.lock().unwrap();
91    guard.clear();
92}
93
94/// Get the currently selected text (convenience function)
95pub fn get_selected_text() -> Option<String> {
96    let selection = global_selection();
97    let guard = selection.lock().unwrap();
98    guard.text.clone()
99}
100
101/// Check if the current selection can be cut
102pub fn can_cut_selection() -> bool {
103    let selection = global_selection();
104    let guard = selection.lock().unwrap();
105    guard.can_cut
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_selection_empty() {
114        let sel = TextSelection::empty();
115        assert!(!sel.has_selection());
116        assert!(sel.selected_text().is_none());
117    }
118
119    #[test]
120    fn test_selection_with_text() {
121        let sel = TextSelection::new("hello".to_string(), SelectionSource::TextInput, true);
122        assert!(sel.has_selection());
123        assert_eq!(sel.selected_text(), Some("hello"));
124        assert!(sel.can_cut);
125    }
126
127    #[test]
128    fn test_global_selection() {
129        // Clear any previous state
130        clear_selection();
131        assert!(get_selected_text().is_none());
132
133        // Set selection
134        set_selection("test text".to_string(), SelectionSource::TextInput, true);
135        assert_eq!(get_selected_text(), Some("test text".to_string()));
136        assert!(can_cut_selection());
137
138        // Clear selection
139        clear_selection();
140        assert!(get_selected_text().is_none());
141    }
142}