Skip to main content

iced_webview/webview/
basic.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU32, Ordering};
3use std::sync::Arc;
4
5use iced::advanced::image as core_image;
6use iced::advanced::{
7    self, layout,
8    renderer::{self},
9    widget::Tree,
10    Clipboard, Layout, Shell, Widget,
11};
12use iced::keyboard;
13use iced::mouse::{self, Interaction};
14use iced::{Element, Point, Size, Task};
15use iced::{Event, Length, Rectangle};
16use url::Url;
17
18use crate::webview::{common, ON_ACTION_REQUIRED};
19use crate::{engines, ImageInfo, PageType, ViewId};
20
21#[allow(missing_docs)]
22#[derive(Debug, Clone, PartialEq)]
23/// Handles Actions for Basic webview
24pub enum Action {
25    /// Changes view to the desired view index
26    ChangeView(u32),
27    /// Closes current window & makes last used view the current one
28    CloseCurrentView,
29    /// Closes specific view index
30    CloseView(u32),
31    /// Creates a new view and makes its index view + 1
32    CreateView(PageType),
33    GoBackward,
34    GoForward,
35    GoToUrl(Url),
36    Refresh,
37    SendKeyboardEvent(keyboard::Event),
38    SendMouseEvent(mouse::Event, Point),
39    /// Allows users to control when the browser engine proccesses interactions in subscriptions
40    Update,
41    Resize(Size<u32>),
42    /// Copy the current text selection to clipboard
43    CopySelection,
44    /// Internal: carries the result of a URL fetch for engines without native URL support.
45    /// On success returns `(html, css_cache)`.
46    #[doc(hidden)]
47    FetchComplete(
48        ViewId,
49        String,
50        Result<(String, HashMap<String, String>), String>,
51    ),
52    /// Internal: carries the result of an image fetch.
53    /// The bool is `redraw_on_ready` — when true, the image doesn't affect
54    /// layout so `doc.render()` can be skipped (redraw only).
55    /// The u64 is the navigation epoch — stale results are discarded.
56    #[doc(hidden)]
57    ImageFetchComplete(ViewId, String, Result<Vec<u8>, String>, bool, u64),
58    /// Internal: carries the window scale factor queried from iced.
59    #[doc(hidden)]
60    SetScaleFactor(f32),
61}
62
63/// The Basic WebView widget that creates and shows webview(s).
64///
65/// **Important:** You must drive the webview with a periodic [`Action::Update`]
66/// subscription (e.g. via `iced::time::every`). Without it the webview will
67/// never render and the screen stays blank.
68///
69/// ```rust,ignore
70/// fn subscription(&self) -> iced::Subscription<Message> {
71///     iced::time::every(std::time::Duration::from_millis(16))
72///         .map(|_| Message::WebView(Action::Update))
73/// }
74/// ```
75pub struct WebView<Engine, Message>
76where
77    Engine: engines::Engine,
78{
79    engine: Engine,
80    view_size: Size<u32>,
81    scale_factor: f32,
82    current_view_index: Option<usize>, // the index corresponding to the view_ids list of ViewIds
83    view_ids: Vec<ViewId>, // allow users to index by simple id like 0 or 1 instead of a true id
84    on_close_view: Option<Message>,
85    on_create_view: Option<Message>,
86    on_url_change: Option<Box<dyn Fn(String) -> Message>>,
87    url: String,
88    on_title_change: Option<Box<dyn Fn(String) -> Message>>,
89    title: String,
90    on_copy: Option<Box<dyn Fn(String) -> Message>>,
91    action_mapper: Option<Arc<dyn Fn(Action) -> Message + Send + Sync>>,
92    /// Number of image fetches currently in flight. Staged images are only
93    /// flushed (triggering an expensive redraw) once this reaches zero, so
94    /// a burst of images causes only one redraw instead of one per image.
95    inflight_images: usize,
96    /// Images fetched for the current navigation; capped at `MAX_IMAGES`.
97    fetched_images: usize,
98    /// Per-view navigation epoch. Incremented on `GoToUrl` so that image
99    /// fetches spawned for a previous page are discarded when they complete.
100    nav_epochs: HashMap<ViewId, u64>,
101    /// Window scale factor observed by the shader path (f32 bits; `0` = unset).
102    /// Read back in the `Update` handler to auto-correct HiDPI rendering.
103    scale_observer: Arc<AtomicU32>,
104}
105
106impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
107    fn get_current_view_id(&self) -> Option<ViewId> {
108        self.current_view_index
109            .and_then(|idx| self.view_ids.get(idx))
110            .copied()
111    }
112
113    fn index_as_view_id(&self, index: u32) -> Option<usize> {
114        self.view_ids.get(index as usize).copied()
115    }
116}
117
118impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> Default
119    for WebView<Engine, Message>
120{
121    fn default() -> Self {
122        Self::with_engine(Engine::default())
123    }
124}
125
126impl<Engine: engines::Engine, Message: Send + Clone + 'static> WebView<Engine, Message> {
127    /// Create a webview widget from a pre-built engine instance, e.g. one
128    /// constructed fallibly via the engine's `try_new()`.
129    pub fn with_engine(engine: Engine) -> Self {
130        WebView {
131            engine,
132            view_size: Size {
133                width: 1920,
134                height: 1080,
135            },
136            scale_factor: 1.0,
137            current_view_index: None,
138            view_ids: Vec::new(),
139            on_close_view: None,
140            on_create_view: None,
141            on_url_change: None,
142            url: String::new(),
143            on_title_change: None,
144            title: String::new(),
145            on_copy: None,
146            action_mapper: None,
147            inflight_images: 0,
148            fetched_images: 0,
149            nav_epochs: HashMap::new(),
150            scale_observer: Arc::new(AtomicU32::new(0)),
151        }
152    }
153}
154
155impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
156    /// Create new basic WebView widget
157    pub fn new() -> Self {
158        Self::default()
159    }
160
161    /// Override the display scale factor for HiDPI rendering.
162    /// The engine renders at `logical_size * scale_factor` pixels. The library
163    /// auto-detects the window scale factor, so calling this is only needed to
164    /// force a specific value.
165    pub fn set_scale_factor(&mut self, scale: f32) {
166        if (self.scale_factor - scale).abs() <= f32::EPSILON {
167            return;
168        }
169        self.scale_factor = scale;
170        self.engine.set_scale_factor(scale);
171    }
172
173    fn query_scale_factor(&self) -> Task<Message> {
174        common::query_scale_factor(&self.action_mapper, Action::SetScaleFactor)
175    }
176
177    /// subscribe to create view events
178    pub fn on_create_view(mut self, on_create_view: Message) -> Self {
179        self.on_create_view = Some(on_create_view);
180        self
181    }
182
183    /// subscribe to close view events
184    pub fn on_close_view(mut self, on_close_view: Message) -> Self {
185        self.on_close_view = Some(on_close_view);
186        self
187    }
188
189    /// subscribe to url change events
190    pub fn on_url_change(mut self, on_url_change: impl Fn(String) -> Message + 'static) -> Self {
191        self.on_url_change = Some(Box::new(on_url_change));
192        self
193    }
194
195    /// subscribe to title change events
196    pub fn on_title_change(
197        mut self,
198        on_title_change: impl Fn(String) -> Message + 'static,
199    ) -> Self {
200        self.on_title_change = Some(Box::new(on_title_change));
201        self
202    }
203
204    /// Subscribe to copy events (text selection copied via Ctrl+C / Cmd+C)
205    pub fn on_copy(mut self, on_copy: impl Fn(String) -> Message + 'static) -> Self {
206        self.on_copy = Some(Box::new(on_copy));
207        self
208    }
209
210    /// Provide a mapper from [`Action`] to `Message` so the webview can spawn
211    /// async tasks that route back through the iced update loop. **Required**
212    /// for litehtml and blitz engines — without it, URL navigation and image
213    /// loading will not work.
214    pub fn on_action(mut self, mapper: impl Fn(Action) -> Message + Send + Sync + 'static) -> Self {
215        self.action_mapper = Some(Arc::new(mapper));
216        self
217    }
218
219    /// Set the initial viewport size used before the first resize event.
220    /// Defaults to 1920x1080.
221    pub fn with_initial_size(mut self, size: Size<u32>) -> Self {
222        self.view_size = size;
223        self
224    }
225
226    /// Passes update to webview
227    pub fn update(&mut self, action: Action) -> Task<Message> {
228        let mut tasks = Vec::new();
229
230        // Poll only on ticks and navigation actions, not per-event actions
231        // like SendMouseEvent (get_url/get_title can be FFI calls).
232        if matches!(
233            action,
234            Action::Update
235                | Action::GoToUrl(_)
236                | Action::GoBackward
237                | Action::GoForward
238                | Action::Refresh
239                | Action::FetchComplete(..)
240        ) {
241            if let Some(view_id) = self.get_current_view_id() {
242                if let Some(on_url_change) = &self.on_url_change {
243                    let url = self.engine.get_url(view_id);
244                    if self.url != url {
245                        tasks.push(Task::done(on_url_change(url.clone())));
246                        self.url = url;
247                    }
248                }
249                if let Some(on_title_change) = &self.on_title_change {
250                    let title = self.engine.get_title(view_id);
251                    if self.title != title {
252                        tasks.push(Task::done(on_title_change(title.clone())));
253                        self.title = title;
254                    }
255                }
256            }
257        }
258
259        match action {
260            Action::ChangeView(index) => {
261                if let Some(view_id) = self.index_as_view_id(index) {
262                    self.current_view_index = Some(index as usize);
263                    self.engine.request_render(view_id);
264                    tasks.push(self.query_scale_factor());
265                } else {
266                    log::error!(
267                        "iced_webview: ChangeView index {} is invalid or already closed",
268                        index
269                    );
270                }
271            }
272            Action::CloseCurrentView => {
273                if let Some(idx) = self.current_view_index {
274                    if let Some(view_id) = self.get_current_view_id() {
275                        self.engine.remove_view(view_id);
276                        self.view_ids.remove(idx);
277                        self.current_view_index = None;
278                        if let Some(on_view_close) = &self.on_close_view {
279                            tasks.push(Task::done(on_view_close.clone()));
280                        }
281                    } else {
282                        log::error!(
283                            "iced_webview: CloseCurrentView failed — view index {} is stale",
284                            idx
285                        );
286                        self.current_view_index = None;
287                    }
288                }
289            }
290            Action::CloseView(index) => {
291                if let Some(view_id) = self.index_as_view_id(index) {
292                    self.engine.remove_view(view_id);
293                    self.view_ids.remove(index as usize);
294
295                    // Adjust current_view_index after removal
296                    if let Some(current) = self.current_view_index {
297                        if current == index as usize {
298                            self.current_view_index = None;
299                        } else if current > index as usize {
300                            self.current_view_index = Some(current - 1);
301                        }
302                    }
303
304                    if let Some(on_view_close) = &self.on_close_view {
305                        tasks.push(Task::done(on_view_close.clone()))
306                    }
307                } else {
308                    log::error!(
309                        "iced_webview: CloseView index {} is invalid or already closed",
310                        index
311                    );
312                }
313            }
314            Action::CreateView(page_type) => {
315                if let PageType::Url(url) = page_type {
316                    if !self.engine.handles_urls() {
317                        let id = self.engine.new_view(self.view_size, None);
318                        self.view_ids.push(id);
319                        self.engine.goto(id, PageType::Url(url.clone()));
320
321                        #[cfg(any(feature = "litehtml", feature = "blitz"))]
322                        if let Some(mapper) = &self.action_mapper {
323                            tasks.push(common::fetch_html_task(
324                                id,
325                                url,
326                                mapper.clone(),
327                                Action::FetchComplete,
328                            ));
329                        } else {
330                            log::error!("{ON_ACTION_REQUIRED}");
331                        }
332
333                        #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
334                        log::error!("{ON_ACTION_REQUIRED}");
335                    } else {
336                        let id = self
337                            .engine
338                            .new_view(self.view_size, Some(PageType::Url(url)));
339                        self.view_ids.push(id);
340                    }
341                } else {
342                    let id = self.engine.new_view(self.view_size, Some(page_type));
343                    self.view_ids.push(id);
344                }
345
346                if let Some(on_view_create) = &self.on_create_view {
347                    tasks.push(Task::done(on_view_create.clone()))
348                }
349                tasks.push(self.query_scale_factor());
350            }
351            Action::GoBackward => {
352                if let Some(view_id) = self.get_current_view_id() {
353                    self.engine.go_back(view_id);
354                }
355            }
356            Action::GoForward => {
357                if let Some(view_id) = self.get_current_view_id() {
358                    self.engine.go_forward(view_id);
359                }
360            }
361            Action::GoToUrl(url) => {
362                if let Some(view_id) = self.get_current_view_id() {
363                    common::begin_navigation(
364                        &mut self.nav_epochs,
365                        &mut self.inflight_images,
366                        &mut self.fetched_images,
367                        view_id,
368                    );
369                    let url_str = url.to_string();
370                    self.engine.goto(view_id, PageType::Url(url_str.clone()));
371
372                    #[cfg(any(feature = "litehtml", feature = "blitz"))]
373                    if !self.engine.handles_urls() {
374                        if let Some(mapper) = &self.action_mapper {
375                            tasks.push(common::fetch_html_task(
376                                view_id,
377                                url_str,
378                                mapper.clone(),
379                                Action::FetchComplete,
380                            ));
381                        } else {
382                            log::error!("{ON_ACTION_REQUIRED}");
383                        }
384                    }
385
386                    #[cfg(not(any(feature = "litehtml", feature = "blitz")))]
387                    if !self.engine.handles_urls() {
388                        log::error!("{ON_ACTION_REQUIRED}");
389                    }
390                }
391            }
392            Action::Refresh => {
393                if let Some(view_id) = self.get_current_view_id() {
394                    self.engine.refresh(view_id);
395                }
396            }
397            Action::SendKeyboardEvent(event) => {
398                if let Some(view_id) = self.get_current_view_id() {
399                    self.engine.handle_keyboard_event(view_id, event);
400                }
401            }
402            Action::SendMouseEvent(event, point) => {
403                if let Some(view_id) = self.get_current_view_id() {
404                    self.engine.handle_mouse_event(view_id, point, event);
405
406                    // Check if the click triggered an anchor navigation
407                    if let Some(href) = self.engine.take_anchor_click(view_id) {
408                        let current = self.engine.get_url(view_id);
409                        match common::resolve_anchor_click(&href, &current) {
410                            Some(common::AnchorTarget::Fragment(fragment)) => {
411                                self.engine.scroll_to_fragment(view_id, &fragment);
412                            }
413                            Some(common::AnchorTarget::Navigate(resolved)) => {
414                                tasks.push(self.update(Action::GoToUrl(resolved)));
415                            }
416                            None => {}
417                        }
418                    }
419                }
420
421                // Don't request_render here — the periodic Update tick handles
422                // it. Re-rendering inline on every mouse event (especially
423                // scroll) creates a new image Handle each time, causing GPU
424                // texture churn and visible gray flashes.
425                return Task::batch(tasks);
426            }
427            Action::Update => {
428                self.engine.update();
429
430                let observed = self.scale_observer.load(Ordering::Relaxed);
431                if observed != 0 {
432                    self.set_scale_factor(f32::from_bits(observed));
433                }
434
435                if let Some(view_id) = self.get_current_view_id() {
436                    self.engine.request_render(view_id);
437
438                    // Flush staged images only when all fetches are done,
439                    // so the entire batch is drawn in one pass.
440                    if self.inflight_images == 0 {
441                        self.engine.flush_staged_images(view_id, self.view_size);
442                    }
443                }
444
445                // Discover images that need fetching after layout
446                #[cfg(any(feature = "litehtml", feature = "blitz"))]
447                if let Some(mapper) = &self.action_mapper {
448                    common::dispatch_image_fetches(
449                        &mut self.engine,
450                        &self.nav_epochs,
451                        &mut self.fetched_images,
452                        &mut self.inflight_images,
453                        mapper,
454                        Action::ImageFetchComplete,
455                        &mut tasks,
456                    );
457                }
458
459                return Task::batch(tasks);
460            }
461            Action::Resize(size) => {
462                if self.view_size != size {
463                    self.view_size = size;
464                    self.engine.resize(size);
465                    tasks.push(self.query_scale_factor());
466                } else {
467                    // No-op resize: skip request_render to avoid texture churn.
468                    return Task::batch(tasks);
469                }
470            }
471            Action::CopySelection => {
472                if let Some(view_id) = self.get_current_view_id() {
473                    if let Some(text) = self.engine.get_selected_text(view_id) {
474                        if let Some(on_copy) = &self.on_copy {
475                            tasks.push(Task::done((on_copy)(text)));
476                        }
477                    }
478                }
479                return Task::batch(tasks);
480            }
481            Action::FetchComplete(view_id, url, result) => {
482                if !common::handle_fetch_complete(&mut self.engine, view_id, &url, result) {
483                    return Task::batch(tasks);
484                }
485            }
486            Action::ImageFetchComplete(view_id, src, result, redraw_on_ready, epoch) => {
487                common::handle_image_fetch_complete(
488                    &mut self.engine,
489                    &self.nav_epochs,
490                    &mut self.inflight_images,
491                    view_id,
492                    &src,
493                    &result,
494                    redraw_on_ready,
495                    epoch,
496                );
497                // Don't call request_render here — the periodic Update tick
498                // picks up staged images via request_render's staged check.
499                return Task::batch(tasks);
500            }
501            Action::SetScaleFactor(f) => {
502                self.set_scale_factor(f);
503            }
504        };
505
506        if let Some(view_id) = self.get_current_view_id() {
507            self.engine.request_render(view_id);
508        }
509
510        Task::batch(tasks)
511    }
512
513    /// Returns webview widget for the current view
514    pub fn view<'a, T: 'a>(&'a self) -> Element<'a, Action, T> {
515        let id = match self.get_current_view_id() {
516            Some(id) => id,
517            None => return iced::widget::Column::new().into(),
518        };
519        let content_height = self.engine.get_content_height(id);
520
521        if content_height > 0.0 {
522            // litehtml renders a full-document buffer: draw it with the image
523            // Handle widget and scroll by y-offset. (blitz/servo report height 0
524            // and take the shader path below.)
525            WebViewWidget::new(
526                self.view_size,
527                self.engine.get_view(id),
528                self.engine.get_cursor(id),
529                self.engine.get_selection_rects(id),
530                self.engine.get_scroll_y(id),
531                content_height,
532            )
533            .into()
534        } else {
535            // Engines that manage their own scrolling and produce a viewport-
536            // sized frame each tick (servo, blitz, cef): use the shader widget
537            // for direct GPU texture updates, avoiding Handle cache churn.
538            #[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
539            {
540                use crate::webview::shader_widget::WebViewShaderProgram;
541                iced::widget::Shader::new(WebViewShaderProgram::new(
542                    self.engine.get_view(id),
543                    self.engine.get_cursor(id),
544                    self.scale_observer.clone(),
545                ))
546                .width(Length::Fill)
547                .height(Length::Fill)
548                .into()
549            }
550            #[cfg(not(any(feature = "servo", feature = "cef", feature = "blitz")))]
551            {
552                WebViewWidget::new(
553                    self.view_size,
554                    self.engine.get_view(id),
555                    self.engine.get_cursor(id),
556                    self.engine.get_selection_rects(id),
557                    0.0,
558                    0.0,
559                )
560                .into()
561            }
562        }
563    }
564
565    /// Get the current view's image info for direct rendering
566    pub fn current_image(&self) -> Option<&crate::ImageInfo> {
567        self.get_current_view_id()
568            .map(|id| self.engine.get_view(id))
569    }
570
571    /// Get the current view's URL
572    pub fn current_url(&self) -> &str {
573        &self.url
574    }
575
576    /// Get the current view's title
577    pub fn current_title(&self) -> &str {
578        &self.title
579    }
580}
581
582#[cfg(feature = "servo")]
583impl<Message: Send + Clone + 'static> WebView<crate::engines::servo::Servo, Message> {
584    /// Event-driven subscription for the Servo engine — yields
585    /// [`Action::Update`] whenever Servo wakes the embedder, with a 500ms
586    /// fallback tick. Use this in place of a hardcoded `time::every(...)`
587    /// timer when running with the `servo` feature.
588    pub fn subscription(&self) -> iced::Subscription<Action> {
589        self.engine.subscription()
590    }
591}
592
593struct WebViewWidget<'a> {
594    handle: core_image::Handle,
595    cursor: Interaction,
596    bounds: Size<u32>,
597    selection_rects: &'a [[f32; 4]],
598    scroll_y: f32,
599    content_height: f32,
600}
601
602impl<'a> WebViewWidget<'a> {
603    fn new(
604        bounds: Size<u32>,
605        image_info: &ImageInfo,
606        cursor: Interaction,
607        selection_rects: &'a [[f32; 4]],
608        scroll_y: f32,
609        content_height: f32,
610    ) -> Self {
611        Self {
612            handle: image_info.as_handle(),
613            cursor,
614            bounds,
615            selection_rects,
616            scroll_y,
617            content_height,
618        }
619    }
620}
621
622impl<'a, Renderer, Theme> Widget<Action, Theme, Renderer> for WebViewWidget<'a>
623where
624    Renderer: iced::advanced::Renderer
625        + iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
626{
627    fn size(&self) -> Size<Length> {
628        Size {
629            width: Length::Fill,
630            height: Length::Fill,
631        }
632    }
633
634    fn layout(
635        &mut self,
636        _tree: &mut Tree,
637        _renderer: &Renderer,
638        limits: &layout::Limits,
639    ) -> layout::Node {
640        layout::Node::new(limits.max())
641    }
642
643    fn draw(
644        &self,
645        _tree: &Tree,
646        renderer: &mut Renderer,
647        _theme: &Theme,
648        _style: &renderer::Style,
649        layout: Layout<'_>,
650        _cursor: mouse::Cursor,
651        viewport: &Rectangle,
652    ) {
653        let bounds = layout.bounds();
654
655        if self.content_height > 0.0 {
656            // Draw rect is in logical coords; iced scales it to physical by the
657            // window scale factor, matching the physically-sized pixel buffer.
658            // content_height and scroll_y are logical — no scale applied here.
659            renderer.with_layer(bounds, |renderer| {
660                let image_bounds = Rectangle {
661                    x: bounds.x,
662                    y: bounds.y - self.scroll_y,
663                    width: bounds.width,
664                    height: self.content_height,
665                };
666                renderer.draw_image(
667                    core_image::Image::new(self.handle.clone())
668                        .snap(true)
669                        .filter_method(core_image::FilterMethod::Nearest),
670                    image_bounds,
671                    *viewport,
672                );
673            });
674        } else {
675            renderer.draw_image(
676                core_image::Image::new(self.handle.clone())
677                    .snap(true)
678                    .filter_method(core_image::FilterMethod::Nearest),
679                bounds,
680                *viewport,
681            );
682        }
683
684        // Selection highlights — stored in document coordinates,
685        // offset by scroll_y to match the scrolled content image.
686        if !self.selection_rects.is_empty() {
687            let rects = self.selection_rects;
688            let scroll_y = self.scroll_y;
689            renderer.with_layer(bounds, |renderer| {
690                let highlight = iced::Color::from_rgba(0.26, 0.52, 0.96, 0.3);
691                for rect in rects {
692                    let quad_bounds = Rectangle {
693                        x: bounds.x + rect[0],
694                        y: bounds.y + rect[1] - scroll_y,
695                        width: rect[2],
696                        height: rect[3],
697                    };
698                    renderer.fill_quad(
699                        renderer::Quad {
700                            bounds: quad_bounds,
701                            ..renderer::Quad::default()
702                        },
703                        highlight,
704                    );
705                }
706            });
707        }
708    }
709
710    fn update(
711        &mut self,
712        _state: &mut Tree,
713        event: &Event,
714        layout: Layout<'_>,
715        cursor: mouse::Cursor,
716        _renderer: &Renderer,
717        _clipboard: &mut dyn Clipboard,
718        shell: &mut Shell<'_, Action>,
719        _viewport: &Rectangle,
720    ) {
721        let size = Size::new(
722            layout.bounds().width.round() as u32,
723            layout.bounds().height.round() as u32,
724        );
725        if self.bounds != size {
726            self.bounds = size;
727            shell.publish(Action::Resize(size));
728        }
729
730        match event {
731            Event::Keyboard(event) => {
732                if let keyboard::Event::KeyPressed {
733                    key: keyboard::Key::Character(c),
734                    modifiers,
735                    ..
736                } = event
737                {
738                    if modifiers.command() && c.as_str() == "c" {
739                        shell.publish(Action::CopySelection);
740                    }
741                }
742                shell.publish(Action::SendKeyboardEvent(event.clone()));
743            }
744            Event::Mouse(event) => {
745                if let Some(point) = cursor.position_in(layout.bounds()) {
746                    shell.publish(Action::SendMouseEvent(*event, point));
747                } else if matches!(event, mouse::Event::CursorLeft) {
748                    shell.publish(Action::SendMouseEvent(*event, Point::ORIGIN));
749                }
750            }
751            _ => (),
752        }
753    }
754
755    fn mouse_interaction(
756        &self,
757        _state: &Tree,
758        layout: Layout<'_>,
759        cursor: mouse::Cursor,
760        _viewport: &Rectangle,
761        _renderer: &Renderer,
762    ) -> mouse::Interaction {
763        if cursor.is_over(layout.bounds()) {
764            self.cursor
765        } else {
766            mouse::Interaction::Idle
767        }
768    }
769}
770
771impl<'a, Message: 'a, Renderer, Theme> From<WebViewWidget<'a>>
772    for Element<'a, Message, Theme, Renderer>
773where
774    Renderer: advanced::Renderer + advanced::image::Renderer<Handle = advanced::image::Handle>,
775    WebViewWidget<'a>: Widget<Message, Theme, Renderer>,
776{
777    fn from(widget: WebViewWidget<'a>) -> Self {
778        Self::new(widget)
779    }
780}