diff options
| author | Polesznyák Márk <contact@pml68.dev> | 2026-02-05 14:20:35 +0100 |
|---|---|---|
| committer | Polesznyák Márk <contact@pml68.dev> | 2026-02-05 14:21:32 +0100 |
| commit | 5eddb62f3cae4740680eaa81d448c3eeda88068a (patch) | |
| tree | f45e6b7afaa8ed3a2a5dea5e65685f83017c0eb7 | |
| parent | chore: bump MSRV to match iced's (diff) | |
| download | iced_selection-5eddb62f3cae4740680eaa81d448c3eeda88068a.tar.gz | |
feat: make click interval for double & triple clicks customizable
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | src/click.rs | 87 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/text.rs | 19 | ||||
| -rw-r--r-- | src/text/rich.rs | 24 |
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) + } +} @@ -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); } |
