aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPolesznyák Márk <contact@pml68.dev>2026-02-05 14:20:35 +0100
committerPolesznyák Márk <contact@pml68.dev>2026-02-05 14:21:32 +0100
commit5eddb62f3cae4740680eaa81d448c3eeda88068a (patch)
treef45e6b7afaa8ed3a2a5dea5e65685f83017c0eb7
parentchore: bump MSRV to match iced's (diff)
downloadiced_selection-5eddb62f3cae4740680eaa81d448c3eeda88068a.tar.gz
feat: make click interval for double & triple clicks customizable
-rw-r--r--CHANGELOG.md2
-rw-r--r--src/click.rs87
-rw-r--r--src/lib.rs1
-rw-r--r--src/text.rs19
-rw-r--r--src/text/rich.rs24
5 files changed, 125 insertions, 8 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 939590e..7c0f86a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- Customizable double & triple click interval
## [0.4.0] - 2025-12-30
### Added
diff --git a/src/click.rs b/src/click.rs
new file mode 100644
index 0000000..83e6956
--- /dev/null
+++ b/src/click.rs
@@ -0,0 +1,87 @@
+use crate::core::Point;
+use crate::core::mouse::Button;
+use crate::core::time::{Duration, Instant};
+
+/// A mouse click.
+#[derive(Debug, Clone, Copy)]
+pub struct Click {
+ kind: Kind,
+ button: Button,
+ position: Point,
+ time: Instant,
+ click_interval: Duration,
+}
+
+/// The kind of mouse click.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Kind {
+ /// A single click
+ Single,
+
+ /// A double click
+ Double,
+
+ /// A triple click
+ Triple,
+}
+
+impl Kind {
+ fn next(self) -> Kind {
+ match self {
+ Kind::Single => Kind::Double,
+ Kind::Double => Kind::Triple,
+ Kind::Triple => Kind::Double,
+ }
+ }
+}
+
+impl Click {
+ /// Creates a new [`Click`] with the given position and previous last
+ /// [`Click`].
+ pub fn new(
+ position: Point,
+ button: Button,
+ previous: Option<Click>,
+ click_interval: Option<Duration>,
+ ) -> Click {
+ let time = Instant::now();
+
+ let kind = if let Some(previous) = previous {
+ if previous.is_consecutive(position, time)
+ && button == previous.button
+ {
+ previous.kind.next()
+ } else {
+ Kind::Single
+ }
+ } else {
+ Kind::Single
+ };
+
+ Click {
+ kind,
+ button,
+ position,
+ time,
+ click_interval: click_interval
+ .unwrap_or(Duration::from_millis(300)),
+ }
+ }
+
+ pub fn kind(&self) -> Kind {
+ self.kind
+ }
+
+ fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
+ let duration = if time > self.time {
+ Some(time - self.time)
+ } else {
+ None
+ };
+
+ self.position.distance(new_position) < 6.0
+ && duration
+ .map(|duration| duration <= self.click_interval)
+ .unwrap_or(false)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 34c1f73..3ca34b7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@
//! [`iced`]: https://iced.rs
//! [`Paragraph`]: https://docs.iced.rs/iced_graphics/text/paragraph/struct.Paragraph.html
+mod click;
#[cfg(feature = "markdown")]
pub mod markdown;
pub mod selection;
diff --git a/src/text.rs b/src/text.rs
index 851e77b..72bdbf8 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -24,15 +24,16 @@ pub use rich::Rich;
use text::{Alignment, LineHeight, Shaping, Wrapping};
pub use text::{Fragment, Highlighter, IntoFragment, Span};
+use crate::click;
use crate::core::alignment;
use crate::core::clipboard;
use crate::core::keyboard::{self, key};
use crate::core::layout;
use crate::core::mouse;
-use crate::core::mouse::click;
use crate::core::renderer;
use crate::core::text;
use crate::core::text::paragraph::Paragraph as _;
+use crate::core::time::Duration;
use crate::core::touch;
use crate::core::widget::Operation;
use crate::core::widget::text::Format;
@@ -69,6 +70,7 @@ pub struct Text<
{
fragment: Fragment<'a>,
format: Format<Renderer::Font>,
+ click_interval: Option<Duration>,
class: Theme::Class<'a>,
}
@@ -82,6 +84,7 @@ where
Self {
fragment: fragment.into_fragment(),
format: Format::default(),
+ click_interval: None,
class: Theme::default(),
}
}
@@ -150,6 +153,15 @@ where
self
}
+ /// The maximum delay required for two consecutive clicks to be interpreted as a double click
+ /// (also applies to triple clicks).
+ ///
+ /// Defaults to 300ms.
+ pub fn click_interval(mut self, click_interval: Duration) -> Self {
+ self.click_interval = Some(click_interval);
+ self
+ }
+
/// Sets the style of the [`Text`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -176,7 +188,7 @@ pub struct State {
is_hovered: bool,
selection: Selection,
dragging: Option<Dragging>,
- last_click: Option<mouse::Click>,
+ last_click: Option<click::Click>,
keyboard_modifiers: keyboard::Modifiers,
visual_lines_bounds: Vec<core::Rectangle>,
}
@@ -393,10 +405,11 @@ where
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(position) = cursor.position_over(bounds) {
- let click = mouse::Click::new(
+ let click = click::Click::new(
position,
mouse::Button::Left,
state.last_click,
+ self.click_interval,
);
let (line, index) = state
diff --git a/src/text/rich.rs b/src/text/rich.rs
index 865f64a..c148566 100644
--- a/src/text/rich.rs
+++ b/src/text/rich.rs
@@ -1,6 +1,7 @@
use iced_widget::graphics::text::Paragraph;
use iced_widget::graphics::text::cosmic_text;
+use crate::click;
use crate::core::alignment;
use crate::core::clipboard;
use crate::core::keyboard;
@@ -9,6 +10,7 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::{Paragraph as _, Span};
+use crate::core::time::Duration;
use crate::core::touch;
use crate::core::widget::text::{Alignment, LineHeight, Shaping, Wrapping};
use crate::core::widget::tree::{self, Tree};
@@ -40,6 +42,7 @@ pub struct Rich<
align_x: Alignment,
align_y: alignment::Vertical,
wrapping: Wrapping,
+ click_interval: Option<Duration>,
class: Theme::Class<'a>,
on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
on_link_hover: Option<Box<dyn Fn(Link) -> Message + 'a>>,
@@ -66,6 +69,7 @@ where
align_x: Alignment::Default,
align_y: alignment::Vertical::Top,
wrapping: Wrapping::default(),
+ click_interval: None,
class: Theme::default(),
on_link_click: None,
on_link_hover: None,
@@ -140,6 +144,15 @@ where
self
}
+ /// The maximum delay required for two consecutive clicks to be interpreted as a double click
+ /// (also applies to triple clicks).
+ ///
+ /// Defaults to 300ms.
+ pub fn click_interval(mut self, click_interval: Duration) -> Self {
+ self.click_interval = Some(click_interval);
+ self
+ }
+
/// Sets the message that will be produced when a link of the [`Rich`] text
/// is clicked.
///
@@ -223,7 +236,7 @@ struct State<Link> {
is_hovered: bool,
selection: Selection,
dragging: Option<Dragging>,
- last_click: Option<mouse::Click>,
+ last_click: Option<click::Click>,
keyboard_modifiers: keyboard::Modifiers,
visual_lines_bounds: Vec<core::Rectangle>,
}
@@ -588,10 +601,11 @@ where
}
if let Some(position) = cursor.position_over(bounds) {
- let click = mouse::Click::new(
+ let click = click::Click::new(
position,
mouse::Button::Left,
state.last_click,
+ self.click_interval,
);
let (line, index) = state
@@ -599,7 +613,7 @@ where
.unwrap_or((0, 0));
match click.kind() {
- mouse::click::Kind::Single => {
+ click::Kind::Single => {
let new_end = SelectionEnd { line, index };
if state.keyboard_modifiers.shift() {
@@ -610,7 +624,7 @@ where
state.dragging = Some(Dragging::Grapheme);
}
- mouse::click::Kind::Double => {
+ click::Kind::Double => {
state.selection.select_word(
line,
index,
@@ -618,7 +632,7 @@ where
);
state.dragging = Some(Dragging::Word);
}
- mouse::click::Kind::Triple => {
+ click::Kind::Triple => {
state.selection.select_line(line, &state.paragraph);
state.dragging = Some(Dragging::Line);
}