Skip to main content

iced_webview/webview/
basic.rs

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