Skip to main content

iced_webview/webview/
advanced.rs

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