Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions examples/09-reference/all_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ fn app() -> Element {
}
div {
style: "padding: 50px;",
div {
style: "display: grid; gap: 12px; max-width: 520px; margin: 0 auto 24px; font-family: sans-serif;",
label {
r#for: "selection-input",
"Select text in the input to inspect the selection range and direction"
}
input {
id: "selection-input",
value: "Select part of this text to fire selection events",
style: "font: inherit; padding: 8px 10px;",
onselect: move |event: Event<SelectionData>| log_event(Rc::new(event.data().selection())),
onselectstart: move |event: Event<SelectionData>| log_event(Rc::new(event.data().selection())),
onselectionchange: move |event: Event<SelectionData>| log_event(Rc::new(event.data().selection())),
}
textarea {
style: "font: inherit; padding: 8px 10px; min-height: 80px;",
onselect: move |event: Event<SelectionData>| log_event(Rc::new(event.data().selection())),
onselectstart: move |event: Event<SelectionData>| log_event(Rc::new(event.data().selection())),
onselectionchange: move |event: Event<SelectionData>| log_event(Rc::new(event.data().selection())),
"Selection events also include textarea ranges and direction.",
}
}
div {
style: "text-align: center; padding: 20px; font-family: sans-serif; overflow: auto; height: 400px;",
onscroll: move |event: Event<ScrollData>| {
Expand Down
153 changes: 147 additions & 6 deletions packages/html/src/events/selection.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,54 @@
use dioxus_core::Event;
use std::ops::Range;

pub type SelectionEvent = Event<SelectionData>;

/// The direction a text selection was created in.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "lowercase"))]
pub enum SelectionDirection {
/// The selection direction is unknown or directionless.
#[default]
None,
/// The focus is after the anchor.
Forward,
/// The focus is before the anchor.
Backward,
}

/// The selection inside a text control.
///
/// The range is measured in UTF-16 code units to match the browser selection
/// APIs on `input` and `textarea`.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TextSelection {
range: Range<usize>,
direction: SelectionDirection,
}

impl TextSelection {
/// Create a new text selection from a UTF-16 range and selection direction.
pub fn new(range: Range<usize>, direction: SelectionDirection) -> Self {
Self { range, direction }
}

/// The selected UTF-16 range.
pub fn range(&self) -> Range<usize> {
self.range.clone()
}

/// The direction the range was selected in.
pub fn direction(&self) -> SelectionDirection {
self.direction
}

/// Returns `true` if the selection is a caret with no selected text.
pub fn is_collapsed(&self) -> bool {
self.range.is_empty()
}
}

pub struct SelectionData {
inner: Box<dyn HasSelectionData>,
}
Expand All @@ -14,6 +61,17 @@ impl SelectionData {
}
}

/// The selection inside a text control.
///
/// This is only populated for event targets that expose the text-control
/// selection APIs, such as `input` and `textarea` on the web. Some
/// selection events, notably `selectstart`, can also fire when selecting
/// normal document text. Those document selections are exposed by browser
/// APIs like `document.getSelection()` and intentionally return `None` here.
pub fn selection(&self) -> Option<TextSelection> {
self.inner.selection()
}

/// Downcast this event to a concrete event type
#[inline(always)]
pub fn downcast<T: 'static>(&self) -> Option<&T> {
Expand All @@ -29,30 +87,79 @@ impl<E: HasSelectionData> From<E> for SelectionData {

impl std::fmt::Debug for SelectionData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SelectionData").finish()
f.debug_struct("SelectionData")
.field("selection", &self.selection())
.finish()
}
}

impl PartialEq for SelectionData {
fn eq(&self, _other: &Self) -> bool {
true
fn eq(&self, other: &Self) -> bool {
self.selection() == other.selection()
}
}

#[cfg(feature = "serialize")]
/// A serialized version of SelectionData
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedSelectionData {}
pub struct SerializedSelectionData {
#[serde(default)]
pub selection_start: Option<usize>,
#[serde(default)]
pub selection_end: Option<usize>,
#[serde(default)]
pub selection_direction: Option<SelectionDirection>,
}

#[cfg(feature = "serialize")]
impl SerializedSelectionData {
/// Create a new serialized selection data object.
pub fn new(
selection_start: Option<usize>,
selection_end: Option<usize>,
selection_direction: Option<SelectionDirection>,
) -> Self {
Self {
selection_start,
selection_end,
selection_direction,
}
}
}

#[cfg(feature = "serialize")]
impl Default for SerializedSelectionData {
fn default() -> Self {
Self::new(None, None, None)
}
}

#[cfg(feature = "serialize")]
impl From<&SelectionData> for SerializedSelectionData {
fn from(_: &SelectionData) -> Self {
Self {}
fn from(data: &SelectionData) -> Self {
let Some(selection) = data.selection() else {
return Self::default();
};
let range = selection.range();
Self::new(
Some(range.start),
Some(range.end),
Some(selection.direction()),
)
}
}

#[cfg(feature = "serialize")]
impl HasSelectionData for SerializedSelectionData {
fn selection(&self) -> Option<TextSelection> {
let start = self.selection_start?;
let end = self.selection_end?;
Some(TextSelection::new(
start..end,
self.selection_direction.unwrap_or_default(),
))
}

fn as_any(&self) -> &dyn std::any::Any {
self
}
Expand All @@ -76,6 +183,40 @@ impl<'de> serde::Deserialize<'de> for SelectionData {
}

pub trait HasSelectionData: std::any::Any {
/// The selection inside a text control.
///
/// Return `None` when the event did not originate from a text control with
/// selection offsets. Document selections should use a separate API instead
/// of being mixed into this payload.
fn selection(&self) -> Option<TextSelection> {
None
}

/// return self as Any
fn as_any(&self) -> &dyn std::any::Any;
}

#[cfg(all(test, feature = "serialize"))]
mod tests {
use super::*;

#[test]
fn serialized_selection_data_deserializes_missing_fields() {
let data: SerializedSelectionData = serde_json::from_str("{}").unwrap();
assert_eq!(data, SerializedSelectionData::default());
}

#[test]
fn selection_data_exposes_serialized_fields() {
let event = SelectionData::new(SerializedSelectionData::new(
Some(1),
Some(4),
Some(SelectionDirection::Forward),
));

assert_eq!(
event.selection(),
Some(TextSelection::new(1..4, SelectionDirection::Forward))
);
}
}
2 changes: 1 addition & 1 deletion packages/interpreter/src/js/hash.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[17669692872757955279, 11420464406527728232, 3770103091118609057, 5444526391971481782, 18429234726379217184, 5052021921702764563, 11493752756395680038, 11339769846046015954]
[17669692872757955279, 11420464406527728232, 3770103091118609057, 5444526391971481782, 18429234726379217184, 5052021921702764563, 15111342379266332823, 11339769846046015954]
Loading
Loading