Skip to main content

dioxus_html/events/
selection.rs

1use dioxus_core::Event;
2use std::ops::Range;
3
4pub type SelectionEvent = Event<SelectionData>;
5
6/// The direction a text selection was created in.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "serialize", serde(rename_all = "lowercase"))]
10pub enum SelectionDirection {
11    /// The selection direction is unknown or directionless.
12    #[default]
13    None,
14    /// The focus is after the anchor.
15    Forward,
16    /// The focus is before the anchor.
17    Backward,
18}
19
20/// The selection inside a text control.
21///
22/// The range is measured in UTF-16 code units to match the browser selection
23/// APIs on `input` and `textarea`.
24#[derive(Clone, Debug, PartialEq, Eq, Hash)]
25pub struct TextSelection {
26    range: Range<usize>,
27    direction: SelectionDirection,
28}
29
30impl TextSelection {
31    /// Create a new text selection from a UTF-16 range and selection direction.
32    pub fn new(range: Range<usize>, direction: SelectionDirection) -> Self {
33        Self { range, direction }
34    }
35
36    /// The selected UTF-16 range.
37    pub fn range(&self) -> Range<usize> {
38        self.range.clone()
39    }
40
41    /// The direction the range was selected in.
42    pub fn direction(&self) -> SelectionDirection {
43        self.direction
44    }
45
46    /// Returns `true` if the selection is a caret with no selected text.
47    pub fn is_collapsed(&self) -> bool {
48        self.range.is_empty()
49    }
50}
51
52pub struct SelectionData {
53    inner: Box<dyn HasSelectionData>,
54}
55
56impl SelectionData {
57    /// Create a new SelectionData
58    pub fn new(inner: impl HasSelectionData + 'static) -> Self {
59        Self {
60            inner: Box::new(inner),
61        }
62    }
63
64    /// The selection inside a text control.
65    ///
66    /// This is only populated for event targets that expose the text-control
67    /// selection APIs, such as `input` and `textarea` on the web. Some
68    /// selection events, notably `selectstart`, can also fire when selecting
69    /// normal document text. Those document selections are exposed by browser
70    /// APIs like `document.getSelection()` and intentionally return `None` here.
71    pub fn selection(&self) -> Option<TextSelection> {
72        self.inner.selection()
73    }
74
75    /// Downcast this event to a concrete event type
76    #[inline(always)]
77    pub fn downcast<T: 'static>(&self) -> Option<&T> {
78        self.inner.as_any().downcast_ref::<T>()
79    }
80}
81
82impl<E: HasSelectionData> From<E> for SelectionData {
83    fn from(e: E) -> Self {
84        Self { inner: Box::new(e) }
85    }
86}
87
88impl std::fmt::Debug for SelectionData {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("SelectionData")
91            .field("selection", &self.selection())
92            .finish()
93    }
94}
95
96impl PartialEq for SelectionData {
97    fn eq(&self, other: &Self) -> bool {
98        self.selection() == other.selection()
99    }
100}
101
102#[cfg(feature = "serialize")]
103/// A serialized version of SelectionData
104#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
105pub struct SerializedSelectionData {
106    #[serde(default)]
107    pub selection_start: Option<usize>,
108    #[serde(default)]
109    pub selection_end: Option<usize>,
110    #[serde(default)]
111    pub selection_direction: Option<SelectionDirection>,
112}
113
114#[cfg(feature = "serialize")]
115impl SerializedSelectionData {
116    /// Create a new serialized selection data object.
117    pub fn new(
118        selection_start: Option<usize>,
119        selection_end: Option<usize>,
120        selection_direction: Option<SelectionDirection>,
121    ) -> Self {
122        Self {
123            selection_start,
124            selection_end,
125            selection_direction,
126        }
127    }
128}
129
130#[cfg(feature = "serialize")]
131impl Default for SerializedSelectionData {
132    fn default() -> Self {
133        Self::new(None, None, None)
134    }
135}
136
137#[cfg(feature = "serialize")]
138impl From<&SelectionData> for SerializedSelectionData {
139    fn from(data: &SelectionData) -> Self {
140        let Some(selection) = data.selection() else {
141            return Self::default();
142        };
143        let range = selection.range();
144        Self::new(
145            Some(range.start),
146            Some(range.end),
147            Some(selection.direction()),
148        )
149    }
150}
151
152#[cfg(feature = "serialize")]
153impl HasSelectionData for SerializedSelectionData {
154    fn selection(&self) -> Option<TextSelection> {
155        let start = self.selection_start?;
156        let end = self.selection_end?;
157        Some(TextSelection::new(
158            start..end,
159            self.selection_direction.unwrap_or_default(),
160        ))
161    }
162
163    fn as_any(&self) -> &dyn std::any::Any {
164        self
165    }
166}
167
168#[cfg(feature = "serialize")]
169impl serde::Serialize for SelectionData {
170    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
171        SerializedSelectionData::from(self).serialize(serializer)
172    }
173}
174
175#[cfg(feature = "serialize")]
176impl<'de> serde::Deserialize<'de> for SelectionData {
177    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
178        let data = SerializedSelectionData::deserialize(deserializer)?;
179        Ok(Self {
180            inner: Box::new(data),
181        })
182    }
183}
184
185pub trait HasSelectionData: std::any::Any {
186    /// The selection inside a text control.
187    ///
188    /// Return `None` when the event did not originate from a text control with
189    /// selection offsets. Document selections should use a separate API instead
190    /// of being mixed into this payload.
191    fn selection(&self) -> Option<TextSelection> {
192        None
193    }
194
195    /// return self as Any
196    fn as_any(&self) -> &dyn std::any::Any;
197}
198
199#[cfg(all(test, feature = "serialize"))]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn serialized_selection_data_deserializes_missing_fields() {
205        let data: SerializedSelectionData = serde_json::from_str("{}").unwrap();
206        assert_eq!(data, SerializedSelectionData::default());
207    }
208
209    #[test]
210    fn selection_data_exposes_serialized_fields() {
211        let event = SelectionData::new(SerializedSelectionData::new(
212            Some(1),
213            Some(4),
214            Some(SelectionDirection::Forward),
215        ));
216
217        assert_eq!(
218            event.selection(),
219            Some(TextSelection::new(1..4, SelectionDirection::Forward))
220        );
221    }
222}