Skip to main content

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