diff --git a/CHANGELOG.md b/CHANGELOG.md index a339c689..bfcc2af9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `add_init_script` to `Page` for scripts before navigation +- Add `click_with` to `Page` and `Element` for custom click behavior ## [0.8.0] 2025-11-28 diff --git a/chromiumoxide_types/src/lib.rs b/chromiumoxide_types/src/lib.rs index ce6f022e..1c4c624e 100644 --- a/chromiumoxide_types/src/lib.rs +++ b/chromiumoxide_types/src/lib.rs @@ -266,3 +266,70 @@ impl From for Binary { Self(expr) } } + +/// Options that control how a click action is performed. +#[derive(Clone, Debug)] +pub struct ClickOptions { + /// Number of times the click action should be executed. + /// + /// A value of `1` represents a single click. + pub click_count: i64, +} + +impl Default for ClickOptions { + /// Creates a [`ClickOptions`] instance with default values. + /// + /// Default values: + /// - `click_count`: `1` + fn default() -> Self { + Self { click_count: 1 } + } +} + +impl ClickOptions { + /// Creates a new [`ClickOptions`] using default values. + pub fn new() -> Self { + Self::default() + } + + /// Creates a builder for constructing [`ClickOptions`]. + pub fn builder() -> ClickOptionsBuilder { + ClickOptionsBuilder::default() + } +} + +/// Builder for [`ClickOptions`]. +#[derive(Clone, Debug)] +pub struct ClickOptionsBuilder { + click_count: i64, +} + +impl Default for ClickOptionsBuilder { + /// Creates a [`ClickOptionsBuilder`] with default values. + /// + /// Default values: + /// - `click_count`: `1` + fn default() -> Self { + Self { click_count: 1 } + } +} + +impl ClickOptionsBuilder { + /// Creates a new [`ClickOptionsBuilder`]. + pub fn new() -> Self { + Self::default() + } + + /// Sets how many times the click action should be executed. + pub fn click_count(mut self, count: impl Into) -> Self { + self.click_count = count.into(); + self + } + + /// Builds the [`ClickOptions`] instance. + pub fn build(self) -> ClickOptions { + ClickOptions { + click_count: self.click_count, + } + } +} diff --git a/src/element.rs b/src/element.rs index 9fcc0c78..37f3632e 100644 --- a/src/element.rs +++ b/src/element.rs @@ -4,6 +4,7 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use chromiumoxide_types::ClickOptions; use futures::{future, Future, FutureExt, Stream}; use chromiumoxide_cdp::cdp::browser_protocol::dom::{ @@ -271,6 +272,19 @@ impl Element { Ok(self) } + /// Clicks the element using the provided [`ClickOptions`]. + /// + /// This behaves the same as [`click()`], but allows customizing + /// click behavior such as click count. + /// + /// Note that if the click triggers a navigation, this element + /// may no longer exist afterwards. + pub async fn click_with(&self, options: ClickOptions) -> Result<&Self> { + let center = self.scroll_into_view().await?.clickable_point().await?; + self.tab.click_with(center, options).await?; + Ok(self) + } + /// Type the input /// /// # Example type text into an input element diff --git a/src/handler/page.rs b/src/handler/page.rs index 294317da..505bf955 100644 --- a/src/handler/page.rs +++ b/src/handler/page.rs @@ -191,11 +191,21 @@ impl PageInner { /// Performs a mouse click event at the point's location pub async fn click(&self, point: Point) -> Result<&Self> { + let default_opts = chromiumoxide_types::ClickOptions::default(); + self.click_with(point, default_opts).await + } + + /// Performs a mouse click event at the point's location with custom options + pub async fn click_with( + &self, + point: Point, + options: chromiumoxide_types::ClickOptions, + ) -> Result<&Self> { let cmd = DispatchMouseEventParams::builder() .x(point.x) .y(point.y) .button(MouseButton::Left) - .click_count(1); + .click_count(options.click_count); self.move_mouse(point) .await? diff --git a/src/page.rs b/src/page.rs index df8da0b2..870afe89 100644 --- a/src/page.rs +++ b/src/page.rs @@ -507,7 +507,7 @@ impl Page { /// immediately loaded when `click()` resolves. To wait until navigation is /// finished an additional `wait_for_navigation()` is required: /// - /// # Example + /// # Examples /// /// Trigger a navigation and wait until the triggered navigation is finished /// @@ -521,9 +521,28 @@ impl Page { /// # } /// ``` /// - /// # Example /// - /// Perform custom click + /// Use [`click_with()`] to perform a custom click: + /// + /// ```no_run + /// # use chromiumoxide::page::Page; + /// # use chromiumoxide::error::Result; + /// # use chromiumoxide::layout::Point; + /// # use chromiumoxide::types::ClickOptions; + /// # async fn demo(page: Page, point: Point) -> Result<()> { + /// let options = ClickOptions::builder() + /// .click_count(2) + /// .build(); + /// + /// page.click_with(point, options).await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// ## Advanced + /// + /// For advanced use cases, the same behavior can be achieved manually by + /// issuing `DispatchMouseEventParams` commands directly via the CDP API. /// /// ```no_run /// # use chromiumoxide::page::Page; @@ -561,6 +580,46 @@ impl Page { Ok(self) } + /// Performs a mouse click event at the point's location using the provided + /// [`ClickOptions`]. + /// + /// This behaves the same as [`click()`], but allows customizing click behavior + /// such as click count or other click-related options. + /// + /// The point is scrolled into view first, then the corresponding + /// `DispatchMouseEventParams` commands are issued according to the supplied + /// options. + /// + /// Bear in mind that if `click_with()` triggers a navigation, the new page is + /// not immediately loaded when this function resolves. To wait until navigation + /// is finished, an additional [`wait_for_navigation()`] is required. + /// + /// # Example + /// + /// Perform a double click using [`ClickOptions`] + /// + /// ```no_run + /// # use chromiumoxide::page::Page; + /// # use chromiumoxide::error::Result; + /// # use chromiumoxide::layout::Point; + /// # use chromiumoxide::types::ClickOptions; + /// # async fn demo(page: Page, point: Point) -> Result<()> { + /// let options = ClickOptions::builder() + /// .click_count(2) + /// .build(); + /// + /// page.click_with(point, options) + /// .await? + /// .wait_for_navigation() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn click_with(&self, point: Point, options: ClickOptions) -> Result<&Self> { + self.inner.click_with(point, options).await?; + Ok(self) + } + /// Dispatches a `mousemove` event and moves the mouse to the position of /// the `point` where `Point.x` is the horizontal position of the mouse and /// `Point.y` the vertical position of the mouse.