Skip to main content

iced_webview/webview/
basic.rs

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