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