Skip to main content

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